Language internals & the CPython runtime
SeniorLanguage vs implementation, bytecode compilation, what CPython stores per object, stack vs heap, shared methods, and what unpacking and swaps compile to.
Created 2 September 2026 · Updated 23 October 2026
8 concepts · detail hidden
01"Python" the language vs "CPython" the implementation — why the distinction matters
#c-python-the-language-vs-cpython-the-implementation-why-the-di“Python” the language specification says nothing about a GIL at all — it’s an implementation detail of CPython specifically, not a fundamental language-level requirement. PyPy, Jython, IronPython, and GraalPython each make their own, different choices about interpreter internals and threading, and CPython’s GIL is simply one implementation’s particular tradeoff, not a property every Python implementation is obligated to share. The flat, blanket statement “Python has a GIL” conflates the language with its most common implementation.
02Is Python compiled or interpreted — bytecode compilation then interpretation in CPython
#c-is-python-compiled-or-interpreted-bytecode-compilation-then-CPython genuinely compiles Python source code down to an intermediate bytecode representation first (cached on disk as
.pycfiles for reuse) — and then that bytecode is what actually gets interpreted, step by step, by the CPython virtual machine at runtime. So it’s accurately neither “purely interpreted” (there genuinely is a real compilation step involved) nor “compiled” in the more traditional sense of producing native machine code ahead of time (unlike PyPy’s JIT, which genuinely does eventually produce real machine code for identified hot paths). The precise, accurate framing is “compiled to an intermediate bytecode, which is then interpreted” — distinct from either of the two simpler, less accurate extremes.03What CPython actually stores in an object — beyond just its attributes
#c-what-cpython-actually-stores-in-an-object-beyond-just-its-atEvery Python object, no matter how simple, begins with a fixed-size C struct header,
PyObject, carrying at minimum two fields:ob_refcnt, the reference count the GC section above relies on, andob_type, a pointer to the object’s type/class — exactly whattype(obj)reads back. Variable-size objects (list,tuple,str, and friends) extend this withPyVarObject, addingob_size(how many elements it holds), since C code needs that count available without inspecting Python-level content. Beyond that fixed C-level header, an ordinary instance holds a pointer to its own per-instance state — normally a__dict__(see “an instance’s__dict__vs a class’s__dict__” below) unless the class defines__slots__, in which case a fixed slot array replaces it. Methods are not part of that per-instance state at all — they live exactly once on the class’s own__dict__and are found via the type pointer plus an MRO walk at access time, then wrapped into a short-lived bound-method object per access rather than being stored on the instance (see “are an object’s methods duplicated per instance” in the OOP & runtime model subtopic for the full mechanism, contrasted with Java). So the honest inventory of what a plain instance actually carries is: a refcount, a type pointer, and a dict or slot array holding just that instance’s own attribute values — the methods, the class hierarchy, and (if enabled) the__weakref__list all live at the class level, not duplicated per object.04What determines whether an object is stack- or heap-allocated
#c-what-determines-whether-an-object-is-stack--or-heap-allocateIn CPython, this question has a blunt answer: every Python object, without exception, is heap-allocated. There is no language- or interpreter-level mechanism for putting an object’s actual data on the stack — the “stack” that exists at the C level only ever holds frame-local pointers to heap objects, plus small C-level bookkeeping (return addresses, saved registers), never the Python objects themselves. This is a direct consequence of CPython’s object model: every object needs a reference count that any number of names, containers, or C-level references can independently hold onto and later release, and Python’s dynamic typing means any name can be rebound to any object at any time — the interpreter generally can’t prove, the way a static compiler or a tracing JIT can, that a given object’s lifetime is safely confined to one function call. Statically- or JIT-compiled runtimes can make this determination — the general technique is called escape analysis: an object can be safely stack-allocated only if the compiler/JIT can prove it never “escapes” the scope that created it (never returned, never stored in a field, never handed to another thread) — the JVM’s HotSpot JIT does this routinely for short-lived objects in hot methods. CPython’s own JIT-adjacent implementation, PyPy, applies a related optimization for some short-lived, provably-local objects inside traced loops — but that’s a PyPy-specific runtime optimization, not something CPython does, and not a language guarantee you can rely on when reasoning about ordinary Python code.
05Stack vs heap — the core differences
#c-stack-vs-heap-the-core-differencesStack: a fixed, per-thread region, allocated and reclaimed in strict LIFO order as functions are called and return — pushing/popping a frame is just moving a stack pointer, making allocation essentially free. Its size is comparatively small and fixed up front (commonly around a megabyte per thread by default on many platforms), so recursing too deeply overflows it — the same underlying limit
sys.setrecursionlimitexists to catch gracefully before it happens at the C level, as covered under “no tail-call optimization” in the data-structures subtopic. Stack storage is automatically reclaimed the instant a frame returns, with no separate bookkeeping needed.Heap: a single region shared across the whole process, managed by a general-purpose allocator (or, in a garbage-collected runtime, by that runtime’s own memory manager) — allocating and freeing requires real bookkeeping to find and return free blocks, so it’s meaningfully more expensive per-operation than a stack push. A heap object’s lifetime is independent of any one function call: it lives until it’s explicitly freed, or — in a reference-counted/garbage-collected language like Python — until nothing references it anymore. Its practical size limit is whatever system memory is available, not a small fixed per-thread budget, and it can fragment over time as objects of varying sizes are allocated and freed in different orders.
In everyday Python conversation, “the stack” usually really means the call stack — the chain of frames shown in a traceback — not a place where your actual data lives; the data those frames reference is, again, always on the heap.
07`*args`/`**kwargs` and `*`/`**` unpacking at the bytecode level
#c-argskwargs-and-unpacking-at-the-bytecode-levelAt the calling side,
*and**trigger dedicated bytecode instructions that unpack an iterable/mapping directly into individual positional/keyword arguments for the call, rather than passing the container itself as a single argument. At the function-definition side,*args/**kwargsinstead trigger the reverse: the interpreter collects any excess positional arguments into a genuinetuplebound toargs, and any excess keyword arguments into a genuinedictbound tokwargs.08Why `a, b = b, a` works without a temp variable — the tuple on the bytecode stack
#c-why-a-b-b-a-works-without-a-temp-variable-the-tuple-on-the-bThe right-hand side
b, ais fully evaluated first, building a temporary tuple(b, a)on the interpreter’s value stack — only after that tuple is completely constructed does unpacking assignment proceed, assigning its elements toaandbrespectively — so there’s genuinely no risk of one assignment clobbering a value the other still needs, since both original values were already safely captured into that tuple before either name gets reassigned.