Variables and Dynamic Typing

This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Python variables are not typed storage slots — they are names bound to objects. The object carries the type; the name is just a label that can be re-pointed at any time.

Names, Not Slots

Assignment binds a name to an object — it never copies the object. Several names can be bound to the same object at once, and a name can be rebound to an object of a completely different type:

x = [1, 2, 3]   # the name 'x' is bound to a list object
y = x           # 'y' is bound to the *same* list object, not a copy
y.append(4)
print(x)        # [1, 2, 3, 4] -- seen through 'x' too

x = "now a string"   # 'x' rebound to a new, unrelated object

This name-binding model is specified in the language reference’s data model chapter: every value in Python is an object, and a variable is nothing more than a reference to one.

Mutability: Rebinding vs. Mutating

Two very different things look similar at a glance:

  • Rebinding a name points it at a new object; the old object is unaffected (and is garbage-collected once nothing else references it).

  • Mutating an object changes its contents in place; every name bound to it sees the change.

x = 5
x = x + 1        # rebinding: 'x' now points to a *different* int object (6)

lst = [1, 2, 3]
lst.append(1)    # mutation: the *same* list object gains an element

Whether an operation rebinds or mutates depends on the object’s type. Numbers, strings, and tuples are immutable — every "modifying" operation on them actually produces a new object. Lists, dicts, and sets are mutable — methods like .append(), .update(), and .add() change the object in place. The full catalogue of built-in types and which category each falls into is in the standard type hierarchy reference.

A preview of the mutable-default-argument pitfall

Because a default argument value is created once, at function-definition time, a mutable default is shared across every call that doesn’t supply its own — covered in depth, with a worked example and the None-sentinel fix, on Functions.

Shared References: is vs. ==

== asks whether two objects have equal value; is asks whether two names are bound to the same object (identity). id() returns a number that uniquely identifies an object for its lifetime — CPython uses its memory address, but that is an implementation detail, not something to depend on.

a = [1, 2, 3]
b = [1, 2, 3]
c = a

a == b   # True  -- same contents
a is b   # False -- two distinct list objects
a is c   # True  -- 'c' is bound to the same object as 'a'

id(a) == id(c)   # True, consistent with 'a is c'

Python’s small-integer and (in many, but not all, situations) string caching means is can appear to succeed for immutable values that were never explicitly shared:

m = 5
n = 5
m is n   # True in CPython -- small ints (-5..256) are cached/interned

p = 1000
q = 1000
p is q   # not guaranteed -- may be True or False depending on context

This caching is an implementation detail of CPython, not a language guarantee — see the is / is not operators in the language reference. Never write code whose correctness depends on interning; use == to compare values and reserve is for identity checks such as x is None.

The Built-In Type Hierarchy, at a Glance

Every object’s type falls into one of a small number of built-in categories. This map is worth keeping in mind as a table of contents for the rest of this section — Collections covers sequences, mappings, and sets in depth; Functions covers callables:

# numbers      -- int, float, complex, bool (bool is a subtype of int)
# sequences    -- str, bytes, list, tuple, range
# mappings     -- dict
# sets         -- set, frozenset
# the null type -- None (the sole instance of NoneType)
# callables    -- functions, methods, classes, and any object with __call__
# classes      -- every class is itself an object, an instance of 'type'

type(42)          # <class 'int'>
type(3.14)        # <class 'float'>
type("hi")        # <class 'str'>
type([1, 2])      # <class 'list'>
type({"a": 1})    # <class 'dict'>
type(None)        # <class 'NoneType'>
type(len)         # <class 'builtin_function_or_method'>

The full, authoritative breakdown — including numeric towers, the iterator protocol, and special methods each category implements — lives in the data model chapter and the built-in types reference.

Two scenarios: rebinding a name to a new int object leaves the old object unreferenced, while mutating a list in place keeps the same name pointing at the same object whose contents changed

The figure above contrasts the two behaviors from the mutability section: on the left, x = x + 1 detaches the name from the old int object and points it at a new one; on the right, lst.append(1) keeps the name pointing at the same list object, whose contents change underneath it.

See Also

  • Collections — sequences, mappings, and sets built on top of this type hierarchy.

  • Functions — callables, and the full mutable-default-argument story previewed above.