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:

  1. ob_refcnt: A reference count (number of pointers pointing to this object).
  2. 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:

  1. Allocates a PyLongObject on the heap to represent 42.
  2. Adds a string key "count" to the local or global symbol table (locals() or globals()), 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 payload

Because 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.

Display Options
Appearance
Text Size
100%