Skip to content

Protocols with property descriptors require all compliant symbols to use the same implementation. #9202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mostrows2 opened this issue Jul 24, 2020 · 2 comments · Fixed by #18943
Labels
topic-descriptors Properties, class vs. instance attributes

Comments

@mostrows2
Copy link

  • Are you reporting a bug, or opening a feature request?

Bug.

  • Please insert below the code you are checking with mypy,
    or a mock-up repro if the source is private. We would appreciate
    if you try to simplify your case to a minimal repro.

Code snippet attached at end of report.
It appears to be not possible to define a Protocol describing an object that has an attribute named "prop1" with type "int".
Any attempt to do so binds the resulting protocol to the implementation chosen in defining the protocol (i.e. must use the same property/descriptor type).

  • What is the actual behavior/output?
 % mypy /tmp/test.py
/tmp/test.py:72: error: Incompatible types in assignment (expression has type "Foo3", variable has type "Proto1")
/tmp/test.py:72: note: Following member(s) of "Foo3" have conflicts:
/tmp/test.py:72: note:     prop1: expected "int", got "cached_property[int]"
/tmp/test.py:73: error: Incompatible types in assignment (expression has type "Foo4", variable has type "Proto1")
/tmp/test.py:73: note: Following member(s) of "Foo4" have conflicts:
/tmp/test.py:73: note:     prop1: expected "int", got "Property[Foo4, int]"
/tmp/test.py:75: note: Revealed type is 'def (self: test.Foo1) -> builtins.int'
/tmp/test.py:76: note: Revealed type is 'builtins.int'
/tmp/test.py:77: note: Revealed type is 'functools.cached_property[builtins.int*]'
/tmp/test.py:78: note: Revealed type is 'test.Property[test.Foo4*, builtins.int*]'
Found 2 errors in 1 file (checked 1 source file)
  • What is the behavior/output you expect?

No errors should be reported.

  • What are the versions of mypy and Python you are using?

mypy == 0.770 , Python=3.8.4.

  • What are the mypy flags you are using? (For example --strict-optional)

None.

import functools
from typing import Callable, cast, Generic, overload, Protocol, Type, TypeVar, Optional, TYPE_CHECKING, Union

T = TypeVar('T')
V = TypeVar('V')


# Arbitrary descriptor implementation.
class Property(Generic[T, V]):
    def __init__(self, fn: Callable[[T], V]):
        self.fn = fn

    @overload
    def __get__(self, obj: T, owner: Type[T]) -> V: ...

    @overload
    def __get__(self, obj: None, owner: Type[T]) -> Property[T, V]: ...    
     
         
    def __get__(self, obj: Optional[T], owner: Type[T]) -> Union[Property[T, V], V]:
        if obj is None:
            return self
        return self.fn(obj)



# Protocols that define pretty much the same thing.
class Proto1(Protocol):
    @property
    def prop1(self) -> int:
        ...

class Proto2(Protocol):
    prop1: int

class Proto3(Protocol):
    @functools.cached_property
    def prop1(self) -> int:
        ...

class Proto4(Protocol):
    @Property
    def prop1(self) -> int:
        ...        

# Implementation of each protocol above
class Foo1:
    @property
    def prop1(self) -> int:
        return 1

class Foo2:
    def __init__(self, x: int):
        self.prop1 = x
    
class Foo3:
    @functools.cached_property
    def prop1(self) -> int:
        return 3

class Foo4:
    @Property
    def prop1(self) -> int:
        return 4    


# All 4 statements below have RHS values that conform to the behavior
# defined by 'Proto1': objects that have a 'prop1' attribute that is an 'int',
# and that is not settable.
# No errors should be reported.
# It does not appear possible to define a common protocol that all 4 classes
# conform to, even though in code they all have valid common behavior:
# "intVal: int = x.prop1"

x : Proto1 = Foo1()
y : Proto1 = Foo2(2)
z : Proto1 = Foo3()
a : Proto1 = Foo4()

if TYPE_CHECKING:
    reveal_type(Foo1.prop1)
    reveal_type(Foo2.prop1)
    reveal_type(Foo3.prop1)
    reveal_type(Foo4.prop1)    

@mostrows2
Copy link
Author

Would not surprise me if this was effectively the same problem as #7153, but here the issue is with how to define a Protocol.

@JelleZijlstra JelleZijlstra added the topic-descriptors Properties, class vs. instance attributes label Mar 19, 2022
@nejcskofic
Copy link

I was hit today by this as well. It seems there is no way to define read only attribute in protocol, which in concrete implementation can be property, custom descriptor or standard field.

I'm using mypy 1.0.1.

Request for defining read only attribute was also raised in #6002.

cdce8p pushed a commit to cdce8p/mypy that referenced this issue May 31, 2025
Fixes python#18024
Fixes python#18706
Fixes python#17734
Fixes python#15097
Fixes python#14814
Fixes python#14806
Fixes python#14259
Fixes python#13041
Fixes python#11993
Fixes python#9585
Fixes python#9266
Fixes python#9202
Fixes python#5481

This is a fourth "major" PR toward
python#7724. This is one is
watershed/crux of the whole series (but to set correct expectations,
there are almost a dozen smaller follow-up/clean-up PRs in the
pipeline).

The core of the idea is to set current type-checker as part of the
global state. There are however some details:
* There are cases where we call `is_subtype()` before type-checking. For
now, I fall back to old logic in this cases. In follow up PRs we may
switch to using type-checker instances before type checking phase (this
requires some care).
* This increases typeops import cycle by a few modules, but
unfortunately this is inevitable.
* This PR increases potential for infinite recursion in protocols. To
mitigate I add: one legitimate fix for `__call__`, and one temporary
hack for `freshen_all_functions_type_vars` (to reduce performance
impact).
* Finally I change semantics for method access on class objects to match
the one in old `find_member()`. Now we will expand type by instance, so
we have something like this:
  ```python
  class B(Generic[T]):
      def foo(self, x: T) -> T: ...
  class C(B[str]): ...
  reveal_type(C.foo)  # def (self: B[str], x: str) -> str
  ```
FWIW, I am not even 100% sure this is correct, it seems to me we _may_
keep the method generic. But in any case what we do currently is
definitely wrong (we infer a _non-generic_ `def (x: T) -> T`).

---------

Co-authored-by: hauntsaninja <[email protected]>
Co-authored-by: Shantanu <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic-descriptors Properties, class vs. instance attributes
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants