Types
A type is an object whose role is to describe other objects. In
Python the type system is reflective: every value has a type, every
type is itself a value, and the class statement is an expression
that produces a type at run time. The implementation lives in
objects/, alongside the value types it manages, but it is large
enough and central enough to deserve its own page.
CPython's reference is Objects/typeobject.c, roughly ten thousand
lines of C that handle class construction, the C3 linearisation,
descriptor protocol, slot wrappers, and the dunder dispatch that
backs every operator in the language.
Where the code lives
| File | Role | CPython counterpart |
|---|---|---|
objects/type.go | The Type struct. Slot tables and the meta-type. | Objects/typeobject.c PyType_Type |
objects/usertype.go | User-defined class construction. type(name, bases, ns). | Objects/typeobject.c type_new |
objects/mro.go | C3 linearisation. | Objects/typeobject.c mro_invoke |
objects/type_attr.go | Type.__getattribute__, descriptor handling on the class. | Objects/typeobject.c type_getattro |
objects/type_call.go | Type.__call__. Calls __new__ and __init__. | Objects/typeobject.c type_call |
objects/type_getsets.go | __name__, __qualname__, __module__, __bases__, ... | Objects/typeobject.c getsets |
objects/type_proto.go | Built-in protocol method synthesis. | Objects/typeobject.c add_operators |
objects/type_repr.go | Type.__repr__. | Objects/typeobject.c type_repr |
objects/type_specialize.go | Type version bump on mutation. | Objects/typeobject.c type_modified |
objects/instance.go | User-defined instance attribute lookup. | Objects/object.c _PyObject_GenericGetAttr |
objects/descr.go | Descriptor protocol (__get__, __set__, __delete__). | Objects/descrobject.c |
objects/property.go | The property descriptor. | Objects/descrobject.c property_* |
objects/super.go | The super() proxy. | Objects/typeobject.c super_* |
objects/slots.go | Slot wrapping. Synthesises slot wrappers from method names. | Objects/typeobject.c slot defs |
objects/slot_wrap_descr.go | The slot wrapper descriptor type itself. | Objects/descrobject.c slot wrappers |
objects/method_descr.go | Method descriptors (built-in unbound methods). | Objects/descrobject.c method_descr |
objects/member.go | Member descriptors (direct field access). | Objects/descrobject.c member_descr |
objects/generic_alias.go | PEP 585 generics (list[int]). | Objects/genericaliasobject.c |
objects/union_type.go | PEP 604 unions (int | str). | Objects/unionobject.c |
objects/type_annotations.go | Lazy annotation evaluation (PEP 649). | Objects/typeobject.c annotations |
What a type is
A type is an instance of Type. Type itself is an instance of
Type. The chain bottoms out at the same object: type(type) is type. Built-in types are instances of type; user-defined classes
are typically instances of type too, unless they override
__metaclass__ (or pass metaclass= to the class statement).
The Type struct is the slot table. Every operator, every protocol
method, every lifecycle hook is a field on it. See Objects
for the full layout.
isinstance(42, int) -> 42's type is int
type(int) -> type
type(type) -> type
type.__bases__ -> (object,)
object.__bases__ -> ()
object is the root of the inheritance tree. Every type has
object in its MRO. Type (the meta-type) inherits from object,
because the meta-type is a type and types are objects.
The MRO
The method resolution order is the linearisation of a class's
ancestors. Python uses the C3 algorithm: a deterministic merge that
respects local precedence (a class's __bases__ order) and
monotonicity (a class always appears after its subclasses).
// objects/mro.go ComputeMRO
func ComputeMRO(cls *Type, bases []*Type) ([]*Type, error)
The algorithm:
- Start with
[cls]. - For each base, recursively compute its MRO.
- Merge: at each step, find a head (the first class in some list) that is not in the tail of any other list. Pick the first such head; emit it; remove it from the heads of all lists.
- If no head qualifies, the hierarchy is inconsistent and the
class statement raises
TypeError. - The result is the linearised MRO.
The merge produces a sequence that, for any two classes A and B where A comes before B in some base's MRO, places A before B in the merged result. The deterministic outcome is what makes multiple inheritance behave predictably.
Class construction
type(name, bases, namespace) constructs a class. The three-arg
form is the dynamic equivalent of the class statement; the
class statement compiles to a call to it.
// objects/usertype.go NewClass
func NewClass(meta *Type, name string, bases *Tuple, namespace *Dict) (*Type, error)
The work NewClass does:
- Validate the metaclass. If
metais nottypeor a subtype,__init_subclass__and__set_name__may be called with different semantics. - Compute the MRO from
bases. - Determine the C-level instance size by inspecting
__slots__and any base's instance layout. - Allocate a fresh
Typestruct. - Copy slot pointers from the bases. Each slot is inherited from the first base in MRO order that defines it.
- Walk the namespace; for each entry, install a descriptor or wrap a Python function into a slot wrapper.
- Run
__init_subclass__on the first base that defines it. - Run
__set_name__on each descriptor in the namespace. - Bump every base's version (their subclass set just grew, so inline caches keyed on shape are invalid).
- Return the new type.
The result is a type that behaves identically to a built-in: its slots are filled, its descriptors are wired, its MRO is computed.
Instance attribute lookup
obj.x is a slot call. The default implementation in instance.go
follows the standard sequence:
- Look up
xintype(obj).__mro__, walking each class's__dict__. - If the result is a data descriptor (has both
__get__and__set__), call its__get__withobjandtype(obj)and return the result. - Look up
xinobj.__dict__(the instance dict). If found, return it. - If the MRO lookup found a non-data descriptor (has only
__get__), call its__get__. - If the MRO lookup found a non-descriptor, return it.
- Otherwise, fall through to
__getattr__if the class defines one. - Raise
AttributeError.
The sequence is what makes @property work (it's a data
descriptor, so it shadows the instance dict), makes regular methods
bind correctly (functions are non-data descriptors), and lets
__getattr__ act as a fallback only when the normal mechanism
fails.
The fast path is specialised in Specializer. The
generic version in instance.go is the fallback when no
specialisation is applicable.
Descriptors
A descriptor is anything whose type has DescrGet. Three kinds
matter:
- Data descriptor: has both
DescrGetandDescrSet. Examples:property, slot descriptors, member descriptors. Wins over instance dict. - Non-data descriptor: has only
DescrGet. Examples: functions (which become bound methods on access),classmethod,staticmethod. Loses to instance dict. - Class attribute: not a descriptor at all. Returned as-is.
The protocol is implemented in descr.go and is used by every
attribute lookup. User-defined descriptors are first-class: any
class with __get__ (and optionally __set__) participates.
Slot wrappers
When a class defines __add__, the type constructor needs to wire
that Python function into the slot table so that
a + b (which compiles to a slot call) finds it. The mechanism is
slot wrapping: the constructor generates a small wrapper function
that calls the Python __add__ and stores the wrapper into
Type.Number.Add.
// objects/slots.go installOperators
func installOperators(t *Type)
The wrapper is generated per type at construction time. It pays one indirection (a function-pointer call) compared to a hand-written slot, but the wrapper is monomorphic per type, so the JIT-friendly inlining in Optimizer flattens the cost on hot paths.
The reverse machinery exists too: when C code installs a slot, a
slot wrapper descriptor is generated and put in the class's dict
so that Python code can call obj.__add__(...) directly. The
descriptor lives in slot_wrap_descr.go.
Method descriptors
Built-in methods (list.append, dict.get, ...) are method
descriptors. The type's dict carries a MethodDescr for each
built-in method; attribute access on an instance retrieves the
descriptor and binds it.
// objects/method_descr.go MethodDescr
type MethodDescr struct {
Header
typ *Type
name string
meth func(self Object, args, kwds Object) (Object, error)
}
MethodDescr implements DescrGet to return a bound version of
itself. The bound form is created lazily; built-in method calls
that go through specialisation skip the binding entirely (see
CALL_METHOD_DESCRIPTOR_* in Specializer).
super()
super() returns a proxy that delegates attribute lookup to the
class after the current one in the MRO.
// objects/super.go Super
type Super struct {
Header
typ *Type
self Object
obj Object
}
The proxy implements __getattribute__ to walk MRO starting from
the position after typ, and binds the result to obj. The
two-arg form (super(C, self)) is explicit; the zero-arg form
(super() inside a method body) is rewritten by the compiler to
the explicit form using the special __class__ cell.
Type version
Every type carries a Version field that increments on any
structural change: adding or removing a method, changing __bases__,
mutating __dict__. The specialiser caches the version with each
specialised opcode; on a version mismatch the opcode deopts.
// objects/type_specialize.go BumpVersion
func BumpVersion(t *Type)
The bump is triggered by __dict__ mutation, __bases__
assignment, and the addition of subclasses. Subclass mutation also
ripples up through MRO because a parent's slot inheritance may
have changed. The watchers in Optimizer hook the same
mutations to invalidate tier-2 traces.
Generic types
PEP 585 lets built-in types act as generics: list[int],
dict[str, int]. The subscript on a built-in type returns a
GenericAlias, an immutable record of (origin, args).
// objects/generic_alias.go GenericAlias
type GenericAlias struct {
Header
Origin Object
Args *Tuple
}
GenericAlias is callable; calling it falls through to the origin.
Its __class_getitem__ returns the same shape parameterised by
nested args. The subscript is the canonical type-hint expression
for the standard library.
Union types
PEP 604 adds X | Y as a way to write Union[X, Y]. The
| operator on types returns a UnionType object.
// objects/union_type.go UnionType
type UnionType struct {
Header
Args *Tuple
}
UnionType is canonicalised on construction: nested unions are
flattened, duplicates are removed, and the result is a stable
tuple.
Annotations
PEP 649 lays out lazy annotation evaluation. Annotations are
stored as a code object that is evaluated on demand. The
implementation in type_annotations.go defers the evaluation
until the annotations are read; if they reference names that are
not yet defined (forward references), the lookup raises
NameError, mirroring the eager-evaluation behaviour but on
access rather than on class construction.
The compiler in Compile emits the annotation code object as part of the class body's bytecode.
Status
The type machinery is complete in its architecture. The MRO,
descriptor protocol, slot wrappers, method descriptors, super,
and version-bump are all in place. User-defined classes work
end-to-end. __init_subclass__, __set_name__, and
__class_getitem__ are wired. Generic types and union types are
ported.
The places where work continues:
- Slot wrapper synthesis for some less-common slots (e.g.,
__rmul__against C extensions that override only__mul__). - Annotation deferral matches PEP 649 for
from __future__ import annotationsbut the broader PEP 649 rollout in 3.14 has more surface; the full PEP 649 path is gated by the__future__flag.
Reference
- Port source:
objects/(type-related files). - CPython source:
Objects/typeobject.c,Objects/descrobject.c,Objects/genericaliasobject.c,Objects/unionobject.c. - PEP 252, Making Types Look More Like Classes.
- PEP 253, Subtyping Built-in Types.
- PEP 487, Simpler customisation of class creation.
- PEP 585, Type Hinting Generics In Standard Collections.
- PEP 604, Allow writing union types as X | Y.
- PEP 649, Deferred Evaluation Of Annotations Using Descriptors.
- C3 linearization, Barrett et al., OOPSLA 1996.