Skip to main content

Objects/unionobject.c

Source:

cpython 3.14 @ ab2d84fe1023/Objects/unionobject.c

Map

LinesSymbolWhat it does
1-35struct unionobjectStruct layout: args tuple, weakreflist
36-80union_newAllocates a new union from a flattened args tuple
81-120union_args_getargs getter: returns the args tuple
121-170union_instancecheckinstancecheck: iterates args, delegates isinstance to each
171-210union_subclasschecksubclasscheck: iterates args, delegates issubclass to each
211-250union_oror: creates a new union by appending another type or union
251-290Py_nb_or slotnb_or: called for the `
291-300PyUnion_TypeType object definition

Reading

unionobject layout and construction

PyUnion_Type (exposed as types.UnionType in Python) represents the result of int | str. The struct holds a single args tuple with all member types flattened. Nesting is eliminated at construction time: int | (str | bytes) becomes (int, str, bytes).

// CPython: Objects/unionobject.c:12 unionobject
typedef struct {
PyObject_HEAD
PyObject *args;
PyObject *weakreflist;
} unionobject;

union_new receives a pre-built tuple (assembled by the | operator logic) and stores it. The flattening happens in the caller, union_or, before union_new is called. This keeps the struct itself minimal.

// CPython: Objects/unionobject.c:41 union_new
static PyObject *
union_new(PyObject *args)
{
unionobject *result = PyObject_GC_New(unionobject, &PyUnion_Type);
if (result == NULL)
return NULL;
result->args = Py_NewRef(args);
result->weakreflist = NULL;
PyObject_GC_Track(result);
return (PyObject *)result;
}

instancecheck and isinstance iteration

union_instancecheck makes isinstance(x, int | str) work by iterating the args tuple and calling PyObject_IsInstance for each member. It returns True as soon as any member matches, and returns False if none do.

// CPython: Objects/unionobject.c:124 union_instancecheck
static PyObject *
union_instancecheck(PyObject *self, PyObject *instance)
{
unionobject *alias = (unionobject *)self;
Py_ssize_t nargs = PyTuple_GET_SIZE(alias->args);
for (Py_ssize_t i = 0; i < nargs; i++) {
PyObject *arg = PyTuple_GET_ITEM(alias->args, i);
int rc = PyObject_IsInstance(instance, arg);
if (rc < 0)
return NULL;
if (rc)
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
}

union_subclasscheck follows the same pattern with PyObject_IsSubclass.

or and the nb_or slot

union_or builds a new flattened args tuple from the two operands. If either operand is itself a union, its args are spliced in rather than nested. The result is passed to union_new.

// CPython: Objects/unionobject.c:214 union_or
static PyObject *
union_or(PyObject *self, PyObject *other)
{
/* flatten: collect all args from both sides */
PyObject *tuple = make_union_args(self, other);
if (tuple == NULL)
return NULL;
PyObject *result = union_new(tuple);
Py_DECREF(tuple);
return result;
}

The nb_or slot on PyUnion_Type points to union_or, so | between an existing union and another type goes through this path. For plain types (not yet unions), their tp_as_number->nb_or slot is NULL, so Python falls through to the reflected __ror__, which also resolves to union_or on the union side. The nb_or slot on PyType_Type itself was added in CPython 3.10 and calls Py_GenericAlias or union_or depending on whether the result is already a union.

gopy notes

Status: not yet ported.

Planned package path: objects/union_type.go.

A stub file objects/union_type.go exists on the current branch but only defines the type struct. The __instancecheck__ iteration, the __or__ flattening logic, and the nb_or wiring on PyType_Type are all pending. The isinstance iteration is a prerequisite for any code that uses X | Y in an isinstance call, which is common in Python 3.10+ codebases.