Variables, Types & Dynamic Typing
Python is dynamically typed: a name can refer to different object types over its lifetime, while each object still has a concrete type at runtime. Understanding reference semantics prevents subtle bugs with mutability and shared state.
Recipe
x = [1, 2, 3]
y = x # same list object
y.append(4)
print(x) # [1, 2, 3, 4]
a: str = "hi"
print(type(a), isinstance(a, str))When to reach for this:
- Debugging why two variables mutate together
- Choosing between copy vs shared reference
- Adding type hints without changing runtime behavior
- Explaining Python to developers from statically typed languages
Working Example
from copy import deepcopy
def demo_references() -> None:
original = {"items": [1, 2]}
alias = original
shallow = original.copy()
deep = deepcopy(original)
alias["items"].append(3)
shallow["items"].append(4)
deep["items"].append(99)
print("original:", original) # items mutated via alias and shallow inner list
print("shallow:", shallow) # top-level dict is new; nested list shared
print("deep:", deep) # fully independent
def demo_dynamic_typing() -> None:
value: int | str = 42
value = "forty-two" # legal - name is not locked to int
print(value, type(value))
if __name__ == "__main__":
demo_references()
demo_dynamic_typing()What this demonstrates:
- Assignment binds names to objects -
alias = originalshares the dict dict.copy()is shallow - nested mutable objects remain shareddeepcopyrecursively duplicates nested structures- Type hints use
int | strbut do not enforce types at runtime
Deep Dive
How It Works
- Names vs objects - Variables are entries in a namespace (dict) pointing to heap objects.
- Reference counting - CPython tracks object lifetimes; when refcount hits zero, memory is reclaimed.
- Immutable objects -
int,str,tuple,frozensetcannot change in place; "mutation" creates new objects. - Mutable objects -
list,dict,setchange in place; aliases see the same changes. - LEGB - Name lookup order: Local, Enclosing, Global, Built-in (covered in depth in functions article).
Core Types at a Glance
| Category | Examples | Mutable? |
|---|---|---|
| Numeric | int, float, complex, Decimal | No (rebind to change) |
| Text/binary | str, bytes, bytearray | bytearray yes |
| Sequences | list, tuple, range | list yes |
| Mappings | dict | Yes |
| Sets | set, frozenset | set yes |
Python Notes
# id() returns object identity; is compares identity
a = [1]
b = a
c = [1]
print(a is b) # True
print(a is c) # False - different list objects
print(a == c) # True - equal valuesGotchas
- Shallow copy surprises -
list.copy()andcopy.copy()still share nested mutables. Fix:deepcopywhen nesting matters. - Default mutable arguments -
def f(items=[])reuses one list. Fix:def f(items=None): items = items or []. - Rebinding vs mutating -
x = x + [1]creates a new list;x.append(1)mutates in place. Fix: Know which operation you intend. - Type hints are not runtime checks - mypy catches mistakes before deploy; Python itself ignores hints. Fix: Use Pydantic or explicit
isinstanceat boundaries. isvs==for small ints/strings - CPython may intern some literals, but never rely onisfor value comparison. Fix: Use==for values,isonly forNonesingleton checks.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
dataclasses with typed fields | Structured records with hints | Simple script variables |
| Pydantic models | Runtime validation at API edges | Inner-loop numeric code |
TypedDict | JSON-shaped dicts with static shape | Objects needing methods |
NamedTuple | Immutable lightweight records | Need mutability |
FAQs
Does Python have variables or references?
Both terms appear in docs. Practically: names bind to object references. There are no C-style variables holding raw values on the stack for arbitrary objects.
Why does changing a list through one variable affect another?
Both names reference the same list object. Assignment never clones unless you explicitly copy.
Are type hints enforced at runtime?
No. Use mypy, pyright, or Pydantic when you need enforcement. Hints are stored in __annotations__ but ignored by the interpreter.
What is the difference between type() and isinstance()?
type(x) is int fails for subclasses. Prefer isinstance(x, int) for polymorphic checks.
Can a function change a caller's variable?
Only if you mutate a shared mutable object. Rebinding a parameter name does not affect the caller's name.
What does immutability buy me?
Immutable objects are hashable (when contents are hashable) and safe as dict keys. They also prevent accidental in-place mutation via aliases.
How do I copy a nested structure?
from copy import deepcopy
clone = deepcopy(nested)Why use id()?
Debugging identity issues - confirming whether two names point at the same object during troubleshooting.
Does x = x + 1 create a new int?
Yes. Small integers may be cached, but semantically += on immutables rebinds the name to a new object.
When should I add type hints to variables?
When the type is not obvious from the assignment, or when mypy needs help narrowing unions.
Related
- Numbers, Booleans & Arithmetic - numeric types and truthiness
- Functions, Args & Scope - parameter binding and scope
- Immutability & Hashability - hashable keys
- Type Hints Basics - annotating names and functions
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.