Repository navigation
typing: one None-valued member makes isinstance() on a runtime_checkable Protocol O(N) per call #156413
Description
Activity
- addedtype-featureA feature request or enhancementA feature request or enhancementperformancePerformance or resource usagePerformance or resource usage
on Aug 26, 2026 - added a commit that references this issue
on Aug 26, 2026 - There could be caching?
- Does it happen on every versions?
- Can you bisect the bad commit, if any?
- Yes.
_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.
>>> [r() for r in _abc._get_dump(P)[1]], [r() for r in _abc._get_dump(P)[2]] ([<class 'Good'>], [<class 'Bad'>]) # positive, negative
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.
- Yes.
- addedstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
on Aug 27, 2026 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
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: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.
A class satisfying a
@runtime_checkableProtocol with any class attribute set toNonestill passesisinstance(), 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.On 3.13.13:
True 0.0121thenTrue 0.1207. Only the timing differs.Linked PRs
None-valued non-callable member keep the Protocol fast path #156451