python/cpython
Interpreter
The CPython interpreter is the loop in Python/ceval.c that dispatches one bytecode instruction at a time. Since Python 3.11 it has been an adaptive interpreter: opcodes rewrite themselves at runtime to specialized variants based on the types and shapes they see. Internally this is sometimes called "tier 1"; tier 2 is the JIT. The authoritative reference is InternalDocs/interpreter.md.
Files
| File | Role |
|---|---|
Python/ceval.c |
The eval loop, frame entry/exit, signal handling, recursion limit. |
Python/ceval_macros.h |
Macros used inside the dispatch (TARGET, DISPATCH, GOTO_ERROR, …). |
Python/ceval.h |
Internal header for the rest of CPython. |
Python/bytecodes.c |
The DSL source for every opcode. Not compiled directly — it's read by the cases generator. |
Python/generated_cases.c.h |
The generated tier-1 dispatch table (#included by ceval.c). |
Python/specialize.c |
Decides when and to which variant to specialize an instruction. |
Python/intrinsics.c |
Implements INTRINSIC_* opcodes (a level of indirection for ad-hoc operations). |
Python/instrumentation.c |
sys.monitoring hooks (PEP 669). |
Python/legacy_tracing.c |
The classic sys.settrace / sys.setprofile shim on top of sys.monitoring. |
Python/opcode_targets.h |
Computed-goto target table (used when supported by the compiler). |
Objects/frameobject.c |
Per-call frame: locals, value stack pointer, exception state. |
Include/internal/pycore_frame.h |
The internal _PyInterpreterFrame layout. |
The dispatch loop
A simplified version of the inner loop:
_Py_CODEUNIT *next_instr = first_instr;
while (1) {
_Py_CODEUNIT word = *next_instr++;
unsigned char opcode = _Py_OPCODE(word);
unsigned int oparg = _Py_OPARG(word);
switch (opcode) {
TARGET(LOAD_FAST): { ... }
TARGET(BINARY_OP): { ... }
...
}
}In production the switch is replaced by computed gotos on platforms that support them: goto *opcode_targets[opcode];. This eliminates the bounds check and lets the CPU branch-predict each opcode independently.
The TARGET(...) macro and the DISPATCH() macro that closes each opcode are defined in Python/ceval_macros.h.
Frames
_PyEval_EvalFrameDefault runs over an _PyInterpreterFrame, not the public PyFrameObject. The internal frame is a struct laid out inside the calling thread's data stack (Python/pystate.c) so call/return is essentially sp += frame->framesize. The PyFrameObject (visible to Python code as sys._getframe()) is now a thin wrapper that materializes lazily and points back at the internal frame.
This frame redesign was one of the headline 3.11 changes; it's documented in detail in InternalDocs/frames.md.
A frame holds:
- Local variables (
localsplus[0:nlocals]). - Cell variables (
localsplus[nlocals:nlocals+ncells]). - The value stack (
localsplus[nlocals+ncells:]). - Instruction pointer (
prev_instr). - A pointer to the
PyCodeObject. - The previous frame, current exception, and tracing state.
Specialization
A "warm" instruction (executed enough times to cross a counter threshold) goes through _Py_Specialize_* in Python/specialize.c. Specialization rewrites the opcode in place — for example:
BINARY_OPmay becomeBINARY_OP_ADD_INT,BINARY_OP_ADD_FLOAT, orBINARY_OP_ADD_UNICODE.LOAD_ATTRmay becomeLOAD_ATTR_INSTANCE_VALUE,LOAD_ATTR_SLOT, orLOAD_ATTR_METHOD.CALLhas many specializations:CALL_PY_EXACT_ARGS,CALL_BUILTIN_O,CALL_LIST_APPEND, …
Each specialized opcode is followed by an inline cache — a few code units that hold a type version, a function pointer, or an attribute offset. The cache size per opcode is in Lib/_opcode_metadata.py (regenerated from Python/bytecodes.c).
If the runtime types diverge, the specialized opcode runs DEOPT_IF(...), which:
- Resets the opcode back to its generic form.
- Resets the warmup counter.
- Falls through to the generic implementation for this iteration.
This makes the system self-tuning at minimal runtime overhead. Statistics are gathered when the build is configured with --enable-pystats (see Python/pystats.c).
Bytecode format
Each instruction is a 16-bit _Py_CODEUNIT:
+--------+--------+
| opcode | oparg |
+--------+--------+
8 bit 8 bitWider arguments are encoded by chaining EXTENDED_ARG:
EXTENDED_ARG 1
EXTENDED_ARG 0
LOAD_CONST 2
⇒ LOAD_CONST oparg = 0x010002A specialized opcode is physically the same size as the unspecialized one — the inline cache lives in subsequent code units that the dispatch table treats as "skip past these".
Stackrefs
Values on the interpreter's value stack are not raw PyObject* but _PyStackRef — a tagged pointer that may carry a deferred reference count. This avoids expensive INCREF/DECREF traffic between very tightly coupled instructions; the implementation lives in Python/stackrefs.c and the design is documented in InternalDocs/stackrefs.md.
Calls
Calling a Python function from C in the interpreter goes through _PyEvalFramePushAndInit and friends (in Python/ceval.c). For C functions, the call goes through one of:
METH_O/METH_NOARGS— fixed arity.METH_VARARGS/METH_KEYWORDS— tuple+dict.METH_FASTCALL(with or withoutMETH_KEYWORDS) — array+Py_ssize_t, the modern fast path.
Specialized call opcodes (CALL_PY_EXACT_ARGS, CALL_BUILTIN_FAST, …) bypass several layers when the shape is known.
Instrumentation
PEP 669 / sys.monitoring lets debugger and profiler tools register interest in a small set of events (line, branch, call, return, raise, …). The interpreter dispatches a parallel "instrumented" copy of each instruction when monitoring is active; that machinery lives in Python/instrumentation.c. The legacy sys.settrace/sys.setprofile API is now a thin wrapper on top, in Python/legacy_tracing.c.
Recursion and signals
CPython enforces a recursion limit (sys.setrecursionlimit) by tracking call depth in _PyThreadState. The check happens at RESUME and at function call. Hard stack overflow detection is platform-specific (see InternalDocs/stack_protection.md).
Signals are delivered to a single thread (the one that holds the GIL on classical builds). Python/ceval.c checks the pending-signal flag at every RESUME/JUMP_BACKWARD, then runs the handler in pure Python (handlers run between bytecodes, never inside one).
Entry points for modification
- New opcode →
Python/bytecodes.c+make regen-cases. - New specialization →
Python/specialize.c. - Frame layout →
Include/internal/pycore_frame.handObjects/frameobject.c. - New
sys.monitoringevent →Python/instrumentation.c. - Per-thread state field →
Include/cpython/pystate.handPython/pystate.c.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.