Skip to content

Clarification - protocol properties vs attributes #1296

@erichulburd

Description

@erichulburd

Describe the Bug

When implementing a typing.Protocol it's not entirely clear to me when an attribute conforms to a property protocol method and vice versa. In particular, I've noticed that Pyrefly will allow typing.Protocol properties to be implemented with attributes, but not vice versa; Pyright somewhat contradictorily, permits properties to be implemented with attributes at a call site, but not if the protocol is inherited.

The Pyrefly behavior is more self-consistent, but I would like to confirm why this is the case. Is there official Python documentation to substantiate this behavior? If not, then why does Pyrefly accept attributes as protocol property implementations but not vice versa?

Note, the PEP 544, which introduced Protocols, explicitly ruled in protocol variable members (what I've referred to as attributes above): https://peps.python.org/pep-0544/#allow-only-protocol-methods-and-force-use-of-getters-and-setters

Examples

Protocol inheritance

from typing import List, Protocol
from dataclasses import dataclass


class Example1(Protocol):
    @property
    def samples(self) -> List[int]: ...


@dataclass(kw_only=True)
class Example1Impl(Example1):
    samples: List[int]


def _example1(result: Example1) -> List[int]:
    return result.samples


class Example2(Protocol):
    samples: List[int]


@dataclass(kw_only=True)
class Example2Impl(Example2):
    @property
    def samples(self) -> List[int]:
        return [1, 2, 3]

def _example2(result: Example2) -> List[int]:
    return result.samples


def main():
    r1 = Example1Impl(samples=[4, 5, 6])
    _example1(r1)

    r2 = Example2Impl()
    _example2(r2)

Pyrefly:

  --> src/my_project/example.py:26:9
   |
26 |     def samples(self) -> List[int]:
   |         ^^^^^^^
   |
  `Example2Impl.samples` is a property, but `Example2.samples` is not

Pyright:

src/my_project/example.py
  src/my_project/example.py:12:5 - error: "samples" incorrectly overrides property of same name in class "Example1" (reportIncompatibleMethodOverride)
  src/my_project/example.py:26:9 - error: "samples" overrides symbol of same name in class "Example2"
    "property" is not assignable to "List[int]" (reportIncompatibleVariableOverride)
2 errors, 0 warnings, 0 informations

Protocol at call site

from typing import List, Protocol
from dataclasses import dataclass


class Example1(Protocol):
    @property
    def samples(self) -> List[int]: ...


@dataclass(kw_only=True)
class Example1Impl:
    samples: List[int]


def _example1(result: Example1) -> List[int]:
    return result.samples


class Example2(Protocol):
    samples: List[int]


@dataclass(kw_only=True)
class Example2Impl:
    @property
    def samples(self) -> List[int]:
        return [1, 2, 3]

def _example2(result: Example2) -> List[int]:
    return result.samples


def main():
    r1 = Example1Impl(samples=[4, 5, 6])
    _example1(r1)

    r2 = Example2Impl()
    _example2(r2)

Pyrefly:

ERROR Argument `Example2Impl` is not assignable to parameter `result` with type `Example2` in function `_example2` [bad-argument-type]
  --> src/my_project/example.py:38:15
   |
38 |     _example2(r2)

Pyright:

  src/my_project/example.py:38:15 - error: Argument of type "Example2Impl" cannot be assigned to parameter "result" of type "Example2" in function "_example2"
    "Example2Impl" is incompatible with protocol "Example2"
      "samples" is invariant because it is mutable
      "samples" is an incompatible type
        "property" is not assignable to "List[int]" (reportArgumentType)
1 error, 0 warnings, 0 informations

Sandbox Link

https://pyrefly.org/sandbox/?project=N4IgZglgNgpgziAXKOBDAdgEwEYHsAeAdAA4CeS4ATrgLYAEALqcROgOZ0Q3G6UN0AZCHAYAaOgAVqDXAGNcUADrow1eplQNUsqKjhx4nbr34atOvXGXX0F-XQCi%2BVN1gBGABRTcM%2BVACUiMp0IXQAAsTUxDB8pMGhmDBgdGiu8B4GUGD%2BdAC0AHyCwgwA2qwMALqIdIS1NsphZtq6%2Bh4A1gDuAPq46FCkALwAKpQArjD%2BynZwjs5pbgCSrh5OLsTugfEhqevw1UIiZeiV9VhJdF0wc7uelPCjUAzVq-M5BUWH5VVbdHcMo5R0L97o9CDtYFZ0DZprM1rAAExeaRyBSbIGhcF7D6lL6nRqaZqWdrdXr9YZjCZTFozF67eFLdYra4ItGhcKRXDRWI-RLJTFwDIwLJvQoHHHHb7otkhP4AoElNzieHiADMFRsvIuVzhMERdzgDyesLS8JF2KOlSCUuB-0BwINoP5p01NFQrA8rNClDcdAGxpuDKgGR1cAGJQALOIAKziABsFUm1suzJgtzciZ%2BlHhvv9CMDHp%2ByZ1etNNhAohAowY0DgJHIiBAAGI6ABVatQCBMOhgUa2au9SHKTVgXiuhhddCjGjYGIefDVcpmkSUK1s2V2sCKEAAOSnM5XdGA%2BAAvlvlOWQGQ7mB%2BoQZDQoBRmxJSNf%2BikMDgCHR5CoIGwAU0CBekIZRmwAZRgGA6AACwYBhiDgRAAHpkKvJJb14NhkJgdBkMwOQ4GQ39IAAyggN6ZDu14OhUAANzdXRsFgH9elIwD%2ByBTlONrZQyAYGDelyOiYjgYCgT9LcVUINx4TPdAQGPCttGrESADFoBgCg0CwPAiDIRSgA

(Only applicable for extension issues) IDE Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-discussionAn issue where it's not clear whether there is a bug or we are behaving as expected.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions