Skip to main content

Python/ceval.c (part 23)

Source:

cpython 3.14 @ ab2d84fe1023/Python/ceval.c

This annotation covers exception handling and context manager opcodes. See python_ceval22_detail for attribute access specializations.

Map

LinesSymbolRole
1-80PUSH_EXC_INFOSave active exception; push new exc onto the exception stack
81-180WITH_EXCEPT_STARTCall __exit__ with current exception info
181-300RERAISERe-raise the current exception (possibly with lasti adjustment)
301-420CLEANUP_THROWHandle throw() into a generator; unwrap StopIteration
421-500Exception table lookupMap bytecode offset to handler target offset

Reading

PUSH_EXC_INFO

// CPython: Python/ceval.c:3820 PUSH_EXC_INFO
inst(PUSH_EXC_INFO, (new_exc -- prev_exc, new_exc)) {
_PyErr_StackItem *exc_info = tstate->exc_info;
if (exc_info->current_exception != NULL) {
prev_exc = exc_info->current_exception;
} else {
prev_exc = Py_None;
Py_INCREF(prev_exc);
}
ASSERT_EXC_TYPE_IS_REDUNDANT(new_exc, stack_pointer[-2]);
Py_INCREF(new_exc);
exc_info->current_exception = new_exc;
}

PUSH_EXC_INFO is generated at each except block entry. It saves the current active exception and installs the new one. The stack now contains (prev_exc, new_exc). At except block exit, POP_EXCEPT restores prev_exc.

WITH_EXCEPT_START

// CPython: Python/ceval.c:3920 WITH_EXCEPT_START
inst(WITH_EXCEPT_START, (exit_func, lasti, exc -- exit_func, lasti, exc, res)) {
/* Call exit_func(type(exc), exc, exc.__traceback__) */
PyObject *tp, *val, *tb;
if (exc == Py_None) { tp = val = tb = Py_None; }
else { tp = (PyObject *)Py_TYPE(exc); val = exc; tb = PyException_GetTraceback(exc); }
res = PyObject_CallFunctionObjArgs(exit_func, tp, val, tb, NULL);
Py_XDECREF(tb);
}

with ctx: at exception exit calls ctx.__exit__(type, value, traceback). If __exit__ returns truthy, the exception is suppressed. WITH_EXCEPT_START is only reached when an exception escapes the with body.

RERAISE

// CPython: Python/ceval.c:4000 RERAISE
inst(RERAISE, (val --)) {
/* oparg: 0 = reraise current exc, 1 = reraise and update lasti */
assert(oparg == 0 || oparg == 1);
if (oparg) {
frame->f_lasti = Py_SAFE_DOWNCAST(INSTR_OFFSET(), Py_ssize_t, int);
}
PyObject *exc = val;
assert(PyExceptionInstance_Check(exc));
_PyErr_SetRaisedException(tstate, Py_NewRef(exc));
goto exception_unwind;
}

raise with no argument inside an except block compiles to RERAISE. The oparg=1 variant (used in finally) updates f_lasti so the traceback shows the correct line.

Exception table lookup

// CPython: Python/ceval.c:4180 exception_unwind
exception_unwind:
/* Scan the exception table for a handler covering the current offset */
for each entry in co->co_exceptiontable:
if entry.start <= current_offset < entry.end:
/* Jump to entry.target; push lasti, the current exc, etc. */
goto handle_exception;
/* No handler: unwind this frame and propagate */
return NULL;

The exception table replaces the old block stack. It maps bytecode ranges to handler offsets in a compact format. compile.py generates entries for try/except, try/finally, with, and for/while else clauses.

gopy notes

PUSH_EXC_INFO is vm.PushExcInfo in vm/eval_gen.go. WITH_EXCEPT_START calls objects.CallFunctionObjArgs(exitFunc, tp, val, tb). RERAISE is vm.Reraise. Exception table scanning is vm.FindHandler in vm/eval_unwind.go.