Adaptive Specialization Families

A specialization family is a generic adaptive bytecode (LOAD_ATTR, BINARY_OP, CALL, …) together with the set of type-specialized variants it can be rewritten into at run time. The specializing adaptive interpreter watches the operands flowing through each instruction and, once warmed up, calls a family-specific dispatcher (_Py_Specialize_LoadAttr, _Py_Specialize_BinaryOp, …) that inspects the live values and installs the matching variant in place — LOAD_ATTRLOAD_ATTR_INSTANCE_VALUE, BINARY_OPBINARY_OP_ADD_INT, CALLCALL_PY_EXACT_ARGS. Each variant runs guards (DEOPT_IF/EXIT_IF checks) that re-verify the assumption it was built on; on a miss it de-optimizes back to the generic form. This note is the catalog: it enumerates the real families as declared in CPython 3.14.5, walks what each member assumes and which guard protects it, and names the SPEC_FAIL_* reasons a dispatcher records when it cannot specialize. Every family list and opcode name below is read verbatim from the family(...) macros and specialize() calls in Python/bytecodes.c and Python/specialize.c at the v3.14.5 tag.

This is the enumeration note, deliberately distinct from its siblings. The Specializing Adaptive Interpreter is the machine — why specialization exists and the generic warm-up → specialize → guard → de-opt life cycle that all families share; this note does not re-derive that cycle, it cross-links it and focuses on which families exist and what each variant means. Inline Caches and Quickening is the cache layout — the _PyAttrCache/_PyCallCache structs whose fields the guards below read. Read this one when you want to know what CALL_BOUND_METHOD_EXACT_ARGS actually requires, or why your obj.x did not specialize.

Mental model: one dispatcher per family, many guarded variants

Think of a family as a small lookup performed once, when the instruction goes hot. The dispatcher answers “given these exact operands, which fast path applies?” and writes that opcode byte into the bytecode. From then on the variant runs its own cheap guards on every execution; the dispatcher does not run again unless a guard miss de-optimizes the site. The key property that makes this safe is that all members of one family reserve the same number of inline-cache code units — declared by the INLINE_CACHE_ENTRIES_<OP> argument to family(...) — so one opcode byte can overwrite another without shifting the bytecode array (see Inline Caches and Quickening).

flowchart TD
    A["Generic adaptive op<br/>(LOAD_ATTR / BINARY_OP / CALL)"] -->|"counter triggers"| B["_Py_Specialize_*<br/>inspect live operands"]
    B -->|"shape recognized"| C["install variant<br/>e.g. LOAD_ATTR_SLOT"]
    B -->|"unrecognized shape"| D["record SPEC_FAIL_*<br/>stay generic + back off"]
    C --> E["run guards every execution<br/>_GUARD_TYPE_VERSION, _GUARD_TOS_INT, ..."]
    E -->|"guards pass"| F["fast body (cached offset / direct C call)"]
    E -->|"DEOPT_IF fires"| G["unspecialize → generic op<br/>+ exponential backoff"]
    G --> A

Diagram: the family decision viewed once-per-specialization. The insight is that the dispatcher’s job is classification, the variant’s job is verification: the expensive type/shape analysis happens a single time in _Py_Specialize_*, and the per-execution cost collapses to a few cheap guards. A family is “rich” (LOAD_ATTR, CALL) precisely when the operation has many distinct fast-paths worth classifying; it is “thin” (CONTAINS_OP) when only a couple of shapes matter.

The LOAD_ATTR family — the richest, because obj.x has many shapes

Attribute access has more distinct fast paths than any other operation, so LOAD_ATTR has the most members. Verbatim from the family(LOAD_ATTR, INLINE_CACHE_ENTRIES_LOAD_ATTR) declaration in bytecodes.c: LOAD_ATTR_INSTANCE_VALUE, LOAD_ATTR_MODULE, LOAD_ATTR_WITH_HINT, LOAD_ATTR_SLOT, LOAD_ATTR_CLASS, LOAD_ATTR_CLASS_WITH_METACLASS_CHECK, LOAD_ATTR_PROPERTY, LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN, LOAD_ATTR_METHOD_WITH_VALUES, LOAD_ATTR_METHOD_NO_DICT, LOAD_ATTR_METHOD_LAZY_DICT, LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES, LOAD_ATTR_NONDESCRIPTOR_NO_DICT.

The dispatcher _Py_Specialize_LoadAttr first branches on the owner’s type. If the owner is a module, it goes down the LOAD_ATTR_MODULE path (the name lives in the module’s globals dict; caches the keys version and index). If the owner is a type object itself (obj is a class), it produces LOAD_ATTR_CLASS — or LOAD_ATTR_CLASS_WITH_METACLASS_CHECK when the metaclass needs an immutability guard. Otherwise it analyzes the instance case through a shared analyze_descriptor_load() routine that classifies what the name resolves to on the type — method, property, slot, plain data, overriding descriptor, and so on — and picks the variant:

  • LOAD_ATTR_INSTANCE_VALUE — the common plain-class case: the attribute lives in the object’s inline values array (the compact per-instance storage managed by the type). Its guard chain, verbatim from bytecodes.c:
    macro(LOAD_ATTR_INSTANCE_VALUE) =
        unused/1 +
        _GUARD_TYPE_VERSION +
        _CHECK_MANAGED_OBJECT_HAS_VALUES +
        _LOAD_ATTR_INSTANCE_VALUE +
        unused/5 +
        _PUSH_NULL_CONDITIONAL;
    _GUARD_TYPE_VERSION compares the owner’s tp->tp_version_tag against the cached version (EXIT_IF(... tp_version_tag != type_version)); _CHECK_MANAGED_OBJECT_HAS_VALUES confirms the object still uses inline values; _LOAD_ATTR_INSTANCE_VALUE reads the cached offset. The unused/1 and unused/5 pad the instruction to the family’s common length.
  • LOAD_ATTR_SLOT — a __slots__ attribute, read at a fixed member-descriptor offset. Guarded by _GUARD_TYPE_VERSION plus the offset read.
  • LOAD_ATTR_WITH_HINT — a __dict__-backed attribute, using a cached dict-keys index “hint” so the lookup is an array index rather than a hash probe; guards the keys version.
  • LOAD_ATTR_PROPERTY — the attribute is a property whose fget is a plain Python function of one argument; specialization caches the function and (per the source) requires a deferred reference count, failing with SPEC_FAIL_ATTR_PROPERTY_NOT_PY_FUNCTION or SPEC_FAIL_ATTR_DESCR_NOT_DEFERRED otherwise.
  • LOAD_ATTR_METHOD_WITH_VALUES / LOAD_ATTR_METHOD_NO_DICT / LOAD_ATTR_METHOD_LAZY_DICT — the method-load trio. Because obj.meth() compiles to a LOAD_ATTR (with the low oparg bit requesting “load method”) followed by CALL, these variants push the unbound method plus self directly and skip allocating a bound-method object — a large, common win. The three differ in whether the instance has inline values, no __dict__, or a lazily-created __dict__.
  • LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES / LOAD_ATTR_NONDESCRIPTOR_NO_DICT — for a class attribute that is not a descriptor (e.g. a plain class-level value shadowed onto instances).
  • LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN — the slow-ish case where the class defines a custom __getattribute__.

When no variant fits, the dispatcher records a reason — verified names from specialize.c include SPEC_FAIL_ATTR_OVERRIDING_DESCRIPTOR, SPEC_FAIL_ATTR_NON_OVERRIDING_DESCRIPTOR, SPEC_FAIL_ATTR_NOT_DESCRIPTOR, SPEC_FAIL_ATTR_METHOD, SPEC_FAIL_ATTR_MUTABLE_CLASS, SPEC_FAIL_ATTR_PROPERTY, SPEC_FAIL_ATTR_NON_OBJECT_SLOT, SPEC_FAIL_ATTR_READ_ONLY, SPEC_FAIL_ATTR_NOT_MANAGED_DICT, SPEC_FAIL_ATTR_SHADOWED, SPEC_FAIL_ATTR_SPLIT_DICT, SPEC_FAIL_ATTR_NON_STRING, and SPEC_FAIL_OUT_OF_VERSIONS (the type-version space is exhausted). The mechanics of how attribute resolution works — descriptors, MRO, the inline-values vs. __dict__ distinction — are the subject of Attribute Lookup Mechanics; the dict/keys versioning the guards compare against is in CPython Dict Internals.

Resolved (2026-06-01)

The SPEC_FAIL_* constants are #defines in Python/specialize.c (not enums in pycore_code.h, as earlier supposed). They are partitioned into a generic low range shared by all dispatchers — SPEC_FAIL_OTHER 0, SPEC_FAIL_NO_DICT 1, SPEC_FAIL_OVERRIDDEN 2, SPEC_FAIL_OUT_OF_VERSIONS 3, SPEC_FAIL_OUT_OF_RANGE 4, SPEC_FAIL_EXPECTED_ERROR 5, SPEC_FAIL_WRONG_NUMBER_ARGUMENTS 6, SPEC_FAIL_CODE_COMPLEX_PARAMETERS 7, SPEC_FAIL_CODE_NOT_OPTIMIZED 8 — followed by per-family blocks starting at 9. For LOAD_ATTR, the block runs SPEC_FAIL_ATTR_OVERRIDING_DESCRIPTOR 9SPEC_FAIL_ATTR_DESCR_NOT_DEFERRED 36 (e.g. _METHOD 12, _PROPERTY 14, _READ_ONLY 16, _SHADOWED 21, _SPLIT_DICT 35). These numbers are internal stat codes, meaningful only in a Py_STATS build (summarized by Tools/scripts/summarize_stats.py) and are not a stable API — the exact integers shift release to release as reasons are added/removed, so treat the names, not the numbers, as the durable reference.

The STORE_ATTR family — the write side

Smaller than LOAD_ATTR because writing has fewer fast shapes. Verbatim: family(STORE_ATTR, ...) = STORE_ATTR_INSTANCE_VALUE, STORE_ATTR_SLOT, STORE_ATTR_WITH_HINT. _Py_Specialize_StoreAttr rejects modules (which store through descriptors) and picks STORE_ATTR_SLOT for a writable member descriptor whose offset fits in uint16, STORE_ATTR_INSTANCE_VALUE for an inline-values managed dict, or STORE_ATTR_WITH_HINT for a materialized __dict__. Failure reasons (verified names) include SPEC_FAIL_ATTR_READ_ONLY (the Py_READONLY flag is set), SPEC_FAIL_ATTR_OVERRIDING_DESCRIPTOR, SPEC_FAIL_ATTR_METHOD, SPEC_FAIL_ATTR_PROPERTY, and SPEC_FAIL_ATTR_MUTABLE_CLASS.

The BINARY_OP family — arithmetic, concatenation, and subscription

Verbatim from family(BINARY_OP, INLINE_CACHE_ENTRIES_BINARY_OP): BINARY_OP_MULTIPLY_INT, BINARY_OP_ADD_INT, BINARY_OP_SUBTRACT_INT, BINARY_OP_MULTIPLY_FLOAT, BINARY_OP_ADD_FLOAT, BINARY_OP_SUBTRACT_FLOAT, BINARY_OP_ADD_UNICODE, BINARY_OP_SUBSCR_LIST_INT, BINARY_OP_SUBSCR_LIST_SLICE, BINARY_OP_SUBSCR_TUPLE_INT, BINARY_OP_SUBSCR_STR_INT, BINARY_OP_SUBSCR_DICT, BINARY_OP_SUBSCR_GETITEM, BINARY_OP_EXTEND.

_Py_Specialize_BinaryOp first requires the operands to be the same exact type for the arithmetic forms — the check is effectively !Py_IS_TYPE(lhs, Py_TYPE(rhs)) fails specialization with SPEC_FAIL_BINARY_OP_ADD_DIFFERENT_TYPES (and the _MULTIPLY_, _SUBTRACT_, _TRUE_DIVIDE_ analogues). Two exact ints and an add → BINARY_OP_ADD_INT; two exact floats → BINARY_OP_ADD_FLOAT; two str and add → BINARY_OP_ADD_UNICODE. The guard chain for the integer add, verbatim:

op(_GUARD_TOS_INT, (value -- value)) {
    PyObject *value_o = PyStackRef_AsPyObjectBorrow(value);
    EXIT_IF(!PyLong_CheckExact(value_o));
}
op(_GUARD_NOS_INT, (left, unused -- left, unused)) {
    PyObject *left_o = PyStackRef_AsPyObjectBorrow(left);
    EXIT_IF(!PyLong_CheckExact(left_o));
}
macro(BINARY_OP_ADD_INT) =
    _GUARD_TOS_INT + _GUARD_NOS_INT + unused/5 + _BINARY_OP_ADD_INT;

_GUARD_TOS_INT checks top-of-stack is an exact int (PyLong_CheckExact, not a subclass), _GUARD_NOS_INT checks next-on-stack; if both pass, _BINARY_OP_ADD_INT calls _PyLong_Add directly with no protocol dispatch. The unused/5 pads to the common family length.

The BINARY_OP_SUBSCR_* members handle a[b]. _Py_Specialize_BinaryOp (and the legacy _Py_Specialize_Subscr) pick BINARY_OP_SUBSCR_LIST_INT / _TUPLE_INT / _STR_INT when the index is a non-negative compact int (the guard DEOPT_IF(!_PyLong_IsNonNegativeCompact(...)) plus a bounds check DEOPT_IF(index >= PyList_GET_SIZE(list))), BINARY_OP_SUBSCR_DICT for a dict, BINARY_OP_SUBSCR_LIST_SLICE for a slice, and BINARY_OP_SUBSCR_GETITEM for a user-defined heap type with a cached __getitem__ Python function (guarded on Py_TPFLAGS_HEAPTYPE and the function’s func_version). BINARY_OP_EXTEND is a catch-all for descriptor-driven extended cases (bitwise ops on compact longs, mixed float/long), set up via binary_op_extended_specialization. The long tail of failure reasons is unusually detailed here — SPEC_FAIL_BINARY_OP_FLOOR_DIVIDE, _POWER, _REMAINDER, _LSHIFT, _RSHIFT, _MATRIX_MULTIPLY, plus per-container subscript reasons like SPEC_FAIL_BINARY_OP_SUBSCR_BYTES, _RANGE, _DEFAULTDICT, _MAPPINGPROXY — which the Faster CPython team uses to decide which new fast-paths are worth adding.

Versioning — BINARY_SUBSCR folded into BINARY_OP

In 3.11–3.12 a[b] was a standalone BINARY_SUBSCR opcode with its own family. In 3.14 there is no such family: the subscript specializations are members of BINARY_OP (the BINARY_OP_SUBSCR_* names above), verified by the single family(BINARY_OP, ...) declaration listing them. Older PEP-659-era write-ups describing BINARY_SUBSCR_LIST_INT are the same idea under a now-merged name. (A _Py_Specialize_Subscr dispatcher still exists in specialize.c for the BINARY_SUBSCR opcode path; the family lives under BINARY_OP.)

The CALL family — every shape of “calling something”

CALL is the second-richest family because “calling” spans Python functions, bound methods, C builtins, type constructors, and method descriptors. Verbatim from family(CALL, INLINE_CACHE_ENTRIES_CALL) in bytecodes.c: CALL_BOUND_METHOD_EXACT_ARGS, CALL_PY_EXACT_ARGS, CALL_TYPE_1, CALL_STR_1, CALL_TUPLE_1, CALL_BUILTIN_CLASS, CALL_BUILTIN_O, CALL_BUILTIN_FAST, CALL_BUILTIN_FAST_WITH_KEYWORDS, CALL_LEN, CALL_ISINSTANCE, CALL_LIST_APPEND, CALL_METHOD_DESCRIPTOR_O, CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS, CALL_METHOD_DESCRIPTOR_NOARGS, CALL_METHOD_DESCRIPTOR_FAST, CALL_ALLOC_AND_ENTER_INIT, CALL_PY_GENERAL, CALL_BOUND_METHOD_GENERAL, CALL_NON_PY_GENERAL.

_Py_Specialize_Call branches on the callable’s type into three internal routines:

  • Python functions (specialize_py_call). The hottest case is CALL_PY_EXACT_ARGS: a plain Python function called with exactly its declared number of positional arguments, no defaults filling in, no *args/**kwargs. It guards the function’s version (func_version) so the cached frame layout stays valid. CALL_PY_GENERAL handles the variable-argument case. CALL_BOUND_METHOD_EXACT_ARGS / CALL_BOUND_METHOD_GENERAL handle bound methods, peeling self out of the bound-method object. Specialization fails with SPEC_FAIL_CALL_PEP_523 when a frame-evaluation hook (PEP 523) is installed, SPEC_FAIL_CALL_VECTORCALL when the function does not use the standard _PyFunction_Vectorcall, SPEC_FAIL_WRONG_NUMBER_ARGUMENTS, or SPEC_FAIL_CALL_BOUND_METHOD.
  • C functions (specialize_c_call). Routed by the builtin’s METH_* flags: CALL_BUILTIN_O (one argument, METH_O), CALL_BUILTIN_FAST / CALL_BUILTIN_FAST_WITH_KEYWORDS (the vectorcall fast path), plus the dedicated opcodes CALL_LEN (len()), CALL_ISINSTANCE (isinstance() with two args), and CALL_LIST_APPEND (list.append when followed by a POP_TOP — the list-building idiom). Method descriptors get CALL_METHOD_DESCRIPTOR_NOARGS / _O / _FAST / _FAST_WITH_KEYWORDS. Failure reasons include SPEC_FAIL_CALL_CFUNC_NOARGS, SPEC_FAIL_CALL_CFUNC_VARARGS, SPEC_FAIL_CALL_CFUNC_VARARGS_KEYWORDS, SPEC_FAIL_CALL_CFUNC_METHOD_FASTCALL_KEYWORDS, SPEC_FAIL_CALL_BAD_CALL_FLAGS.
  • Type constructors (specialize_class_call). Calling a type to construct: CALL_TYPE_1 / CALL_STR_1 / CALL_TUPLE_1 are single-argument fast paths for the immutable builtins type, str, tuple; CALL_BUILTIN_CLASS covers an immutable type with a tp_vectorcall; and CALL_ALLOC_AND_ENTER_INIT is the fast path for instantiating a managed Python class MyClass(...) — it allocates the instance and enters __init__ in one shot, with a cached __init__. It fails with SPEC_FAIL_CALL_CLASS_MUTABLE, SPEC_FAIL_CALL_INIT_NOT_PYTHON, SPEC_FAIL_CALL_INIT_NOT_SIMPLE, or SPEC_FAIL_CALL_INIT_NOT_INLINE_VALUES. The fallbacks CALL_PY_GENERAL / CALL_NON_PY_GENERAL catch everything else.

(There is a parallel CALL_KW family for the keyword-argument call path, dispatched by _Py_Specialize_CallKw; it mirrors CALL for calls carrying explicit keywords.)

LOAD_GLOBAL — two variants

Verbatim: family(LOAD_GLOBAL, ...) = LOAD_GLOBAL_MODULE, LOAD_GLOBAL_BUILTIN. _Py_Specialize_LoadGlobal requires both the module globals and builtins to be exact dicts with unicode-only (DICT_KEYS_UNICODE) keys and the index/versions to fit uint16. If the name lives in the module’s own globals, it installs LOAD_GLOBAL_MODULE, caching the keys version and slot index. If the name is only in builtins, it installs LOAD_GLOBAL_BUILTIN, which caches both the module-globals keys version (to prove the name was not shadowed in globals) and the builtins keys version. Failure reasons (verified) include SPEC_FAIL_LOAD_GLOBAL_NON_DICT, SPEC_FAIL_LOAD_GLOBAL_NON_STRING_OR_SPLIT, SPEC_FAIL_OUT_OF_RANGE, SPEC_FAIL_OUT_OF_VERSIONS. This family is also special because the tier-2 optimizer’s remove_globals pass can turn these uops into baked-in constant loads once it proves the global is stable.

The “shape” families: FOR_ITER, TO_BOOL, COMPARE_OP, CONTAINS_OP, STORE_SUBSCR, UNPACK_SEQUENCE, SEND

These smaller families specialize on the concrete container/value type rather than on descriptor analysis.

  • FOR_ITER — loop iteration. Verbatim family: FOR_ITER_LIST, FOR_ITER_TUPLE, FOR_ITER_RANGE, FOR_ITER_GEN. _Py_Specialize_ForIter checks the iterator type with PyList_CheckExact / PyTuple_CheckExact / PyRange_CheckExact and installs the matching variant, which advances an index without going through the general iterator protocol; a generator/coroutine iterator gets FOR_ITER_GEN. Failure reasons name the rejected kinds — SPEC_FAIL_ITER_GENERATOR, SPEC_FAIL_ITER_COROUTINE, SPEC_FAIL_ITER_ASYNC_GENERATOR, SPEC_FAIL_ITER_DICT_KEYS, etc.

    Resolved (2026-06-01) FOR_ITER specializes to exactly four variants in 3.14.5: FOR_ITER_LIST, FOR_ITER_TUPLE, FOR_ITER_RANGE, FOR_ITER_GEN. The family(FOR_ITER, INLINE_CACHE_ENTRIES_FOR_ITER) macro in bytecodes.c v3.14.5 lists precisely those four and no more. Dict-view / enumerate / zip / reversed iterators are not specialized — where SPEC_FAIL_ITER_* reason names reference them, they are recognized-but-rejected failure reasons, not installed variants. (An earlier auto-summary that listed FOR_ITER_DICT_KEYS/_ENUMERATE/_ZIP/_MAP/_REVERSED_LIST as members was wrong — those identifiers are absent from the v3.14.5 family macro.)

  • TO_BOOL — the truthiness test behind if/while. Verbatim family: TO_BOOL_ALWAYS_TRUE, TO_BOOL_BOOL, TO_BOOL_INT, TO_BOOL_LIST, TO_BOOL_NONE, TO_BOOL_STR. _Py_Specialize_ToBool installs TO_BOOL_BOOL for an actual bool (near-identity), TO_BOOL_INT for an exact int (truthy unless zero), TO_BOOL_LIST/_STR for emptiness checks, TO_BOOL_NONE for the None singleton, and TO_BOOL_ALWAYS_TRUE for a type whose instances are always truthy. The list guard, verbatim:
    op(_GUARD_TOS_LIST, (tos -- tos)) {
        PyObject *o = PyStackRef_AsPyObjectBorrow(tos);
        EXIT_IF(!PyList_CheckExact(o));
    }
    macro(TO_BOOL_LIST) = _GUARD_TOS_LIST + unused/1 + unused/2 + _TO_BOOL_LIST;
    Failure reasons include SPEC_FAIL_TO_BOOL_BYTEARRAY, SPEC_FAIL_TO_BOOL_BYTES, SPEC_FAIL_TO_BOOL_DICT.
  • COMPARE_OP — verbatim family COMPARE_OP_FLOAT, COMPARE_OP_INT, COMPARE_OP_STR. _Py_Specialize_CompareOp requires both operands to be the same exact float/int/str; mixed types fail with SPEC_FAIL_COMPARE_OP_DIFFERENT_TYPES, and big integers are rejected (SPEC_FAIL_COMPARE_OP_BIG_INT) because the fast path only handles single-digit comparisons cheaply.
  • CONTAINS_OP (the in operator) — verbatim family CONTAINS_OP_SET, CONTAINS_OP_DICT (only two members in 3.14.5). _Py_Specialize_ContainsOp installs the set or dict membership fast path; lists/tuples/strings are not specialized (they fall through with reasons like SPEC_FAIL_CONTAINS_OP_LIST, _TUPLE, _STR).
  • STORE_SUBSCR — verbatim family STORE_SUBSCR_DICT, STORE_SUBSCR_LIST_INT. _Py_Specialize_StoreSubscr picks STORE_SUBSCR_LIST_INT for a list with an in-bounds non-negative int index, STORE_SUBSCR_DICT for a dict; a slice fails with SPEC_FAIL_SUBSCR_LIST_SLICE.
  • UNPACK_SEQUENCE — verbatim family UNPACK_SEQUENCE_TWO_TUPLE, UNPACK_SEQUENCE_TUPLE, UNPACK_SEQUENCE_LIST (e.g. a, b = pair specializes to the two-tuple variant).
  • SEND — verbatim family with a single member SEND_GEN (driving a generator/coroutine via .send()/yield from).
  • LOAD_SUPER_ATTR — verbatim family LOAD_SUPER_ATTR_ATTR, LOAD_SUPER_ATTR_METHOD (specializing super().x / super().meth()).

The complete set of adaptive families declared with family(...) in 3.14.5 bytecodes.c is: BINARY_OP, CALL, CALL_KW, COMPARE_OP, CONTAINS_OP, FOR_ITER, JUMP_BACKWARD, LOAD_ATTR, LOAD_CONST, LOAD_GLOBAL, LOAD_SUPER_ATTR, RESUME, SEND, STORE_ATTR, STORE_SUBSCR, TO_BOOL, UNPACK_SEQUENCE — verified by grepping every family( declaration. (JUMP_BACKWARD and RESUME are “families” in the macro sense — they have adaptive variants like JUMP_BACKWARD_JIT — but they specialize on execution state, not operand types; JUMP_BACKWARD is the trace-formation trigger of Trace-Based Optimization in CPython.)

The guard-and-deopt path, shared by every family

Each variant’s guards are DEOPT_IF/EXIT_IF macros that revert to the family’s generic opcode on a miss. The generic installation and reversion helpers — specialize() (writes the opcode, sets the cooldown counter) and unspecialize() (reverts via the _PyOpcode_Deopt table and applies exponential back-off) — are not re-derived here; they belong to The Specializing Adaptive Interpreter, which walks them line by line. The point for this catalog is that each family’s DEOPT_IF conditions you saw above (!PyLong_CheckExact, tp_version_tag != type_version, func_version != cached_version, index-out-of-bounds) are exactly the assumptions the dispatcher bet on; a miss routes through unspecialize() back to the adaptive base.

Observing families in action

import dis
 
class Point:
    __slots__ = ("x", "y")
    def __init__(self, x, y): self.x, self.y = x, y
 
def hot(pts):
    total = 0
    for p in pts:            # FOR_ITER → FOR_ITER_LIST
        total = total + p.x  # LOAD_ATTR → LOAD_ATTR_SLOT ; BINARY_OP → BINARY_OP_ADD_INT
    return total
 
pts = [Point(i, i) for i in range(1000)]
for _ in range(100):
    hot(pts)
 
dis.dis(hot, adaptive=True)  # shows the specialized family members

After warm-up, dis(..., adaptive=True) shows FOR_ITER_LIST, LOAD_ATTR_SLOT (because Point uses __slots__), and BINARY_OP_ADD_INT (dis docs). Swap __slots__ for a plain __dict__ and the LOAD_ATTR specializes to LOAD_ATTR_INSTANCE_VALUE instead; feed hot a list whose elements have varying attribute layouts and the LOAD_ATTR will de-optimize and stay generic. A Py_STATS build dumps per-family hit/deopt/SPEC_FAIL_* histograms, which is how the Faster CPython team decides which new family members to add.

Common misunderstandings

  • “Every container type has a FOR_ITER/CONTAINS_OP fast path.” No — the families are deliberately small. FOR_ITER specializes only list/tuple/range/generator; CONTAINS_OP only set/dict in 3.14.5. Other types stay generic by design (the SPEC_FAIL_* names for them mean “recognized but not worth a dedicated opcode,” not “bug”).
  • LOAD_ATTR_METHOD_* and CALL are independent.” They cooperate: obj.meth() is LOAD_ATTR (load-method bit set) + CALL, and the method variant pushes unbound-method + self so the following CALL can use CALL_BOUND_METHOD_EXACT_ARGS without a bound-method allocation.
  • “Subclasses get the fast path.” Almost never — the guards use *_CheckExact, so an int subclass or a str subclass fails the guard and de-optimizes. Specialization rewards using exact builtin types in hot code.
  • “More family members = faster.” Members are added only when stats show a shape is hot and common; a rare shape gets a SPEC_FAIL_* counter, not an opcode, to avoid bloating every site’s cache footprint.

Production notes

The families are the concrete payoff of PEP 659 and the largest single reason CPython 3.11+ is materially faster than 3.10. They have grown every release — the BINARY_OP/BINARY_SUBSCR merge, expanded CALL coverage, the TO_BOOL family — driven by the Py_STATS SPEC_FAIL_* histograms. The practical guidance for application authors is the same monomorphism lesson as the machine note: keep exact builtin types flowing through hot call sites (int, not an int subclass; a fixed instance layout; the same callee), and the family dispatchers will pick a variant and the guards will keep hitting. Polymorphic sites churn through unspecialize() and pay full generic dispatch forever.

See Also