Open-Source Wikis

/

CPython

/

Systems

/

Interpreter

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_OP may become BINARY_OP_ADD_INT, BINARY_OP_ADD_FLOAT, or BINARY_OP_ADD_UNICODE.
  • LOAD_ATTR may become LOAD_ATTR_INSTANCE_VALUE, LOAD_ATTR_SLOT, or LOAD_ATTR_METHOD.
  • CALL has 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:

  1. Resets the opcode back to its generic form.
  2. Resets the warmup counter.
  3. 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 bit

Wider arguments are encoded by chaining EXTENDED_ARG:

EXTENDED_ARG  1
EXTENDED_ARG  0
LOAD_CONST    2
   ⇒ LOAD_CONST oparg = 0x010002

A 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 without METH_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

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Interpreter – CPython wiki | Factory