← board

self.__class__(...) cannot be called as a constructor

Repro

class A:
    def __init__(self, v):
        self.v = v
    def clone(self):
        return self.__class__(self.v)   # error: A has no method __class__

print(A(7).clone().v)

CPython prints 7.

The boundary

shape result
self.__class__.__name__ OK — reads A
type(a).__name__ OK — reads A
self.__class__(self.v) error: A has no method class

So __class__ resolves as something you can read an attribute off, but not as a class object you can call. The diagnostic is also misleading: it reports a missing method __class__ on a class whose __class__ attribute demonstrably works one line earlier.

Why it matters beyond the diagnostic

self.__class__(...) is the idiom for "make another one of me", and its point is that a SUBCLASS gets an instance of itself. Naming the class explicitly — the only workaround — silently changes that: Base.copy() called on a Derived returns a Base. So this is not a spelling preference; the two forms mean different things, and only the unsupported one is correct for a class anyone might subclass.

Track B site

lib/rtl/mimic_xml_sax_xmlreader.py (AttributesImpl.copy, AttributesNSImpl.copy) names the class explicitly and says so at the site. Registered in devdocs/dev/track-b-workarounds.md; revert to the __class__ form when this lands.

Resolution — 2026-08-27

Fixedpoint 85391dfb6ddf, tools/gate.sh quick GREEN. Test: test/test_nilpy_self_class_constructs.npy + .expected, 9 rows, all matching CPython, registered in the Makefile.

No new representation was needed — that is the whole shape of the fix. The refusal this ticket reports was written when it was TRUE ("this frontend has no class-object value") and stopped being true when [[feature-nilpy-class-as-a-value]] landed VT_CLASSREF. So a bare x.__class__ now lowers to exactly the classref variant a class held in a variable already is, and construction rides pyvar_callvN -> PyClassRefNew, which already reflects create by name over the RTTI. No second construction path. cls = self.__class__, print(self.__class__), self.__class__ is B and isinstance(x, self.__class__) all came along for free, because none of them was ever about __class__.

The RUNTIME class, deliberately. In a base method self.__class__ must answer the SUBCLASS — that is the entire reason the idiom exists. A static ci would have been easy and would have silently constructed the base for every descendant: a wrong VALUE, which is strictly worse than the compile error it replaced. So the blob comes from __pxxRttiOf(x) ([[x+0] - 8]), the same pointer x.ClassType is. Every B row in the test is there to hold that down.

Four routes, because that is how many there are. PyIsBareClassRefAhead + PyMakeClassRefOf (pyparser.inc) are tested right after the existing __class__.__name__ chain check at all three of its sites — pasparser_lval.inc and two in pyparser.inc — which keeps the __name__ chain on its own direct lowering instead of boxing a classref only to read a name back off it. The fourth route is a receiver whose tag is only known at RUN time (an unannotated parameter, a for-loop element, a dict value); that one is answered in pydynattr_get_v, and the same arm went into pydynattr_get so the two getters agree — the pairing that file's own comments keep calling out.

One sibling fixed on the way: isinstance(x, a.__class__) was unexpected token. isinstance already routes a non-identifier second argument to the runtime pyisinstance_v, and already routed a module-qualified mod.Class there — but an identifier CARRYING A SELECTOR missed the arm by one token and fell into the name arms, none of which consume a .. The condition now also admits ident ..

Two pre-existing limitations found by the sibling sweep, both measured identical on pinned v384, both filed rather than folded in:

Track B workaround marked REVERTABLE in devdocs/dev/track-b-workarounds.mdlib/rtl/mimic_xml_sax_xmlreader.py's AttributesImpl.copy / AttributesNSImpl.copy name their class explicitly. Not reverted here: that is Track B's file and its lifecycle, and it wants the next pin under it first.

Log