Sitelet https://github.com/python/cpython/pull/156451
Skip to content

gh-156413: Let a None-valued non-callable member keep the Protocol fast path - #156451

Open
adamtheturtle wants to merge 2 commits into
python:mainfrom
adamtheturtle:gh-156413-protocol-none-cache
Open

gh-156413: Let a None-valued non-callable member keep the Protocol fast path#156451
adamtheturtle wants to merge 2 commits into
python:mainfrom
adamtheturtle:gh-156413-protocol-none-cache

Conversation

@adamtheturtle

@adamtheturtle adamtheturtle commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

_ProtocolMeta.__instancecheck__ treats a member set to None as "explicitly not implemented" only for callable members:

if val is None and attr not in cls.__non_callable_proto_members__:
    break

_proto_hook applies the same sentinel to every member, callable or not. So a class that sets a non-callable protocol member to None passes isinstance() but is rejected by the subclass hook. It lands in ABCMeta's negative cache instead of the positive one, and every subsequent isinstance() call falls through to the getattr_static loop over all protocol members.

>>> [r() for r in _abc._get_dump(P)[1]], [r() for r in _abc._get_dump(P)[2]]
([<class 'Good'>], [<class 'Bad'>])   # positive, negative

This makes the check O(N) in the number of protocol members, on every call, for a class that conforms.

Performance

Release build of this branch, macOS on Apple silicon, best of 11 runs of 20,000 isinstance() calls, ns/call. P has N property members; Bad differs from Good only in setting the last one to None.

N Good Bad, before Bad, after
2 178 1334 (7.5x) 162 (1.0x)
5 166 2861 (17x) 170 (1.0x)
20 167 10704 (64x) 169 (1.0x)
50 183 26882 (147x) 172 (1.0x)
110 192 55820 (291x) 186 (1.0x)

The before column scales at roughly 500 ns per protocol member. After the change Bad is a cache hit like Good, and Good itself is unaffected.

This gives _proto_hook the same rule, so such a class is cached like any other. None-valued method members are still rejected, and a missing attribute still fails.

The only externally visible change is via _allow_reckless_class_checks: an issubclass() originating in abc, functools or _py_abc now returns True where it returned False, which is what isinstance() already answers for the same pair. A direct issubclass() still raises TypeError for protocols with non-method members, so user-facing behaviour is unchanged.

…col fast path

`_ProtocolMeta.__instancecheck__` treats a member set to `None` as
"explicitly not implemented" only for callable members, but `_proto_hook`
treated any `None` in a class `__dict__` that way. A class that set a
non-callable protocol member to `None` therefore passed `isinstance()` but
was rejected by the subclass hook, so it never entered `ABCMeta`'s cache
and re-walked every protocol member on every call.

Give `_proto_hook` the same rule, so such a class is cached like any other.
`None`-valued *method* members are still rejected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TM2nkuPFj6FFmZyUJZYUQ
Comment thread Lib/test/test_typing.py
# all of the protocol members again.
@runtime_checkable
class P(Protocol):
x = 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check with a protocol defining a property as well? it's not really a callable strictly speaking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Deleted comment that the bot wrote).

Yes - added in a new commit.

A property is not callable when looked up on the class, so it lands in
__non_callable_proto_members__ alongside a plain class attribute. Both
subtests fail without the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TM2nkuPFj6FFmZyUJZYUQ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants