Skip to main content

1611. gopy errors

What we are porting

Two files from CPython:

  • cpython/Python/errors.c (about 2200 lines). Holds the C-API for setting, fetching, clearing, formatting, normalizing, and printing exceptions, plus the chaining (__cause__ / __context__) machinery. Reads and writes through the current thread state.
  • cpython/Objects/exceptions.c (about 4500 lines). Defines every built-in exception class, the BaseException slots (args, traceback, __cause__, __context__, __suppress_context__, __notes__), and the type hierarchy.

Python/errors.c is a leaf of the runtime: it depends only on exceptions, traceback, and thread state. We can port it before the VM because every entry point is callable from Go directly.

Objects/exceptions.c is much larger. v0.3 ships only the gating hierarchy: enough exception classes to cover what v0.1 and v0.2 internally raise (ValueError, TypeError, KeyError, IndexError, StopIteration, RuntimeError, OverflowError, ZeroDivisionError), plus their bases (BaseException, Exception, LookupError, ArithmeticError). The remaining classes (OSError subtree, SyntaxError and friends, ImportError, Warning subtree) land incrementally as later phases need them.

Go translation

package errors

// Exception is the runtime representation of a raised Python exception.
// Mirrors PyBaseExceptionObject from Objects/exceptions.c.
type Exception struct {
objects.Header
Type *objects.Type
Args *objects.Tuple
Cause *Exception
Context *Exception
Suppress bool
Notes *objects.List
TB *traceback.Traceback
}

// Set raises an exception with the given args tuple. Mirrors PyErr_SetObject.
func Set(state *state.Thread, t *objects.Type, args *objects.Tuple)

// SetString raises an exception with a single-string args tuple.
// Mirrors PyErr_SetString.
func SetString(state *state.Thread, t *objects.Type, msg string)

// Format raises an exception built from a printf-style template.
// Mirrors PyErr_Format. Returns nil so callers can `return errors.Format(...)`.
func Format(state *state.Thread, t *objects.Type, format string, args ...any) *Exception

// Occurred returns the current exception or nil. Mirrors PyErr_Occurred.
func Occurred(state *state.Thread) *Exception

// Clear drops the current exception. Mirrors PyErr_Clear.
func Clear(state *state.Thread)

// Fetch atomically removes and returns the current exception triple.
// Mirrors PyErr_Fetch.
func Fetch(state *state.Thread) (typ *objects.Type, value *Exception, tb *traceback.Traceback)

// Restore atomically installs an exception triple. Mirrors PyErr_Restore.
func Restore(state *state.Thread, typ *objects.Type, value *Exception, tb *traceback.Traceback)

// NormalizeException ensures the value is an instance of the type.
// Mirrors PyErr_NormalizeException.
func NormalizeException(state *state.Thread)

The state slot

CPython stores the current exception in tstate->current_exception. In gopy this becomes a field on state.Thread:

package state

type Thread struct {
// ...
exc atomic.Pointer[errors.Exception]
}

Atomic because under the free-threaded build readers (the VM POP_EXCEPT handler) and writers (signal handlers) can race. GIL-build reads remain single-threaded but the cost of an atomic load is negligible.

The exception class hierarchy

v0.3 ships the following classes, with their MRO matching CPython exactly:

BaseException
Exception
LookupError
KeyError
IndexError
ArithmeticError
OverflowError
ZeroDivisionError
RuntimeError
NotImplementedError
AttributeError
NameError
TypeError
ValueError
StopIteration

Each class has a Type registered with the runtime. KeyError has the special __str__ that wraps args[0] in repr(...) if args has one element; that override is preserved exactly.

The full hierarchy (OSError subtree, SyntaxError, ImportError, Warning) is added in later phases as needed; the v0.3 spec only guarantees the gating subset. See 1612_gopy_exceptions_full.md for the full list once it lands.

Chaining: __cause__ and __context__

raise X sets X.__context__ = previous. raise X from Y sets X.__cause__ = Y and __suppress_context__ = True. CPython does this in do_raise() in Python/ceval.c, but the same logic is reachable from _PyErr_SetObject:

// Raise sets exc as the current exception. If a previous exception
// was current, it becomes exc.Context.
func Raise(state *state.Thread, exc *Exception)

// RaiseFrom is `raise exc from cause`. It sets exc.Cause and
// suppresses context display.
func RaiseFrom(state *state.Thread, exc *Exception, cause *Exception)

Normalize

PyErr_NormalizeException is the legacy 3-argument API. CPython 3.12+ folds normalization into _PyErr_SetObject; we follow the modern single-step path. NormalizeException exists for source-shape parity but its implementation is a single call to the modern set path.

Print

PyErr_Print writes the exception to stderr. v0.3 ships a minimal implementation that uses traceback.Format for the body and prepends the type name; v0.6 hooks it into sys.excepthook.

File mapping

C sourceGo target
Python/errors.c:_PyErr_SetObject and friendserrors/api.go
Python/errors.c:_PyErr_Occurred / _PyErr_Clearerrors/api.go
Python/errors.c:_PyErr_Fetch / _PyErr_Restoreerrors/api.go
Python/errors.c:_PyErr_FormatVerrors/api.go
Python/errors.c:_PyErr_NormalizeExceptionerrors/api.go
Python/errors.c:_PyErr_Displayerrors/print.go
Python/suggestions.cerrors/suggest.go
Objects/exceptions.c:BaseException_*errors/exception.go
Objects/exceptions.c:KeyError_strerrors/keyerror.go
Objects/exceptions.c:<each leaf>errors/builtins.go

v0.3 bundles the small errors.c entry points into a single api.go. The CPython source location is preserved in each function's doc comment so the source-shape mapping stays auditable. Future phases may split as the file grows.