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 management
  • ob_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:

ImmutableMutable
int, float, boollist
strdict
tupleset
frozensetcustom classes (usually)
bytesbytearray

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:

  1. Checking if they’re small enough for fast-path (single digit)
  2. Otherwise, allocating a new int object with enough digits
  3. Performing the addition with carry propagation
  4. 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:

  1. Local — inside the current function
  2. Enclosing — in any enclosing function (for closures)
  3. Global — at module level
  4. 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 is vs == matters more in Python than in most other languages.

pythonvariablesobject-modelmutabilitytype-systeminternals

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.