facebook / facebook/pyrefly

Clarification - protocol properties vs attributes

Open
#1,296 3 comments 0 reactions 0 assignees View on GitHub
needs-discussion
Dominant language
Rust
Stars
7k
Forks
516
PR merge metrics
No merged PRs in 30d

Description

### 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

```python
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

```python
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_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.