Skip to content

False positive: Protocol with EXCLUDED_ATTRIBUTES cannot be used with issubclass() #5614

Description

@amas127

Describe the Bug

from typing import runtime_checkable, Protocol
from abc import abstractmethod

# >>> Current

@runtime_checkable
class Buffer(Protocol):
    __slots__ = ()

    @abstractmethod
    def __buffer__(self, flags: int, /) -> memoryview: ...

x = issubclass(type(bytes()), Buffer)  # error, expected good

# >>> Comparison

@runtime_checkable
class BufferExt(Protocol):
    @abstractmethod
    def __buffer__(self, flags: int, /) -> memoryview: ...

x = issubclass(type(bytes()), BufferExt)  # good

Python: 3.12

Sandbox Link

https://pyrefly.org/sandbox/?project=v2.xZXNboMwEIRfZSUOgITwPQdUteq9bxAZYojVgJF_UvL2Hdu4LUVKVfXQq4XHu-x8s7-ITO0mKzGg7iy615gdKeXWmPiSNLyFm_BqhMFPLaOmaejJaS0mG3DeCSZCH13fC10k9WTK49FclDXeIRSd4E8fvj_1SXUbdD647i98QCTAJBWxYM1RjErfrlK8Haiuay-5QFwa49pQSuFTtWhv-JtFWVZrZSVRFk1SkVhmDFGcaFCbPtU4cy2NwsnPrT4vdtftP_SFMkJrvpW9vTN6iQe0OgXBDi4Gh_UBv-OVDJtD0Nna2RwYS9eVHpiY2El1hm1uMPIk8yuXYSlt5UgFHA1k_8JVHrHK71GVe6jwxV1Q3gE

(Only applicable for extension issues) IDE Information

No response

Activity

  1. amas127 commented on Nov 11, 2026

    @amas127
    Author

    Description

    When an "runtime_checkable" Protocol defines attributes defined in EXCLUDED_ATTRIBUTES (e.g., __slots__), Pyrefly incorrectly treats them as non-callable / data members of the protocol. As a result, using this protocol with issubclass() triggers a false-positive error.

    Steps to Reproduce

    Shown as in Sandbox Link.

    Related

    cpython/Lib/typing.py
    python/typing #1833
    microsoft/pyright #11834
    "Should Protocols with empty __slots__ count as Data Protocols?"
    python/cpython #113320
    python/cpython #106363

    Expected Behavior

    Pyrefly should exclude attribute names included in EXCLUDED_ATTRIBUTES when checking whether a protocol contains non-method members. issubclass(bytes, Buffer) should be accepted without diagnostic errors.

    Current Behavior

    Pyrefly flags issubclass(bytes, Buffer) with:

    Protocol `Buffer` has non-method members and cannot be used with issubclass()
    

    Additional Context & Behavior

    1. CPython Runtime Behavior

    In CPython's typing.py, __slots__ is explicitly listed in EXCLUDED_ATTRIBUTES and filtered out by _get_protocol_attrs(). Consequently, Buffer.__non_callable_proto_members__ evaluates to set(), and issubclass(bytes, Buffer) executes cleanly at runtime, returning True.

    1. Standard Library Precedent

    Many @runtime_checkable protocols in the standard library (such as typing.SupportsInt, typing.SupportsFloat, and collections.abc.Buffer) define __slots__ = () by convention, and checking issubclass(..., SupportsInt) is valid both at runtime and in typeshed.

    1. Typing Community Consensus

    As noted by Guido van Rossum on Discuss Python "Should Protocols with empty __slots__ count as Data Protocols?":

    "Empty `__slots__` no more makes it a data protocol as `__doc__`, `__module__` and others."
    

    Control Cases and Comparisons

    Case Declaration CPython 3.12+ runtime
    A __slots__ = () plus a method issubclass() succeeds
    B typing.SupportsInt (__slots__ = () in typeshed) issubclass() succeeds
    C __slots__ = ("value",) plus a method TypeError: Protocols with non-method members don't support issubclass()
    D Plain data member value: int TypeError: Protocols with non-method members don't support issubclass()
    E __slots__ = () plus value: int TypeError: Protocols with non-method members don't support issubclass()

    A correct checker should accept A and B, and report an error for C, D, and E.

    The following results were produced with each checker's default settings, except where noted. TypeError below refers to the CPython runtime result, not to the checker. A checker never raises TypeError in this comparison; it either emits a diagnostic or does not.

    • none: the checker emits no diagnostic; CPython succeeds.
    • FP: the checker emits a diagnostic; CPython succeeds, so the diagnostic is a false positive.
    • diagnostic: the checker emits a diagnostic; CPython raises TypeError, so the diagnostic is correct.
    • missed: the checker emits no diagnostic; CPython raises TypeError, so the checker missed an error.
    mypy --strict --python-version 3.13 case.py
    pyright --pythonversion 3.13 --level error case.py
    basedpyright --pythonversion 3.13 --level error case.py
    pytype -V 3.12 case.py
    zuban check --strict case.py
    ty check case.py
    pyre check                         # with source_directories = ["."]
    pyrefly check case.py --preset strict
    Checker A: empty slots B: SupportsInt C: non-empty slots D: data member E: empty slots + data member
    CPython 3.13 / 3.15 succeeds succeeds TypeError TypeError TypeError
    mypy 2.4.1 (--strict) none none missed diagnostic diagnostic
    pyright 1.1.414 FP FP diagnostic diagnostic diagnostic
    basedpyright 1.40.2 FP FP diagnostic diagnostic diagnostic
    pytype 2024.10.11 (-V 3.12) none none missed missed missed
    zuban 0.10.2 (--strict) none none missed diagnostic diagnostic
    ty 0.0.101 none none missed diagnostic diagnostic
    pyre-check 0.10.0 none none missed missed missed
    Pyrefly 1.4.1 (--preset strict) FP FP diagnostic diagnostic diagnostic
  2. amas127 commented on Nov 11, 2026

    @amas127
    Author

    #claim

  3. github-actions commented on Nov 11, 2026

    @github-actions

    @amas127 you've claimed this issue — it's now assigned to you. Thanks for picking it up! 🎉

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions