Polyglot
Languages Python types
Python § types

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:

TypeExamplesMutability
int0, 42, -1, 1_000_000immutable
float3.14, 1e6, inf, nanimmutable
complex3 + 4j, 1jimmutable
boolTrue, Falseimmutable
str"hello", 'a', r"raw"immutable
bytesb"hello", bytes([1, 2, 3])immutable
bytearraybytearray(b"hello")mutable
list[1, 2, 3], []mutable
tuple(1, 2, 3), (), (1,)immutable
rangerange(10), range(1, 11, 2)immutable
dict{"a": 1, "b": 2}, {}mutable
set{1, 2, 3}mutable
frozensetfrozenset([1, 2, 3])immutable
NoneTypeNone(only one value)
typeint, 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: NumberComplexRealRationalIntegral. 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 typesint is arbitrary-precision; for fixed widths, use numpy or the array module.
  • No type erasure — runtime types are always available through type().
  • No type aliases at the language leveltyping.TypeAlias admits 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.