Consider this code:
import typing
class A:
@property
def f(self) -> int:
return 1
@f.setter # Possible error for defining a setter that doesn't accept the type the @property returns
def f(self, x: str) -> None:
pass
a = A()
a.f = '' # Possibly not an error, but currently mypy gives an error
a.f = 1 # Certainly should be an error, unless we disallowed
# defining the setter this way in the first place.
reveal_type(a.f) # E: Revealed type is 'builtins.int'
Whatever we decide, I'm happy to build a PR; I have this code loaded into my head.
In other words "with arbitrary setters, you can have an lvalue be of a different type than its corresponding rvalue"
IIRC we debated a similar issues when descriptors were added and decided that it's a perverse style that we just won't support. If you really have this you can add # type: ignore.
Right now, you have to # type: ignore the line that says a.f = '', but not the definition. Should you have to # type: ignore the setter definition too? My inclination is yes, but I'm not sure of this.
What does a discrepancy between __get__ and __set__ (when using as a descriptor) do? I think that's the example to follow.
One real-life case where I encountered this a few times now is a normalizing property like this:
from typing import Set, Iterable
class Foo:
def __init__(self) -> None:
self._foo = set() # type: Set[int]
@property
def foo(self) -> Set[int]:
return self._foo
@foo.setter
def foo(self, v: Iterable[int]) -> None:
self._foo = set(v)
Foo().foo = [1, 2, 3]
I like this implementation, because it allows a user of the cleverly named class Foo not to care about the exact type of the property, while Foo can use the best representation internally, while also giving additional guarantees when accessing the property.
Using the same type in the getter and setter would complicate the life of its users.
I disagree that this is "perverse" -- contravariance in the setter is one example, e.g. where the getter returns Set[x] and the setter takes Collection[x]:
from typing import Collection, Set
class X():
@property
def hello(self) -> Set[str]:
return {"x", "y"}
@hello.setter
def hello(self, value: Collection[str]) -> None:
pass
x = X()
x.hello = ["1", "2", "3"]
% mypy --version
mypy 0.701
% mypy mypy3004.py
mypy3004.py:13: error: Incompatible types in assignment (expression has type "List[str]", variable has type "Set[str]")
In my current case, I'm being even stricter in the getter, returning FrozenSet[x] to prevent accidental misuse of the returned collection (thinking it will change the attribute in the object itself).
What does a discrepancy between
__get__and__set__(when using as a descriptor) do? I think that's the example to follow.
Descriptors actually support different types as one would expect. However, properties were implemented before descriptors (using some heavy special-casing) so they don't support this.
I think this is a valid feature to support. I have seen this a lot recently in internal code bases, mostly in context similar to example by @srittau (canonical representation). This however may be tricky to implement (because of the special-casing I mentioned). Maybe a better strategy would be to just make property a regular descriptor in mypy (I believe we already have an issue for that), then this will be supported automatically.
FTR, the main issue about properties is https://github.com/python/mypy/issues/220
Here's another example that I don't find terribly perverse. Using None to indicate "use the default value":
class A:
def __init__(self, label=None):
# type: (Optional[str]) -> None
self._user_label = None # type: Optional[str]
self.label = label
@property
def label(self):
# type: () -> str
return self._user_label or self.default_label()
@label.setter
def label(self, value):
# type: (Optional[str]) -> None
self._user_label = value
def default_label(self):
# type: () -> str
return self.__class__.__name__
I am going to remove the "needs discussion" label. IMO it is now pretty clear we should support this, the only discussion is what is the best way to implement this.
perhaps I'm missing sth, but doesn't that mean you can get
class X: pass
class Foo:
@property
def foo(self) -> int:
...
@foo.setter
def foo(self, o: Union[X, int]) -> None:
...
foo = Foo()
x = X()
foo.bar = x
assert foo.bar != x
I wonder if there are cases where this makes sense (e.g. where equality still holds - perhaps this can work in the Iterable/Set example), but others where I also feel it seems odd e.g. using None for default values
My request would be for there to be a flag to turn this on, and it to be off by default
I think this is the problem I am encountering now with mypy 0.781. Here's my distilled example:
from datetime import timedelta
from typing import Union
Interval = Union[timedelta, int]
class Foo:
def __init__(self):
self._x = timedelta(seconds=15)
@property
def x(self) -> timedelta:
return self._x
@x.setter
def x(self, delta: Interval) -> None:
if isinstance(delta, timedelta):
self.x = delta
else:
self.x = timedelta(seconds=delta)
foo = Foo()
foo.x = 7
foo.x = timedelta(seconds=8)
And the error I'm getting:
% mypy foo.py
foo.py:24: error: Incompatible types in assignment (expression has type "int", variable has type "timedelta")
Found 1 error in 1 file (checked 1 source file)
The idea behind the x property is that its type "is" a timedelta but you can set it with an int or a timedelta and the former always gets coerced to the latter. I think those are pretty pedestrian semantics!
Seems like this is the same problem described in this issue. I can understand that mypy may not be able to infer this.
I wonder if there are cases where this makes sense (e.g. where equality still holds - perhaps this can work in the Iterable/Set example), but others where I also feel it seems odd e.g. using
Nonefor default valuesMy request would be for there to be a flag to turn this on, and it to be off by default
@joelberkeley-pio, what you're asking for is a Python feature, not a MyPy feature. This is part of the whole point of properties -- to divorce the getters and setters from exposing bare values of members, so we don't need to have setX() and getX() like in Java.
For example, this is also valid Python (and, apart from the contrived simplification, actually used in some libraries):
from typing import Union
import requests
class RemoteObject:
def __init__(self, object_id: str):
self.object_id = object_id
@property
def value(self) -> float:
return float(requests.get(f"https://example.com/{self.object_id}/value").text)
@value.setter
def value(self, new_value: Union[int, float]) -> None:
requests.post(f"https://example.com/{self.object_id}/value",
data=str(new_value).encode("utf-8"))
Anything could happen on the remote server between the time the value is set and retrieved. There is absolutely no expectation that the values should be identical.
Please allow me to add another example of that same issue, for a use case which I believe is sane:
from typing import Any, Optional, Union
class Foo:
def __init__(self, foo: str, **kwargs: Any) -> None:
self.foo = foo
class Bar:
def __init__(self, foo: Union[str, Foo] = None) -> None:
self.foo = foo
@property
def foo(self) -> Optional[Foo]:
return self._foo
@foo.setter
def foo(self, value: Union[str, Foo]) -> None:
if value is not None and not isinstance(value, Foo):
value = Foo(value)
self._foo = value
This gives: mypy_property.py:9: error: Incompatible types in assignment (expression has type "Union[str, Foo, None]", variable has type "Optional[Foo]")
following
following
Github has a subscribe button for this purpose that creates less noise.
Is there some workaround for this problem which does not require the consumer of the property to use # type: ignore on each property usage? I have a library Im trying to add type hints to, similar to the (very simplified) code below, and this problem comes up a lot.
class Vec:
x: float = 0
y: float = 0
def __init__(self, x, y):
self.x = x
self.y = y
class A:
@property
def position(self):
return self._position
@position.setter
def position(self, v):
if isinstance(v, Vec):
self._position = v
else:
self._position = Vec(v[0], v[1])
a = A()
a.position = (1, 2)
print(a.position.x)
Use ‘Any’.
Thanks for the quick reply! Unfortunately I still cant figure it out.
I modified my example like below:
from typing import Any
class Vec:
x: float = 0
y: float = 0
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
class A:
_position: Vec
@property
def position(self) -> Any:
return self._position
@position.setter
def position(self, v: Any) -> None:
if isinstance(v, Vec):
self._position = v
else:
self._position = Vec(v[0], v[1])
a = A()
a.position = (1, 2)
reveal_type(a.position)
print(a.position.x)
Now it works in Pylance/pyright, but mypy still complains. In pyright the revealed type is Any, but in mypy it says builtins.list[Any] and I still get the type error on the last line.
Try getting help on gitter
There is a way to work around the example given in the comment above by @srittau
https://github.com/python/mypy/issues/3004#issuecomment-368007795
from typing import Set, Iterable
class Foo:
def __init__(self) -> None:
self._foo = set() # type: Set[int]
def get_foo(self) -> Set[int]:
return self._foo
def set_foo(self, v: Iterable[int]) -> None:
self._foo = set(v)
foo = property(get_foo, set_foo)
a = Foo()
a.foo = [1, 2, 3]
print(a.foo)
Instead of using the property decorators, just use the property class
There is a way to work around the example given in the comment above by @srittau
#3004 (comment)from typing import Set, Iterable class Foo: def __init__(self) -> None: self._foo = set() # type: Set[int] def get_foo(self) -> Set[int]: return self._foo def set_foo(self, v: Iterable[int]) -> None: self._foo = set(v) foo = property(get_foo, set_foo) a = Foo() a.foo = [1, 2, 3] print(a.foo)Instead of using the property decorators, just use the property class
That removes the mypy error, but I believe you also won't get an error when you type
a.foo = "type should fail"
Most helpful comment
I am going to remove the "needs discussion" label. IMO it is now pretty clear we should support this, the only discussion is what is the best way to implement this.