A list and a set share one class, so introspection cannot tell them apart
Filed 2026-08-26 while resolving
[[bug-n-hasattr-through-an-untyped-parameter-is-always-false]]. Pre-existing for
add/discard/union; that fix adds update and the other set-only aliases to
the same list, which is stated as an explicit trade in its resolution rather than
worked around.
Measured (self-hosted, at the fix's sha)
def h(x, n):
return hasattr(x, 'update') # and 'add', per row
print(type([1]).__name__, type({1, 2}).__name__) # list set — both, correct
| CPython | pxx | |
|---|---|---|
hasattr([1], 'add') |
False | True (pre-existing) |
hasattr([1], 'update') |
False | True (new with the hasattr fix) |
hasattr({1, 2}, 'add') |
True | True |
hasattr({1, 2}, 'update') |
True | True (was False) |
type([1]).__name__ |
list |
list |
type({1, 2}).__name__ |
set |
set |
Cause
list and set are both TPyList. Every introspection path that decides
by class identity — the AN_IS_TEST chain PyHasAttrRuntimeChain builds, and
pylib's PyFindMethByName(GetInstanceRTTI(obj), …) — therefore cannot separate
them, and answers the union of both Python types' surfaces for either.
The interesting part is that the information is not missing:
type(x).__name__ / pytype_name_v answers list vs set correctly, so
whatever distinguishes them at run time is reachable. The predicate is deciding
by the wrong key, not from missing data.
Not just hasattr
isinstance is worth measuring on the same pair before choosing a fix — the
NilPy charter's own worked example is that isinstance(t, list) answering True
for a non-list IS a bug, "because ordinary working code branches on it"
(devdocs/dev/nilpy-semantics-divergences.md). If isinstance(a_list, set) is
also True, that is the same defect and the fix should serve both, not grow a
second mechanism.
Shape of the fix
Do not add a per-name exclusion list to hasattr — that is the second path that
stays broken. Either give the two Python types two classes (TPySet deriving
from or beside TPyList), or make the introspection predicates decide on the
same run-time discriminator pytype_name_v already uses instead of on class
identity. The first makes is correct everywhere for free and is the reason to
prefer it; measure what it costs in pylib first.
Gate
The table above as a .npy with a CPython-generated .expected, plus the
isinstance rows, plus the calls ({1,2}.update({9}) and [1].append(9)) still
working next to them.