Module 01 - CPython Internals Overview
Python Is a Magic Trick. You Are About to See Behind the Curtain.
When you write x = 42, Python does not store the number 42 somewhere called x. There is no concept of a variable as a memory address, no register assignment, no pointer arithmetic visible to you. Python makes it look effortless. You write clean, readable code and it runs.
That is the magic trick.
The trick works because underneath every Python expression is approximately 200,000 lines of C code - the CPython interpreter - doing very concrete, very mechanical things. When you call a function, CPython builds a C struct called a PyFrameObject. When you assign a variable, it stores a pointer in a C array. When you delete an object, it decrements a 64-bit integer and, if that integer hits zero, calls a function pointer stored in a type struct.
None of this is mysterious. It is just C code that most Python engineers never read.
This module changes that. By the end of it, you will be able to look at any Python snippet and describe - at the C level - exactly what the interpreter does. You will understand why your multithreaded Python program does not get faster with more cores, why sys.getsizeof(True) returns 28, why importing the same module twice is fast, and why adding __slots__ to a class cuts its memory footprint by 40%.
More importantly, you will be able to apply this knowledge. You will diagnose memory leaks, understand GIL contention in production, write C extensions that release the GIL, and make informed decisions about when to use threads vs processes vs async.
What Python Does vs. What CPython Actually Does
Here is the translation table every Python engineer should have memorised. Five common Python operations, and what the CPython interpreter actually does to execute them:
| Python Operation | What you think happens | What CPython actually does |
|---|---|---|
x = 42 | Stores the integer 42 in a variable named x | Finds (or allocates) a PyLongObject for 42 (from the small-int cache), stores a pointer to it in the current frame's fastlocals C array at the index corresponding to x |
def f(a, b): ... | Defines a function | Creates a PyCodeObject (compile-time, stored in the .pyc), then at runtime wraps it in a PyFunctionObject that captures the current __globals__ dict |
import json | Loads the json library | Checks sys.modules dict first (O(1) hash lookup); if present, returns the cached module object; if absent, finds the file, compiles it to bytecode, executes the module body in a fresh namespace, stores result in sys.modules |
for x in items: | Iterates over a collection | Calls iter(items) → PyObject_GetIter() → calls tp_iter slot on the type object; then repeatedly calls next() → iternext slot; pushes FOR_ITER opcode in the eval loop |
del x | Deletes a variable | Calls STORE_FAST with NULL (sets the slot in fastlocals to NULL); calls Py_DECREF on the old pointer; if ob_refcnt hits 0, calls tp_dealloc on the type object |
The pattern is consistent: Python syntax maps to opcode instructions, which map to C function calls, which manipulate C structs with reference counts.
Understanding this mapping is the foundation for everything else in this module.
Module Map: Six Lessons, One Coherent Picture
This module builds understanding from the ground up. Each lesson adds one layer of the interpreter stack:
Module 1 - CPython Internals
│
├── Lesson 01: CPython Architecture
│ ├── Source tree layout (Objects/, Python/, Modules/, Lib/)
│ ├── Execution pipeline: .py → tokens → AST → bytecode → eval loop
│ ├── PyObject and PyFrameObject structs
│ └── dis.dis() - reading opcode output
│ │
│ ▼
├── Lesson 02: Python Objects Under the Hood
│ ├── PyObject header: ob_refcnt + ob_type (16 bytes minimum)
│ ├── PyLongObject, PyFloatObject, PyListObject, PyDictObject layouts
│ ├── Small integer cache (-5 to 256) and string interning
│ ├── Type objects as vtables: tp_new, tp_dealloc, tp_richcompare
│ └── Attribute lookup cost: __dict__, descriptors, __slots__
│ │
│ ▼
├── Lesson 03: The GIL Explained
│ ├── Why the GIL exists: reference counting is not atomic
│ ├── How the GIL works: _PyRuntimeState.ceval.gil
│ ├── GIL release: I/O, C extensions, Py_BEGIN_ALLOW_THREADS
│ ├── Benchmark: threads vs processes for CPU vs I/O bound
│ └── Python 3.13 free-threaded mode: biased refcounting
│ │
│ ▼
├── Lesson 04: Memory Management Internals
│ ├── Allocator layers: OS → pymalloc → object allocators
│ ├── pymalloc: arenas (256KB) → pools (4KB) → blocks (8–512B)
│ ├── Free lists for int, float, list objects
│ ├── Cyclic GC: 3 generations, tp_traverse, mark-and-sweep
│ └── tracemalloc: finding what allocates your RAM
│ │
│ ▼
├── Lesson 05: Bytecode and the Compiler
│ ├── Compilation pipeline: source → tokens → CST → AST → bytecode
│ ├── The dis module: reading bytecode output
│ ├── Code objects: co_code, co_consts, co_varnames
│ ├── Peephole / AST optimiser: constant folding, dead code
│ └── Specialising adaptive interpreter (3.11+): QUICKENED opcodes
│ │
│ ▼
└── Lesson 06: CPython in Python 3.13
├── 3.11: specialising adaptive interpreter (25% speedup)
├── 3.11: _PyInterpreterFrame - zero-copy frame stack
├── 3.12: immortal objects (True, False, None, small ints)
├── 3.12: per-interpreter GIL
└── 3.13: free-threaded mode - biased refcounting, per-object locks
Each lesson is self-contained but references the others. You can read them in order (recommended) or jump to the topic most relevant to your current problem.
Prerequisites and What You Will Build
This module assumes you are comfortable with Python at the application level - you have written Python professionally, used async/await, worked with threads or multiprocessing, and you know what a GIL is by name even if the internals are fuzzy.
You do not need to know C. Every C concept introduced is explained inline. You will not write C code in this module (that comes in the C Extensions module). You will read C struct definitions, understand how they map to Python behaviour, and use Python's own introspection tools (sys, gc, dis, inspect, tracemalloc, ctypes) to peer into the runtime.
By the end of this module you will be able to:
- Read CPython source code and understand what a Python expression compiles to
- Use
dis.dis()to read bytecode and identify performance issues at the opcode level - Explain exactly why the GIL prevents CPU-bound threads from scaling, and demonstrate it with a benchmark
- Use
tracemallocto find memory leaks in production code - Understand why Python 3.11 is 25% faster than 3.10 without changing any Python code
- Make an informed decision about whether to use Python 3.13's free-threaded mode for a given workload
- Answer CPython internals questions in senior engineering interviews with precision
A Quick Orientation: The CPython Source Tree
Before the lessons dive in, it helps to know where things live in the CPython source. If you want to follow along, clone the repo:
git clone https://github.com/python/cpython.git
cd cpython
git checkout 3.13
The directories you will reference throughout this module:
cpython/
├── Objects/ # PyObject, PyLongObject, PyListObject, etc.
│ ├── object.c # Py_INCREF, Py_DECREF, PyObject_Repr
│ ├── longobject.c # int implementation
│ ├── listobject.c # list implementation
│ └── dictobject.c # dict implementation (hash table)
│
├── Python/ # The interpreter itself
│ ├── ceval.c # The evaluation loop (_PyEval_EvalFrameDefault)
│ ├── compile.c # Bytecode compiler
│ ├── ast.c # AST construction
│ └── gc.c # Cyclic garbage collector
│
├── Include/ # C header files (the public API)
│ ├── object.h # PyObject definition
│ ├── cpython/ # Internal headers (not part of stable ABI)
│ └── internal/ # Implementation details
│
├── Modules/ # Built-in and standard library C modules
│ ├── _io/ # io module
│ ├── _csv.c # csv module
│ └── socketmodule.c # socket module
│
└── Lib/ # Python standard library (pure Python)
├── os.py
├── pathlib.py
└── ...
When you see a claim in this module like "the GIL is stored in _PyRuntimeState.ceval.gil", you can verify it by looking at Include/internal/pycore_runtime.h. When you see "the eval loop is in ceval.c", you can open Python/ceval.c and search for _PyEval_EvalFrameDefault.
The source is readable. It has comments. Go look.
How to Use This Module
Each lesson opens with a concrete scenario - a production incident, a surprising benchmark, an unexpected measurement - that motivates why the topic matters. The technical content follows. Every section has runnable Python code that you can execute directly; none of the examples require special setup beyond a standard Python 3.11+ installation.
At the end of each lesson is an Interview Q&A section with five questions and detailed answers. These are the questions that come up in senior Python engineering interviews at companies that care about runtime knowledge.
Start with Lesson 01: CPython Architecture. It establishes the vocabulary and the execution model that every other lesson builds on.
