Types
Python is dynamically typed: the type of a value travels with the value, not with the variable that holds it. Every variable is a reference to an object; every object carries its type at runtime. The compiler does not check types; the runtime raises TypeError when an operation is applied to an incompatible operand. The trade-off is a substantial reduction in compile-time verification (the price of dynamic typing) for a substantial increase in expressiveness and rapid development (the benefit). Modern Python admits type hints (PEP 484, since 3.5) that external tools can check; the runtime ignores them.
The language is also strongly typed in the sense that values do not implicitly convert across types: a str does not become an int without an explicit int(...) call. This distinguishes Python from JavaScript (which converts implicitly with + between strings and numbers) and PHP (which converts loosely throughout).
This page covers the principal built-in types, the conversion mechanisms, and the dynamic-typing model. The dedicated pages cover Type hints, Duck typing and protocols, and Classes and inheritance.
Built-in types
The principal built-in types:
| Type | Examples | Mutability |
|---|---|---|
int | 0, 42, -1, 1_000_000 | immutable |
float | 3.14, 1e6, inf, nan | immutable |
complex | 3 + 4j, 1j | immutable |
bool | True, False | immutable |
str | "hello", 'a', r"raw" | immutable |
bytes | b"hello", bytes([1, 2, 3]) | immutable |
bytearray | bytearray(b"hello") | mutable |
list | [1, 2, 3], [] | mutable |
tuple | (1, 2, 3), (), (1,) | immutable |
range | range(10), range(1, 11, 2) | immutable |
dict | {"a": 1, "b": 2}, {} | mutable |
set | {1, 2, 3} | mutable |
frozenset | frozenset([1, 2, 3]) | immutable |
NoneType | None | (only one value) |
type | int, str, MyClass | (a type’s type) |
Each is a class; values are instances. The type() function returns the type:
type(42) # <class 'int'>
type("hello") # <class 'str'>
type([]) # <class 'list'>
type(int) # <class 'type'>
Numeric types
int
Arbitrary-precision integers (no fixed width):
n = 42
big = 2 ** 100 # 1267650600228229401496703205376
even_bigger = factorial(1000) # arbitrarily large
Python integers are not bounded by hardware word size; the runtime uses big-integer arithmetic for values that exceed a machine word. The cost is performance; the benefit is correctness — overflow does not occur.
Numeric literals admit underscores for readability:
million = 1_000_000
hex_value = 0xff_ff
binary = 0b1010_1010
octal = 0o755
float
IEEE 754 binary64:
pi = 3.14159
e = 2.71828
inf = float("inf")
nan = float("nan")
small = 1e-6
Floating-point literals contain a . or use exponential notation. The inf and nan constants are accessible via the float constructor or math.inf / math.nan.
complex
Native complex numbers:
z = 3 + 4j
print(z.real) # 3.0
print(z.imag) # 4.0
print(abs(z)) # 5.0
The j (or J) suffix denotes the imaginary part. Complex arithmetic is standard library — cmath for transcendental functions.
bool
True and False:
flag = True
done = False
bool is a subclass of int: True == 1, False == 0. Boolean operations interoperate with integer arithmetic:
sum([True, True, False, True]) # 3
The convention is to use True and False for clarity; the integer interoperation is occasionally useful in counting.
Sequences
str
Unicode strings; immutable:
s = "hello"
s[0] # 'h'
s[1:3] # 'el'
s + " world" # "hello world"
len(s) # 5
"e" in s # True
Strings are sequences of Unicode code points. The full treatment is in Strings.
bytes and bytearray
Raw byte sequences:
b = b"hello" # bytes; immutable
ba = bytearray(b"hello") # mutable
b[0] # 104 (the integer value)
chr(104) # 'h'
# Decoding to str:
b.decode("utf-8") # "hello"
"hello".encode("utf-8") # b"hello"
The bytes type is for byte data — files, network protocols, binary formats. The str type is for text. Conversion between them requires an explicit charset.
list
Mutable, ordered, heterogeneous sequence:
xs = [1, 2, 3]
xs.append(4) # [1, 2, 3, 4]
xs.insert(0, 0) # [0, 1, 2, 3, 4]
xs.pop() # 4 (modifies xs)
xs.remove(0) # removes the first 0
xs[0] # 1
xs[-1] # 3 (negative indices count from the end)
xs[1:3] # [2, 3] (slicing)
xs + [5, 6] # [1, 2, 3, 5, 6] (concatenation, new list)
xs * 2 # [1, 2, 3, 1, 2, 3] (repetition)
Lists are the conventional default sequence. Mutating operations modify in place; concatenation produces a new list.
tuple
Immutable, ordered sequence:
t = (1, 2, 3)
t = 1, 2, 3 # parens optional
empty = ()
single = (1,) # trailing comma required for one-element tuple
t[0] # 1
t[1:] # (2, 3)
Tuples are conventionally for fixed-shape records (the type’s structure is stable) — coordinates, return values from functions, dictionary keys. For variable-length sequences, use list.
range
A lazy integer sequence:
r = range(10) # 0, 1, 2, ..., 9
r = range(1, 11) # 1, 2, ..., 10
r = range(0, 100, 5) # 0, 5, 10, ..., 95
r = range(10, 0, -1) # 10, 9, ..., 1
list(r) # materialise as a list
sum(range(1, 101)) # 5050
range produces values on demand; the entire sequence is not stored. The conventional iteration source for for loops with numeric indices.
Mappings
dict
Ordered key-value mappings (insertion-ordered since 3.7):
ages = {"alice": 30, "bob": 28}
ages["alice"] # 30
ages["carol"] = 35
del ages["bob"]
ages.get("dave", 0) # 0 (default if missing)
"alice" in ages # True
for key, value in ages.items():
print(f"{key}: {value}")
ages.keys() # dict_keys(['alice', 'carol'])
ages.values() # dict_values([30, 35])
dict is the principal Python container; nearly every program uses it heavily. Keys must be hashable (immutable in practice — int, str, tuple of hashables; not list or dict).
Dictionary unpacking and merging (since 3.5/3.9):
combined = {**a, **b} # merge a and b; b's values override
merged = a | b # since 3.9
Sets
set and frozenset
Unordered collections of unique elements:
s = {1, 2, 3}
s.add(4)
s.remove(2)
2 in s # False
a = {1, 2, 3}
b = {2, 3, 4}
a | b # {1, 2, 3, 4} (union)
a & b # {2, 3} (intersection)
a - b # {1} (difference)
a ^ b # {1, 4} (symmetric difference)
frozen = frozenset([1, 2, 3]) # immutable
The set is mutable; frozenset is the immutable variant (admits use as a dict key or set member). Both require their elements to be hashable.
Singletons
None
The single value of type NoneType. The conventional “no value” / “absence”:
x = None
result = compute() or None
if x is None:
print("missing")
The conventional discipline is to test for None with is None (identity comparison), not ==. The is form is faster and more idiomatic.
NotImplemented and Ellipsis
Two other singletons:
NotImplemented— returned by binary methods when the operation is not supported (admits__radd__fallback).Ellipsis(or...) — used in type hints, slicing, and as a placeholder.
def __add__(self, other):
if not isinstance(other, MyType):
return NotImplemented # Python tries other.__radd__
return MyType(self.x + other.x)
def todo():
... # placeholder body
Custom types: classes
User-defined types are introduced with class:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def distance(self, other: "Point") -> float:
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
p = Point(3, 4)
q = Point(0, 0)
print(p.distance(q)) # 5.0
The full treatment is in Classes and inheritance.
Object identity, equality, and hashing
Python distinguishes:
- Identity (
is): two references designate the same object. - Equality (
==): two values represent the same value. - Hashing (
hash()): the value’s hash for use in dicts and sets.
a = [1, 2, 3]
b = [1, 2, 3]
a == b # True (equality)
a is b # False (different objects)
c = a
a is c # True
hash(42) # 42
hash("hello") # implementation-defined
hash([1, 2, 3]) # TypeError: unhashable type: 'list'
The contract: equal objects must have equal hashes. Custom classes implement __eq__ and __hash__ together; defining __eq__ without __hash__ makes the class unhashable (the default).
Type conversion
Python does not perform implicit numeric conversions across types beyond the numeric tower. Conversions are explicit:
int("42") # 42
int(3.7) # 3 (truncation)
int("0xff", 16) # 255
int("1010", 2) # 10
float("3.14") # 3.14
float(42) # 42.0
str(42) # "42"
str([1, 2, 3]) # "[1, 2, 3]"
list("hello") # ['h', 'e', 'l', 'l', 'o']
list(range(5)) # [0, 1, 2, 3, 4]
tuple([1, 2, 3]) # (1, 2, 3)
set("hello") # {'h', 'e', 'l', 'o'}
dict([("a", 1), ("b", 2)]) # {"a": 1, "b": 2}
bool(0) # False
bool(1) # True
bool("") # False
bool("hello") # True
bool([]) # False
bool([0]) # True
The int(...), float(...), str(...) constructors handle the conventional conversions. The bool(...) follows Python’s truthiness rules:
False,None, numeric zeros, empty containers ("",[],{},set(),()) are falsy.- Everything else is truthy.
Custom classes can override truthiness by defining __bool__.
isinstance and issubclass
The conventional type tests:
isinstance(42, int) # True
isinstance(42, (int, float)) # True (multiple types)
isinstance(True, int) # True (bool is a subclass of int)
issubclass(bool, int) # True
issubclass(int, object) # True
isinstance is the conventional Python type-test; the type(x) is C form is rare and explicitly stricter (it does not admit subclass instances).
For protocol-based testing (duck typing), hasattr(x, 'method') and the typing.Protocol machinery; treated in Duck typing and protocols.
Numeric tower
The numbers module declares an abstract numeric tower:
from numbers import Number, Complex, Real, Rational, Integral
isinstance(42, Integral) # True
isinstance(3.14, Real) # True
isinstance(3 + 4j, Complex) # True
The hierarchy: Number ⊃ Complex ⊃ Real ⊃ Rational ⊃ Integral. The mechanism admits generic numeric code that works on any numeric type.
In practice, generic numeric code is rare; most Python uses int and float directly.
Object representation
Python does not expose memory layout directly. Every value is an object on the heap with:
- A reference count (for the garbage collector).
- A type pointer.
- The value data.
The sys.getsizeof() function returns the size in bytes:
import sys
sys.getsizeof(42) # ~28 bytes (CPython 3.x)
sys.getsizeof(42 ** 100) # larger; big-int representation
sys.getsizeof("hello") # depends on encoding
sys.getsizeof([]) # ~56 bytes (empty list)
sys.getsizeof([0] * 100) # ~952 bytes
The substantial overhead per object (CPython is not memory-efficient by language standards) is one of Python’s cost-trade-offs. For numeric workloads, NumPy’s packed arrays sidestep the overhead.
Conversions and the dispatch model
Python’s binary operators dispatch through dunder methods:
a + b # tries a.__add__(b); if NotImplemented, tries b.__radd__(a)
a < b # a.__lt__(b)
a == b # a.__eq__(b)
str(a) # a.__str__()
repr(a) # a.__repr__()
len(a) # a.__len__()
a[i] # a.__getitem__(i)
The mechanism admits operator overloading and protocol participation; the full treatment is in Operators and Duck typing and protocols.
A note on what Python’s types are not
Several conventional features are absent or different:
- No primitive vs reference distinction — every value is an object.
- No fixed-width numeric types —
intis arbitrary-precision; for fixed widths, usenumpyor thearraymodule. - No type erasure — runtime types are always available through
type(). - No type aliases at the language level —
typing.TypeAliasadmits explicit aliases for type checkers. - No interfaces in the C# sense — Python uses duck typing and
typing.Protocol. - No value vs reference parameter passing — every call is “pass by value of the reference”.
The combination — dynamic typing, gradual type hints, duck typing through dunders — is the substance of Python’s typing story. The treatment of each is in the dedicated pages.