isinstance does not accept a qualified class name
Filed 2026-08-19 from [[feature-b-mimic-collections-abc-mapping-and-mutablemapping]].
Measured on pinned v356 (2bb09afb0cff):
import collections.abc as cabc
print(isinstance({}, cabc.Mapping))
| CPython | True |
| pxx (pinned v356) | error: Nil Python: unknown type in isinstance: cabc |
The diagnostic names cabc as the unknown type, so the second argument is being
parsed as a bare name and the .Mapping selector is not folded in. from mod import Cls then isinstance(x, Cls) works — only the qualified form fails.
Ordinary working CPython code uses this spelling constantly, so it is an upward- compatibility defect, not a dialect choice.
Workaround in test/lib_mimic_collections_abc.npy: bind Mapping = cabc.Mapping
at module top and test against the bare name. Registered in
devdocs/dev/track-b-workarounds.md.
Resolution (2026-08-27)
Fixed in compiler/pyparser.inc, PyParseIsinstance. Witness:
test/test_nilpy_isinstance_against_a_qualified_class_name.npy (+
test/nilpy_isishapes.py), registered in test-core as test_nilpy_qualisi26,
.expected generated by CPython. Every row red at pinned v382
(b12e5e27a), green at HEAD.
The reported defect — one predicate
Each arm of the type loop read a bare CurTok.SVal, so mod.Box was taken
as the type mod and the .Box selector was left in the stream. An attribute
of a symbol (holder.klass) already worked, because a symbol reaches the
bound-name arm which parses the whole expression; a module is not a symbol,
and that is the entire gap. A nested QualifiedTypeAhead predicate now routes
ident . ident where the head is a unit-or-alias and neither a symbol nor a
class to the not-an-identifier arm — the same runtime pyisinstance_v route
type(y) and a bound class name have taken since
[[bug-n-a-type-name-is-not-a-first-class-value]]. Only that ticket made the
route exist: before a class was a value there was nothing an expression could
have evaluated to.
Two older defects the fix exposed — both in the same loop, both pre-existing
The ticket is one line of behaviour; the loop it lives in had two more holes, found by varying the shape rather than by reading. Both reproduce at v382 with nothing qualified in sight, which is what makes them independent:
- A tuple was silently TRUNCATED at its first runtime-route element. The
loop is
repeat … until not Eat(tkComma), and the three arms that parse an expression endedbegin Next; Continue; end.Continuein arepeatjumps to theuntiltest — measured, both FPC and pxx — so the arm's ownNextate the comma thatEat(tkComma)then went looking for, found none, and closed the tuple.isinstance(b, (type(o), Box))answered ontype(o)alone and the parse then hit the tuple's)where the call's belonged:Expected: ), but got: Box. Dropping theNextfrom all three arms is the whole fix. Only when such an element was last did the shape work, which is why it survived — that is the position a one-offisinstanceputs it in. isinstance(x, (C,))was a compile error. With the arms handing their comma to theuntiltest, control returns to the loop head with the tuple's)current, and the not-an-identifier arm read that as a missing type:expected a type in isinstance. A trailing comma is legal in any Python tuple and for a one-element tuple it is the only spelling there is, so this refused a form with no alternative. Guarded at the loop head, where the concept lives, rather than in each arm.
And one deliberate extra, same loop, same category: isinstance(x, ()).
CPython runs it and answers False — it is the base case of a computed
isinstance(x, tuple(accepted)) — and we refused it. Under Track N's
upward-compatibility rule that is a defect, not a dialect choice, so the empty
tuple now folds to a False literal. The expected a type diagnostic is kept for
a genuinely missing type (isinstance(x, )), which CPython rejects too.
Two hypotheses measured WRONG, recorded because they were plausible
- "
PyParseBoolExpreats the comma." It does not. AWriteLnimmediately after it printedtok=80 hadTuple=1— 80 istkComma— so the separator was sitting right there and the arm's guard should have fired. It had fired; the fault was one line later, in whatContinuemeans. Reading the loop was cheaper than the trace and would have been wrong. - "
(cls.Other,)compiles." Recorded earlier in the session from a mis-measurement. It does not, and never did — that is defect 2 above. It was caught only because the witness included the shape.
Not done
test/lib_mimic_collections_abc.npy still binds Mapping = cabc.Mapping at
module top and tests the bare name, with the workaround registered in
devdocs/dev/track-b-workarounds.md. It builds with $(PXX_STABLE), so Track B
can drop it once this lands in a pin — a Track B edit, not one to make from
here. Same for test/lib_mimic_bisect.npy's now-stale comment.
Log
- 2026-08-27 — resolved, commit 6c9f8cd70.