The interpreter state, thread state, frame stack, and the runtime lifecycle from Py_Initialize to Py_Finalize.
The CPython runtime is the machinery that exists after the process starts and before Python code finishes executing. It owns interpreter state, thread state, modules, builtins, memory allocators, exception state, import state, frames, pending calls, signal handling, and shutdown behavior.
A Python program appears to run as a sequence of statements. CPython runs it inside a layered runtime system.
operating system process
CPython runtime
interpreter state
thread state
frame stack
executing code object
bytecode instructions
object operationsThis hierarchy is the main map for understanding execution.
5.1 Process, Runtime, Interpreter, Thread, Frame
A running CPython program has several nested execution units.
| Unit | Meaning |
|---|---|
| Process | The OS process containing the CPython executable or embedded runtime |
| Runtime | Global CPython state shared across the process |
| Interpreter | An isolated Python interpreter state inside the runtime |
| Thread state | Per-thread execution state for one interpreter |
| Frame | One active execution context |
| Code object | Compiled bytecode and metadata |
| Object | Runtime value manipulated by bytecode |
A simple script:
x = 1
print(x + 2)runs inside a frame. That frame belongs to a thread state. The thread state belongs to an interpreter. The interpreter belongs to the CPython runtime.
Conceptually:
_PyRuntimeState
PyInterpreterState
PyThreadState
PyFrameObject or internal frame
PyCodeObject
locals
globals
builtins
value stackThe exact C structs change across versions, but the hierarchy is stable enough to guide source reading.
5.2 The Runtime
The runtime is process-wide CPython state.
It includes global services used by one or more interpreters:
runtime initialization state
memory allocator state
interpreter list
GIL state
pending calls
signal handling state
audit hook state
global caches
runtime finalization flagsAt a high level, the runtime answers:
Has CPython been initialized?
Which interpreters exist?
Is the runtime finalizing?
Which global locks and services are active?
Which process-wide hooks are installed?This matters most during startup, shutdown, embedding, subinterpreters, and free-threading work.
A normal Python script usually has one runtime and one main interpreter.
5.3 Interpreter State
An interpreter state represents one Python interpreter inside the process.
It owns language-level state such as:
modules dictionary
builtins
import state
sys module state
codec state
warnings state
garbage collector state
interned strings
per-interpreter caches
execution configurationConceptually:
PyInterpreterState
modules
builtins
sysdict
import machinery
gc generations
codec registry
pending async exception stateMost Python programs use only the main interpreter.
Subinterpreters create additional interpreter states inside the same process. They can have separate module dictionaries and runtime state, but they still share some process-level resources.
5.4 Thread State
Each OS thread that executes Python code needs a thread state.
A thread state stores execution information for one thread in one interpreter:
current frame
current exception state
recursion depth
profiling function
tracing function
async exception request
thread-local interpreter dataA thread state connects native execution to Python execution.
Conceptually:
current OS thread
PyThreadState
current interpreter
current frame
exception information
tracing and profiling hooksWhen C code needs to raise an exception, access the current frame, or interact with Python APIs, it often needs the current thread state.
5.5 Frames
A frame is an execution record.
CPython creates a frame when it executes a module body, function body, class body, generator, coroutine, or comprehension.
A frame contains:
code object
globals dictionary
builtins dictionary
locals storage
value stack
instruction pointer
block and exception state
line number state
owner informationFor a function:
def add(a, b):
c = a + b
return ccalling add(2, 3) creates a frame for that call.
The frame stores:
code object for add
a = 2
b = 3
c after assignment
temporary stack values
current bytecode offset
globals of the defining module
builtins visible to the functionA frame is the concrete runtime object that makes a function call active.
5.6 Code Objects
A code object is compiled executable content.
It is immutable metadata plus bytecode. It does not store the current values of local variables.
For example:
def f(x):
y = x + 1
return yThe function object has a code object:
code = f.__code__
print(code.co_name)
print(code.co_varnames)
print(code.co_consts)The code object says how to execute the function. The frame stores one active execution of that code.
This distinction matters:
code object: reusable compiled program
frame: one running invocation of that program
function: callable object that wraps code with globals, defaults, and closureMany calls to the same function reuse the same code object but create separate frames.
5.7 Function Objects
A Python function object wraps a code object with runtime context.
It contains:
code object
globals dictionary
defaults
keyword-only defaults
closure cells
annotations
function name
qualified name
module name
dict for custom attributesFor example:
x = 10
def f(y):
return x + yThe function f needs more than bytecode. It also needs the globals dictionary where x can be found.
Conceptually:
PyFunctionObject
func_code code for f
func_globals module globals
func_defaults default argument values
func_closure captured cellsCalling the function creates a frame using these components.
5.8 Objects and Types
At runtime, bytecode manipulates object references.
Every Python value is an object:
42
"hello"
[1, 2, 3]
{"a": 1}
lambda x: x + 1
Exception("bad")Each object has a type.
The type determines behavior:
allocation
deallocation
attribute lookup
method lookup
call behavior
numeric operations
sequence operations
mapping operations
iteration
representation
hashing
comparisonFor example:
len(x)does not directly inspect every possible object layout. It asks the object’s type how length works.
This is why the runtime model depends on type objects. Bytecode instructions are generic. Type slots provide concrete behavior.
5.9 Namespaces
Python execution uses namespaces.
The main namespace categories are:
| Namespace | Backing storage |
|---|---|
| Locals | Function fast locals, class namespace, or mapping |
| Globals | Module dictionary |
| Builtins | Builtins dictionary |
| Object attributes | Instance dict, slots, descriptors, type lookup |
| Module attributes | Module dictionary |
For this code:
x = 1
def f():
return x + len([1, 2])inside f, CPython resolves:
x global name in module dictionary
len builtin name if not found in globals
[1,2] newly created list objectName resolution depends on compile-time classification and runtime namespace lookup.
5.10 Fast Locals
Function local variables are usually stored in an optimized array-like layout, not a normal dictionary lookup for every access.
For a function:
def f(a, b):
c = a + b
return cthe compiler knows the local names:
a
b
cThe frame can store them in indexed slots.
Bytecode can then use fast local operations:
LOAD_FAST a
LOAD_FAST b
STORE_FAST cThis is faster than dictionary lookup.
A locals dictionary may be materialized when needed, for example by locals(), tracing, debugging, or frame inspection. But the normal execution path uses fast locals.
5.11 The Value Stack
CPython bytecode uses a stack machine.
Instructions push and pop temporary values from the frame’s value stack.
For:
z = x + ythe conceptual execution is:
LOAD_FAST x push x
LOAD_FAST y push y
BINARY_OP + pop x and y, push result
STORE_FAST z pop result into local zThe value stack is temporary execution storage. It is separate from local variables.
A frame therefore contains both:
local variable storage
value stack for intermediate operationsThis explains why bytecode can be compact. Instructions communicate through the stack.
5.12 Calls
A function call is one of the most important runtime operations.
For:
result = f(1, 2)CPython must:
evaluate f
evaluate arguments
prepare call layout
check callable type
create or enter callable execution
bind arguments
execute body or C function
return resultDifferent callable objects have different paths:
| Callable | Runtime path |
|---|---|
| Python function | Create frame and execute code object |
| Built-in function | Call C function wrapper |
| Method | Bind receiver and call underlying function |
| Class | Allocate and initialize instance |
Object with __call__ | Invoke type call slot |
CPython has optimized calling conventions to reduce temporary tuple and dictionary allocation. Modern CPython uses fast call paths such as vectorcall for many callable types.
5.13 Exceptions
Exceptions are runtime state plus control flow.
When Python code raises:
raise ValueError("bad")CPython records exception information in the current thread state and begins unwinding execution.
A C function usually reports failure by:
setting an exception
returning NULL or -1The caller checks the return value and propagates failure.
Conceptually:
PyErr_SetString(PyExc_ValueError, "bad");
return NULL;At bytecode level, exceptions affect:
current frame
exception table
stack unwinding
finally blocks
except matching
traceback construction
propagation to callerExceptions are not ordinary return values. They are a separate control path through the runtime.
5.14 Tracebacks
A traceback records where an exception traveled.
When an exception propagates through frames, CPython can attach traceback entries that identify:
file name
function name
line number
bytecode position
frameFor example:
def a():
b()
def b():
1 / 0
a()The traceback contains the call chain:
module frame
a frame
b frameTracebacks are objects. Holding a traceback can keep frames alive. Holding frames can keep local variables alive. This is important for memory behavior.
5.15 Modules
A module is an object with a namespace.
When CPython imports a module, it creates or retrieves a module object and executes code in that module’s dictionary.
Conceptually:
find module spec
create module object
insert into sys.modules
execute module code in module namespace
return module objectThe module dictionary becomes the global namespace for functions defined in that module.
For:
# example.py
x = 10
def f():
return xthe function f stores a reference to the module globals dictionary. When f looks up x, it searches that dictionary.
5.16 sys.modules
sys.modules is the import cache.
It maps module names to module objects.
import sys
print(sys.modules["sys"])This cache prevents repeated imports from re-executing the same module.
Importing the same module twice usually returns the already loaded module:
import math
import mathThe second import checks sys.modules and reuses the module.
This cache also handles circular imports. A module may appear in sys.modules before its code has finished executing.
5.17 Builtins
Builtins are names available when local and global lookup fail.
Examples:
len
print
range
object
type
ExceptionA frame has access to a builtins dictionary.
Name lookup for an unqualified name inside a function roughly follows:
locals
globals
builtinsSo:
def f(xs):
return len(xs)usually resolves len from builtins unless a global named len shadows it.
This lookup path is part of the runtime model. It explains why assigning a global named len changes behavior inside the module.
5.18 Descriptors and Attribute Access
Attribute access is runtime dispatch.
For:
obj.nameCPython does not simply look inside obj.__dict__.
It follows descriptor and type lookup rules:
look on type for data descriptor
look in instance dictionary
look on type for non-data descriptor or class attribute
call __getattr__ if needed
raise AttributeError if missingThis is why methods bind automatically:
class C:
def f(self):
return 1
c = C()
m = c.fThe function object stored on the class is a descriptor. Accessing it through an instance creates a bound method or an optimized equivalent call path.
Attribute lookup connects object layout, type objects, descriptors, method calls, and performance.
5.19 Iteration
Iteration uses a small runtime protocol.
For:
for x in obj:
body(x)CPython does roughly:
iterator = iter(obj)
loop:
x = next(iterator)
if StopIteration: exit loop
execute bodyAt C level, this maps to type slots and protocol helpers.
The iterator object stores iteration state. For a list iterator, that state includes the list and current index. For a generator, the iterator is the suspended execution frame itself.
This common protocol powers:
for loops
comprehensions
tuple unpacking
list()
sum()
any()
all()
many standard library functions5.20 Generators and Coroutines
A generator is a suspended frame.
For:
def gen():
yield 1
yield 2calling gen() does not immediately execute the body. It creates a generator object.
The generator owns execution state:
code object
suspended frame or frame-like state
instruction position
local variables
exception state
running flagCalling next() resumes execution until the next yield or return.
Coroutines and async generators extend this idea with awaitable protocol behavior and event-loop integration.
5.21 Memory Management
The runtime manages memory at several layers.
raw memory allocator
object allocator
type-specific free lists or caches
reference counting
cyclic garbage collectorReference counting handles most lifetime events:
new reference increases lifetime
decref releases ownership
zero refcount triggers deallocationThe cyclic garbage collector handles unreachable object cycles.
Memory management is part of runtime behavior because object destruction can execute code through finalizers, weakref callbacks, or deallocation paths that release more objects.
5.22 The GIL in the Runtime Model
In the traditional CPython runtime, the Global Interpreter Lock protects execution of Python bytecode and many internal data structures.
The GIL simplifies:
reference count updates
object mutation invariants
interpreter state access
C extension assumptionsA thread must hold the GIL to execute Python bytecode.
C extensions can release the GIL around blocking or long-running native work, then reacquire it before touching Python objects again.
The runtime model therefore has two layers of concurrency:
OS threads may run concurrently
Python bytecode execution is serialized by the GIL in traditional buildsModern CPython also has free-threaded build work, which changes many internal assumptions. But the traditional GIL model remains essential for understanding existing code and extensions.
5.23 Initialization
Before Python code runs, CPython initializes the runtime.
Startup includes:
configure memory allocators
initialize runtime state
create main interpreter
create main thread state
initialize builtins
initialize sys
set up import machinery
initialize encodings
configure paths
process command-line options
run startup hooks
execute requested codeThis is why startup code is complex. Many modules depend on other modules already existing, but the import system itself also needs runtime support.
CPython bootstraps itself carefully.
5.24 Shutdown
Shutdown is also complex.
Finalization may involve:
running atexit handlers
flushing standard streams
clearing modules
destroying interpreter state
collecting garbage
finalizing objects
releasing memory
tearing down runtime servicesObjects may run finalizers during shutdown, but their module globals may already be cleared. This is why shutdown bugs can be subtle.
A finalizer that expects sys, os, or another module to be fully available may fail during interpreter teardown.
5.25 Embedded Python
CPython can be embedded inside another C or C++ program.
In that case, the host process controls runtime initialization and finalization.
Conceptually:
Py_Initialize();
/* run Python code */
Py_Finalize();Embedding makes the runtime hierarchy more visible. The CPython runtime lives inside a process that may have its own threads, allocators, event loops, logging systems, and shutdown rules.
Embedding code must respect:
initialization order
thread state management
GIL rules
reference ownership
exception handling
finalization constraints5.26 Runtime State and Observability
Python exposes parts of the runtime through standard modules.
| Module | Runtime view |
|---|---|
sys | Interpreter settings, modules, path, frames, refcounts |
gc | Garbage collector state |
inspect | Frames, functions, source, signatures |
dis | Bytecode |
types | Runtime type objects |
threading | Python thread abstractions |
tracemalloc | Allocation traces |
importlib | Import machinery |
Examples:
import sys
import gc
import inspect
print(sys.modules.keys())
print(gc.get_count())
print(inspect.currentframe())These modules are useful because they let you observe runtime structures without immediately reading C code.
5.27 A Complete Execution Sketch
For this program:
def add(a, b):
return a + b
print(add(2, 3))CPython roughly does:
initialize runtime
create main interpreter
create main thread state
load builtins and sys
compile module source to code object
create module frame
execute module bytecode
define function:
create code object for add
create function object
bind name add in module globals
call print(add(2, 3)):
load print from builtins
load add from globals
load constants 2 and 3
call add
create frame for add
bind a = 2, b = 3
load a
load b
perform binary addition through object protocol
return integer result
call print
execute built-in C function
write output
discard return value
finish module frame
run shutdown sequenceThis is the runtime model in action.
5.28 Working Mental Model
Keep this compact model while reading later chapters:
A process owns a CPython runtime.
The runtime owns interpreters.
An interpreter owns modules, builtins, import state, and GC state.
A thread state owns the current execution state for one thread.
A frame runs one code object.
Bytecode instructions operate on a value stack and local storage.
Objects carry type pointers.
Types define behavior through slots.
Errors use exception state plus sentinel returns.
Memory is managed by reference counting plus cyclic GC.This model connects most CPython internals.
5.29 Chapter Summary
The CPython runtime is a layered execution system. The process contains a runtime. The runtime contains interpreter states. Each executing thread has a thread state. Each active call has a frame. Each frame runs a code object. Bytecode manipulates object references. Objects point to types. Types define behavior.
Understanding this hierarchy makes the rest of CPython easier to read. Startup, imports, function calls, exceptions, generators, garbage collection, and shutdown all fit into the same runtime model.