Skip to content

5. The Runtime Model

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 operations

This hierarchy is the main map for understanding execution.

5.1 Process, Runtime, Interpreter, Thread, Frame

A running CPython program has several nested execution units.

UnitMeaning
ProcessThe OS process containing the CPython executable or embedded runtime
RuntimeGlobal CPython state shared across the process
InterpreterAn isolated Python interpreter state inside the runtime
Thread statePer-thread execution state for one interpreter
FrameOne active execution context
Code objectCompiled bytecode and metadata
ObjectRuntime 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 stack

The 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 flags

At 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 configuration

Conceptually:

PyInterpreterState
    modules
    builtins
    sysdict
    import machinery
    gc generations
    codec registry
    pending async exception state

Most 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 data

A thread state connects native execution to Python execution.

Conceptually:

current OS thread
    PyThreadState
        current interpreter
        current frame
        exception information
        tracing and profiling hooks

When 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 information

For a function:

def add(a, b):
    c = a + b
    return c

calling 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 function

A 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 y

The 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 closure

Many 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 attributes

For example:

x = 10

def f(y):
    return x + y

The 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 cells

Calling 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
comparison

For 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:

NamespaceBacking storage
LocalsFunction fast locals, class namespace, or mapping
GlobalsModule dictionary
BuiltinsBuiltins dictionary
Object attributesInstance dict, slots, descriptors, type lookup
Module attributesModule 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 object

Name 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 c

the compiler knows the local names:

a
b
c

The frame can store them in indexed slots.

Bytecode can then use fast local operations:

LOAD_FAST a
LOAD_FAST b
STORE_FAST c

This 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 + y

the 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 z

The value stack is temporary execution storage. It is separate from local variables.

A frame therefore contains both:

local variable storage
value stack for intermediate operations

This 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 result

Different callable objects have different paths:

CallableRuntime path
Python functionCreate frame and execute code object
Built-in functionCall C function wrapper
MethodBind receiver and call underlying function
ClassAllocate 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 -1

The 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 caller

Exceptions 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
frame

For example:

def a():
    b()

def b():
    1 / 0

a()

The traceback contains the call chain:

module frame
a frame
b frame

Tracebacks 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 object

The module dictionary becomes the global namespace for functions defined in that module.

For:

# example.py
x = 10

def f():
    return x

the 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 math

The 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
Exception

A frame has access to a builtins dictionary.

Name lookup for an unqualified name inside a function roughly follows:

locals
globals
builtins

So:

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

CPython 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 missing

This is why methods bind automatically:

class C:
    def f(self):
        return 1

c = C()
m = c.f

The 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 body

At 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 functions

5.20 Generators and Coroutines

A generator is a suspended frame.

For:

def gen():
    yield 1
    yield 2

calling 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 flag

Calling 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 collector

Reference counting handles most lifetime events:

new reference increases lifetime
decref releases ownership
zero refcount triggers deallocation

The 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 assumptions

A 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 builds

Modern 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 code

This 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 services

Objects 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 constraints

5.26 Runtime State and Observability

Python exposes parts of the runtime through standard modules.

ModuleRuntime view
sysInterpreter settings, modules, path, frames, refcounts
gcGarbage collector state
inspectFrames, functions, source, signatures
disBytecode
typesRuntime type objects
threadingPython thread abstractions
tracemallocAllocation traces
importlibImport 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 sequence

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