You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
typing: one None-valued member makes isinstance() on a runtime_checkable Protocol O(N) per call #156413
A class satisfying a @runtime_checkable Protocol with any class attribute set to None still passes isinstance(), but is orders of magnitude slower, scaling linearly with member count. Best of 5 runs of 20,000 calls, at 5/20/50/110 members: 25x/84x/316x/735x slower.
Yes. _ProtocolMeta.__instancecheck__ starts with if _abc_instancecheck(cls, instance): return True. _proto_hook accepts Good, so it goes into ABCMeta's positive cache and every later call is a cache hit. It rejects Bad, which goes into the negative cache, so Bad re-runs the O(N) getattr_static loop every time.
The two paths don't agree on what = None means. _proto_hook treats any None in a class __dict__ as "not implemented". __instancecheck__ only treats it that way for callable members (attr not in cls.__non_callable_proto_members__).
No, 3.12 and up. On 3.9 to 3.11 both classes take the slow path and the ratio is 1.0x. At 110 members, best of 5 runs of 20k, Good goes from 26615 ns/call on 3.11.14 to 101 on 3.14.4, while Bad stays at 26645 and 52966. Good is what changed. So this is Bad missing the 3.12 fast path rather than a slowdown.
There is nothing to bisect. The asymmetry starts at the commit that added the fast path,
Sorry, but for me this looks like the bad commit I wanted :') I wanted to know whether it was already like that when we introduced typing.Protocol but no.
I don't think it's that bad, considering that for 100+ members you get 101ns for good classes and 50ms otherwise. However, what's annoying is that it's easy to have a property that could return None or some other type and one class decides to be None while an other decides to be non-None.
OTOH, I didn't use @runtime_checkable a lot and we document it as follows:
An isinstance() check against a runtime-checkable protocol can be surprisingly slow compared to an isinstance() check against a non-protocol class. Consider using alternative idioms such as hasattr() calls for structural checks in performance-sensitive code.
I don't have a good idea of how to tackle this correctly but if the patch is un-necessarily complex and could cause breakage, then it's also possible to leave things as is.
Thank you for looking at this deeply @picnixz . It is a bit beyond my understanding. I asked the AI to generate a PR, and it doesn't look particularly big, but I prefer to leave that to the experts on whether it is a worthwhile trade-off.
Activity
Merge pull request #4628 from adamtheturtle/adamtheturtle/cite-cpytho…
picnixz commentedon Aug 26, 2026
adamtheturtle commentedon Aug 27, 2026
_ProtocolMeta.__instancecheck__starts withif _abc_instancecheck(cls, instance): return True._proto_hookacceptsGood, so it goes intoABCMeta's positive cache and every later call is a cache hit. It rejectsBad, which goes into the negative cache, soBadre-runs the O(N)getattr_staticloop every time.The two paths don't agree on what
= Nonemeans._proto_hooktreats anyNonein a class__dict__as "not implemented".__instancecheck__only treats it that way for callable members (attr not in cls.__non_callable_proto_members__).No, 3.12 and up. On 3.9 to 3.11 both classes take the slow path and the ratio is 1.0x. At 110 members, best of 5 runs of 20k,
Goodgoes from 26615 ns/call on 3.11.14 to 101 on 3.14.4, whileBadstays at 26645 and 52966.Goodis what changed. So this isBadmissing the 3.12 fast path rather than a slowdown.There is nothing to bisect. The asymmetry starts at the commit that added the fast path, 47753ec (Performance of typing._ProtocolMeta._get_protocol_attrs and isinstance #74690, gh-74690: typing: Simplify and optimise
_ProtocolMeta.__instancecheck__#103159, 3.12.0b1). It dropped the_is_callable_members_only(...)guard and movedsuper().__instancecheck__(instance)to the top, which is what made the ABC cache reachable for protocols with non-callable members.Badnever satisfies_proto_hook, so it was never covered by it.picnixz commentedon Aug 27, 2026
Sorry, but for me this looks like the bad commit I wanted :') I wanted to know whether it was already like that when we introduced typing.Protocol but no.
I don't think it's that bad, considering that for 100+ members you get 101ns for good classes and 50ms otherwise. However, what's annoying is that it's easy to have a property that could return
Noneor some other type and one class decides to be None while an other decides to be non-None.OTOH, I didn't use
@runtime_checkablea lot and we document it as follows:I don't have a good idea of how to tackle this correctly but if the patch is un-necessarily complex and could cause breakage, then it's also possible to leave things as is.
cc @JelleZijlstra @AlexWaygood
adamtheturtle commentedon Aug 27, 2026
Thank you for looking at this deeply @picnixz . It is a bit beyond my understanding. I asked the AI to generate a PR, and it doesn't look particularly big, but I prefer to leave that to the experts on whether it is a worthwhile trade-off.