CPython's Exception Implementation in C
BaseException as a GC object, the PyException_HEAD layout, args, __traceback__, __context__, __cause__, and exception groups traced through CPython 3.15's Objects/exceptions.c.
Python exceptions are ordinary objects. In CPython every exception instance is a
PyBaseExceptionObject (or a subclass struct that starts with the same
PyException_HEAD), allocated through the normal type machinery, tracked by
the garbage collector, and carrying args, __traceback__, __context__,
__cause__, and __notes__. The whole hierarchy lives in
Objects/exceptions.c, with the shared layout declared in
Include/cpython/pyerrors.h.
The shared layout: PyException_HEAD
Every exception class uses the same prefix, defined as a macro so that
PySyntaxErrorObject, PyOSErrorObject, PyStopIterationObject, etc. can
append their own fields:
// Include/cpython/pyerrors.h
#define PyException_HEAD PyObject_HEAD PyObject *dict;\
PyObject *args; PyObject *notes; PyObject *traceback;\
PyObject *context; PyObject *cause;\
char suppress_context;
typedef struct {
PyException_HEAD
} PyBaseExceptionObject;So PyBaseExceptionObject is just the prefix: dict, args, notes,
traceback, context, cause, and suppress_context. The dict field is
the instance __dict__; traceback is the head of the traceback chain;
context is the implicitly chained exception (PEP 3134); cause is the
explicitly chained one; suppress_context records whether raise ... from
was used.
BaseException: the root type
BaseException is a real GC type. BaseException_new allocates with
tp_alloc, zeroes the pointers, and either takes ownership of the passed
args tuple or creates an empty one:
// Objects/exceptions.c
static PyObject *
BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyBaseExceptionObject *self;
self = (PyBaseExceptionObject *)type->tp_alloc(type, 0);
if (!self)
return NULL;
self->dict = NULL;
self->notes = NULL;
self->traceback = self->cause = self->context = NULL;
self->suppress_context = 0;
if (args) {
self->args = Py_NewRef(args);
return (PyObject *)self;
}
self->args = PyTuple_New(0);
...
}BaseException_init then replaces args from the call arguments, so
BaseException.__init__ is what actually stores them. There is also a
BaseException_vectorcall fast path that allocates and builds args in one
step via PyTuple_FromArray.
Because exceptions can be chained through __context__ and __traceback__,
deallocating a long chain can overflow the C stack. BaseException_dealloc
runs through the trashcan and calls BaseException_clear, which Py_CLEARs
every owned reference:
// Objects/exceptions.c
static int
BaseException_clear(PyObject *op)
{
PyBaseExceptionObject *self = PyBaseExceptionObject_CAST(op);
Py_CLEAR(self->dict);
Py_CLEAR(self->args);
Py_CLEAR(self->notes);
Py_CLEAR(self->traceback);
Py_CLEAR(self->cause);
Py_CLEAR(self->context);
return 0;
}BaseException_traverse visits the same fields so the cyclic GC can find
reference cycles. The type is registered with Py_TPFLAGS_HAVE_GC and
Py_TPFLAGS_BASE_EXC_SUBCLASS (the latter lets the interpreter treat any
instance of a BaseException subclass specially, e.g. for sys.excepthook).
Exception and the hierarchy
Exception itself adds nothing: it is produced by the
SimpleExtendsException macro, which declares a PyTypeObject whose
tp_basicsize is sizeof(PyBaseExceptionObject) and whose base is
_PyExc_BaseException:
// Objects/exceptions.c
#define SimpleExtendsException(EXCBASE, EXCNAME, EXCDOC) \
static PyTypeObject _PyExc_ ## EXCNAME = { \
...
sizeof(PyBaseExceptionObject), \
...
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, \
... \
0, 0, 0, 0, 0, 0, 0, &_ ## EXCBASE, \
...
BaseException_init, 0, BaseException_new, \
};
SimpleExtendsException(PyExc_BaseException, Exception,
"Common base class for all non-exit exceptions.");Most builtin exceptions (TypeError, ValueError, …) are created the same
way. Exceptions that need extra state (StopIteration.value,
SystemExit.code, OSError.errno/filename, SyntaxError location
fields, …) use ComplexExtendsException with a larger struct such as
PyStopIterationObject or PyOSErrorObject, and their own _init,
_clear, _traverse, and _dealloc that call down to
BaseException_clear/BaseException_traverse.
args
args is a tuple (or None if deleted is attempted, though deletion is
rejected). The getter returns a new reference; the setter runs the value
through PySequence_Tuple, so assigning a list converts it:
// Objects/exceptions.c
static PyObject *
BaseException_args_get_impl(PyBaseExceptionObject *self)
{
if (self->args == NULL) {
Py_RETURN_NONE;
}
return Py_NewRef(self->args);
}
static int
BaseException_args_set_impl(PyBaseExceptionObject *self, PyObject *value)
{
PyObject *seq;
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "args may not be deleted");
return -1;
}
seq = PySequence_Tuple(value);
if (!seq)
return -1;
Py_XSETREF(self->args, seq);
return 0;
}BaseException_str uses this: with zero args it returns "", with one arg it
returns str(args[0]), otherwise str(args). BaseException.__reduce__
packages type(self), self.args, and (if present) self.__dict__ so
exceptions pickle and unpickle faithfully.
__notes__ and add_note
notes is a list (or NULL). add_note lazily creates __notes__ as an
empty list and appends to it, so exceptions created without notes carry no
list at all:
// Objects/exceptions.c
static PyObject *
BaseException_add_note_impl(PyBaseExceptionObject *self, PyObject *note)
{
PyObject *notes;
if (PyObject_GetOptionalAttr((PyObject *)self, &_Py_ID(__notes__), ¬es) < 0)
return NULL;
if (notes == NULL) {
notes = PyList_New(0);
...
if (PyObject_SetAttr((PyObject *)self, &_Py_ID(__notes__), notes) < 0) { ... }
}
else if (!PyList_Check(notes)) { ... }
if (PyList_Append(notes, note) < 0) { ... }
...
}__traceback__
The internal traceback pointer is exposed as __traceback__. The getter
returns None when the pointer is NULL; the setter accepts only a traceback
object or None:
// Objects/exceptions.c
static int
BaseException___traceback___set_impl(PyBaseExceptionObject *self,
PyObject *value)
{
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "__traceback__ may not be deleted");
return -1;
}
if (PyTraceBack_Check(value)) {
Py_XSETREF(self->traceback, Py_NewRef(value));
}
else if (value == Py_None) {
Py_CLEAR(self->traceback);
}
else {
PyErr_SetString(PyExc_TypeError,
"__traceback__ must be a traceback or None");
return -1;
}
return 0;
}with_traceback(tb) is a thin wrapper that calls this setter and returns
self. The C API mirrors it: PyException_GetTraceback/PyException_SetTraceback.
Traceback objects
A traceback is a linked list of frames, not stored inside the exception’s
struct beyond the head pointer. The struct is in Include/cpython/traceback.h:
// Include/cpython/traceback.h
struct _traceback {
PyObject_HEAD
PyTracebackObject *tb_next;
PyFrameObject *tb_frame;
int tb_lasti;
int tb_lineno;
};tb_next chains to the caller’s traceback, tb_frame is the frame, and
tb_lasti/tb_lineno record where execution was. When an exception is raised,
the interpreter attaches a fresh PyTracebackObject to exc->traceback; as
the exception unwinds, frames are prepended by setting tb_next. Because
__traceback__ is a normal attribute, user code can replace or truncate the
chain (e.g. exc.__traceback__ = exc.__traceback__.tb_next).
__context__ and __cause__
__context__ is the exception that was being handled when this one was
raised (implicit chaining). Its setter validates that the value is None or
an exception instance, and stores NULL for None:
// Objects/exceptions.c
static int
BaseException___context___set_impl(PyBaseExceptionObject *self,
PyObject *value)
{
if (value == NULL) { ... }
else if (value == Py_None) {
value = NULL;
} else if (!PyExceptionInstance_Check(value)) { ... }
else {
Py_INCREF(value);
}
Py_XSETREF(self->context, value);
return 0;
}__cause__ is set explicitly by raise X from Y. Its setter delegates to
PyException_SetCause, which sets suppress_context = 1 (so __context__ is
not printed) and steals the reference:
// Objects/exceptions.c
void
PyException_SetCause(PyObject *self, PyObject *cause)
{
Py_BEGIN_CRITICAL_SECTION(self);
PyBaseExceptionObject *base_self = PyBaseExceptionObject_CAST(self);
base_self->suppress_context = 1;
Py_XSETREF(base_self->cause, cause);
Py_END_CRITICAL_SECTION();
}The suppress_context flag is also exposed as the read/write member
__suppress_context__.
Exception groups
PEP 654 added BaseExceptionGroup and ExceptionGroup. The group struct
extends PyException_HEAD with msg, excs, and excs_str:
// Include/cpython/pyerrors.h
typedef struct {
PyException_HEAD
PyObject *msg;
PyObject *excs;
PyObject *excs_str;
} PyBaseExceptionGroupObject;BaseExceptionGroup_new does the real work: it parses message, exceptions,
converts exceptions to a tuple, rejects empty sequences and non-exception
items, and then chooses the concrete class. If a BaseExceptionGroup is
constructed with only Exception subclasses, it is upgraded to
ExceptionGroup; conversely ExceptionGroup cannot wrap bare BaseExceptions:
// Objects/exceptions.c (excerpt)
PyTypeObject *cls = type;
if (cls == PyExc_ExceptionGroup) {
if (nested_base_exceptions) { ... reject ... }
}
else if (cls == (PyTypeObject*)PyExc_BaseExceptionGroup) {
if (!nested_base_exceptions) {
cls = PyExc_ExceptionGroup;
}
}excs_str caches the original repr of the input sequence so that repr
stays stable even if the caller mutates a list later. The group stores
msg/excs and reuses BaseException for args/traceback/context/cause.
Group operations are implemented in C: split(matcher) recursively walks the
nested tree, subgroup returns the matching part, and derive(excs) builds a
new group copying msg. exceptiongroup_subset propagates traceback,
context, cause, and a copy of __notes__ from the original group to each
derived part. The interpreter uses _PyExc_PrepReraiseStar (exposed as
PyUnstable_Exc_PrepReraiseStar) to reconstruct reraised groups in
except* blocks, preserving metadata so reraised subgroups compare equal to
the original.
Key takeaways
- Every exception is a GC object whose layout starts with
PyException_HEAD(dict,args,notes,traceback,context,cause,suppress_context); subclasses append their own fields. BaseExceptionlives inObjects/exceptions.cwithBaseException_new,BaseException_clear/_traverse, andtp_flagsincludingPy_TPFLAGS_BASE_EXC_SUBCLASS.Exceptionand most builtins are generated bySimpleExtendsException, sharingPyBaseExceptionObject’s size andBaseException_*callbacks.argsis always (after init) a tuple;__traceback__holds the head of aPyTracebackObjectlinked list;__context__/__cause__store chained exceptions, withPyException_SetCausesettingsuppress_context.- Exception groups add
msg/excs/excs_strand implement split/subgroup/ derive plus interpreter-side reraise reconstruction in C.
Further reading
Objects/exceptions.c–BaseException_*, macros, group implementation.Include/cpython/pyerrors.h–PyException_HEADand all exception structs.Python/traceback.candInclude/cpython/traceback.h– traceback objects.Lib/test/exception_hierarchy.txt– the documented class hierarchy.