Python Variables and Types — Deep Dive
Variables Are Name Bindings, Not Boxes
The “box” analogy breaks down at the CPython level. A Python variable is a name in a namespace that references an object in memory. The name doesn’t contain the value — it points to it.
a = [1, 2, 3]
b = a # b is another name for the same list object
b.append(4)
print(a) # [1, 2, 3, 4] — same object, modified through b
Both a and b are entries in the current namespace dictionary pointing to the same list object. This is critical to understand — Python assignment doesn’t copy values, it binds names.
You can inspect this:
print(id(a)) # e.g., 140234567890
print(id(b)) # same number — same object
print(a is b) # True
id() returns the memory address of the object in CPython (not guaranteed across implementations). is checks identity (same object), == checks equality (same value).
The CPython Object Model
Every Python object is a C struct with at minimum:
ob_refcnt— reference count for memory managementob_type— pointer to the type object- The actual value
For integers, the struct also includes ob_digit (an array of “digits” for arbitrary-precision math). For strings, it includes the character buffer and cached hash.
Small Integer Caching
CPython caches integers from -5 to 256 as singletons. This optimization means that for small integers, is behaves like ==:
x = 100; y = 100
print(x is y) # True — same cached object
x = 1000; y = 1000
print(x is y) # False — two separate objects with equal values
This is an implementation detail of CPython, not guaranteed behavior. Never rely on is for value comparison.
String Interning
Python also interns certain strings — string literals that look like identifiers (no spaces, start with a letter) are typically stored as singletons:
a = "hello"
b = "hello"
print(a is b) # True (interned)
a = "hello world"
b = "hello world"
print(a is b) # Maybe True, maybe False — CPython may or may not intern this
You can force interning with sys.intern(). This matters in hot paths where you’re doing many string equality checks — identity checks (is) are faster than equality checks (==) because they skip hash comparison.
Mutability: The Real Divide
Python types divide cleanly into mutable and immutable:
| Immutable | Mutable |
|---|---|
int, float, bool | list |
str | dict |
tuple | set |
frozenset | custom classes (usually) |
bytes | bytearray |
Immutable objects cannot be changed after creation. Operations that appear to modify them actually return new objects.
Mutable objects can be changed in-place, and all names pointing to them see the change.
The Mutable Default Argument Bug
The single most common Python gotcha for intermediate developers:
def append_to(item, lst=[]): # DON'T DO THIS
lst.append(item)
return lst
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] — SURPRISE! Same list!
print(append_to(3)) # [1, 2, 3]
Default argument values are evaluated once when the function is defined, not each time it’s called. The [] is created once and reused. Since lists are mutable, it accumulates across calls.
The fix:
def append_to(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
The Type Hierarchy
Python’s type system is a class hierarchy with object at the root. Every type inherits from object:
print(isinstance(42, object)) # True
print(isinstance("hello", object)) # True
print(isinstance(int, type)) # True — int is itself a type object
type is the metaclass — the class of classes. int is an instance of type. The hierarchy:
object
├── int (also bool)
├── float
├── str
├── list
├── dict
├── tuple
├── type (metaclass)
│ ├── int
│ ├── str
│ └── ... (all type objects)
This isn’t just trivia — when you write class MyClass:, Python uses type to construct MyClass as an object. You can intercept this with custom metaclasses.
Numbers: What’s Actually Happening
Integer Arithmetic
Python integers have unlimited precision because CPython represents them as arrays of 30-bit “digits” (on 64-bit systems). Addition of two integers involves:
- Checking if they’re small enough for fast-path (single digit)
- Otherwise, allocating a new int object with enough digits
- Performing the addition with carry propagation
- Returning the new object
This is why 10 ** 1000 works, but is slower than C’s 64-bit integer addition.
Float IEEE 754
Python floats are C double — 64-bit IEEE 754. This means:
- 53 bits of significand precision (about 15-17 significant decimal digits)
- Exponent range: roughly 10^-308 to 10^308
- Special values:
float('inf'),float('-inf'),float('nan')
import math
print(math.isnan(float('nan'))) # True
print(float('inf') > 10**308) # True
For decimal arithmetic without floating-point errors:
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2')) # 0.3 — exactly
Variable Scoping: LEGB
Python resolves names using the LEGB rule — it searches scopes in this order:
- Local — inside the current function
- Enclosing — in any enclosing function (for closures)
- Global — at module level
- Built-in — Python’s built-in names (
len,print, etc.)
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local" — L wins
inner()
print(x) # "enclosing"
outer()
print(x) # "global"
To modify a global from inside a function, you need global x. For enclosing scope, nonlocal x. Without these declarations, assignment in a function always creates a new local variable.
Type Annotations and Runtime Behavior
Type hints (PEP 484, 526, 585, etc.) are syntactically valid but semantically ignored by the Python interpreter:
x: int = "this is a string" # No error at runtime!
print(x) # "this is a string"
Type annotations are stored in __annotations__:
def f(x: int) -> str:
return str(x)
print(f.__annotations__) # {'x': <class 'int'>, 'return': <class 'str'>}
Static type checkers (mypy, pyright, pytype) read these annotations and validate them without running the code. This is the pragmatic middle ground Python chose: optional static analysis without mandatory type declarations.
One Thing to Remember
Python variables are names in namespace dictionaries pointing to objects — which is why assignment doesn’t copy, why mutable defaults are dangerous, and why understanding
isvs==matters more in Python than in most other languages.
See Also
- Python Async Await Async/await helps one Python program juggle many waiting jobs at once, like a chef who keeps multiple pots moving without standing still.
- Python Basics Python is the programming language that reads like plain English — here's why millions of beginners (and experts) choose it first.
- Python Booleans Make Booleans click with one clear analogy you can reuse whenever Python feels confusing.
- Python Break Continue Make Break Continue click with one clear analogy you can reuse whenever Python feels confusing.
- Python Closures See how Python functions can remember private information, even after the outer function has already finished.