← board

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:

  1. 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 ended begin Next; Continue; end. Continue in a repeat jumps to the until test — measured, both FPC and pxx — so the arm's own Next ate the comma that Eat(tkComma) then went looking for, found none, and closed the tuple. isinstance(b, (type(o), Box)) answered on type(o) alone and the parse then hit the tuple's ) where the call's belonged: Expected: ), but got: Box. Dropping the Next from 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-off isinstance puts it in.
  2. isinstance(x, (C,)) was a compile error. With the arms handing their comma to the until test, 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

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