Syntax, Names & CPython Memory Allocation
At a high level, Python’s syntax is minimal—blocks are defined by indentation, and variables don’t require static type declarations. But under the hood, this elegant syntax requires a complex orchestration of symbol tables, pointer indirection, and C-struct heap allocations.
In CPython, a “variable” is never a fixed memory location holding a primitive value. It is simply a string key in a dictionary (the symbol table) pointing to a dynamically allocated C-struct on the heap.
1. CPython Internal Architecture: The PyObject Struct
Every piece of data in Python is a subclass of the fundamental C struct: PyObject.
Because Python is dynamically typed, the VM cannot rely on the compiler to enforce type safety or memory sizes. Instead, every object carries its own metadata at runtime.
A basic PyObject contains:
ob_refcnt: A reference count (number of pointers pointing to this object).ob_type: A pointer to a type object (which dictates its behaviors and methods).
For variable-length objects (like strings or lists), CPython uses PyVarObject, which adds an ob_size field indicating the number of items.
Because of these headers, a simple Python integer requires a minimum of 28 bytes of memory on a 64-bit system (8 bytes for ob_refcnt, 8 bytes for ob_type, 8 bytes for ob_size, and 4 bytes for the actual digit payload), compared to just 4 or 8 bytes for an integer in C or Dart.
2. Visual Mental Model: Names and Pointer Indirection
When you execute count = 42, CPython does not allocate a 4-byte box named “count” holding 42. Instead, it performs two distinct steps:
- Allocates a
PyLongObjecton the heap to represent42. - Adds a string key
"count"to the local or global symbol table (locals()orglobals()), mapping it to the memory address of the newly allocated object.
Memory Layout of Python Assignment: `count = 42`
[ Symbol Table (e.g., globals()) ]
Key | Value (Pointer)
----------------|----------------------
"count" | -----> [ Heap Memory Address: 0x10A4 ]
[ PyLongObject @ 0x10A4 ]
| ob_refcnt : 1 | <-- Garbage collection tracking
| ob_type : &PyLong_Type | <-- Type resolution
| ob_size : 1 |
| ob_digit : [ 42 ] | <-- Actual payloadBecause variables are merely pointers, dynamic typing is a natural consequence: rebinding a variable simply changes the pointer in the symbol table to point to a different PyObject*.
3. Scope Resolution and Symbol Tables
Python resolves variable names at runtime using the LEGB rule (Local, Enclosing, Global, Built-in).
When a function is compiled, CPython optimizes local variable lookups. Instead of performing expensive hash table lookups in locals(), local variables are assigned fixed array indices (accessed via LOAD_FAST and STORE_FAST bytecodes). Globals, however, still rely on dictionary lookups (LOAD_GLOBAL), which is why accessing global variables in hot loops is significantly slower than passing them as local arguments.