GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — `typing`: one `None`-valued member makes `isinstance()` on a `runtime_checkable` Protocol O(N) per call · Issue #156413 · python/cpython

typing: one None-valued member makes isinstance() on a runtime_checkable Protocol O(N) per call #156413

New issue
Copy link
New issue
Copy link
Open
Open
typing: one None-valued member makes isinstance() on a runtime_checkable Protocol O(N) per call#156413
Copy link
Labels
performancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-featureA feature request or enhancementA feature request or enhancement

Description

@adamtheturtle
adamtheturtle
opened on Aug 26, 2026
Issue body actions

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.

import timeit
from typing import Protocol, runtime_checkable

@runtime_checkable
class P(Protocol):
    @property
    def a(self) -> bool: ...
    @property
    def b(self) -> bool: ...

class Good:
    a = b = True

class Bad:
    a, b = True, None

for x in (Good(), Bad()):
    print(isinstance(x, P), timeit.timeit(lambda: isinstance(x, P), number=100_000))

On 3.13.13: True 0.0121 then True 0.1207. Only the timing differs.

Linked PRs

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

Activity

  1. aisk
    added
    topic-typing
    type-featureA feature request or enhancementA feature request or enhancement
    performancePerformance or resource usagePerformance or resource usage
    on Aug 26, 2026
  2. adamtheturtle
    added a commit that references this issue on Aug 26, 2026

    Merge pull request #4628 from adamtheturtle/adamtheturtle/cite-cpytho…

    a925507
  3. picnixz commented on Aug 26, 2026

    @picnixz
    picnixz
    on Aug 26, 2026
    Member
    More actions
    1. There could be caching?
    2. Does it happen on every versions?
    3. Can you bisect the bad commit, if any?
  4. adamtheturtle commented on Aug 27, 2026

    @adamtheturtle
    adamtheturtle
    on Aug 27, 2026
    ContributorAuthor
    More actions
    1. 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.
    >>> [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 = 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__).

    1. 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.

    2. 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 moved super().__instancecheck__(instance) to the top, which is what made the ABC cache reachable for protocols with non-callable members. Bad never satisfies _proto_hook, so it was never covered by it.

  5. picnixz
    added
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    on Aug 27, 2026
  6. picnixz commented on Aug 27, 2026

    @picnixz
    picnixz
    on Aug 27, 2026
    Member
    More actions

    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.

    cc @JelleZijlstra @AlexWaygood

  7. adamtheturtle commented on Aug 27, 2026

    @adamtheturtle
    adamtheturtle
    on Aug 27, 2026
    ContributorAuthor
    More actions

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-featureA feature request or enhancementA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.