attrs
attrs copied to clipboard
Typing with converters not working
Using attrs 19.1.0 on Python 3.6.7,
from typing import Any, Iterable, List, Tuple, TypeVar
from attr import attrs, attrib
T = TypeVar("T")
def to_tuple(val: Iterable[T]) -> Tuple[T, ...]:
return tuple(val)
def to_list(val: Iterable[T]) -> List[T]:
return list(val)
@attrs
class C:
elements: Tuple[str, ...] = attrib(converter=to_tuple)
@attrs
class D:
elements: Tuple[str, ...] = attrib(converter=tuple)
@attrs
class E:
elements: List[str] = attrib(converter=list)
@attrs
class F:
elements: List[str] = attrib(converter=to_list)
if __name__ == "__main__":
l = ["a", "b", "c"]
c = C(l)
d = D(l)
e = E(l)
f = F(l)
gives
32: error: Argument 1 to "C" has incompatible type "List[str]"; expected "Iterable[T]"
33: error: Argument 1 to "D" has incompatible type "List[str]"; expected "Iterable[_T_co]"
34: error: Argument 1 to "E" has incompatible type "List[str]"; expected "Iterable[_T]"
35: error: Argument 1 to "F" has incompatible type "List[str]"; expected "Iterable[T]"
This may be related to https://github.com/python/mypy/issues/5738, but the issue goes back as far as mypy 0.610.
That looks like exactly @euresti's comment in that mypy bug?
That mypy bug appears to have been fixed, but I'm seeing something that looks like a similar problem with dict. The example in @berquist's original comment is also still failing for me.
@berquist, did you get anywhere with a workaround? Or have you seen any other mypy issues that look relevant? (I'm struggling to find any)
my minimal example with `dict`
import attrs
@attrs.frozen
class C:
foo: dict = attrs.field(factory=dict, converter=dict)
C(foo={"a": 1})
$ mypy t.py
t.py:9: error: Argument "foo" to "C" has incompatible type "dict[str, int]"; expected "_VT | SupportsKeysAndGetItem[_KT, _VT] | SupportsKeysAndGetItem[str, _VT] | Iterable[tuple[_KT, _VT]] | Iterable[tuple[str, _VT]] | Iterable[list[str]] | Iterable[list[bytes]]" [arg-type]
Found 1 error in 1 file (checked 1 source file)
$ python --version
Python 3.11.5
$ pip freeze
attrs==23.1.0
mypy==1.5.1
mypy-extensions==1.0.0
typing_extensions==4.7.1
I just ran into the same problem in an essentially analogous situation to @berquist's. (Hiya, Eric! Been a while, hope all is well.)
I was able to satisfy mypy by wrapping the list conversion in a lambda, instead of just passing the raw type itself.
Toy example:
import attrs
@attrs.define(kw_only=True)
class ClassConverter:
my_list: list[int] = attrs.field(converter=list)
def evolve_my_list(self, new_list: list[int]):
return self.__class__(my_list=new_list)
@attrs.define(kw_only=True)
class LambdaConverter:
my_list: list[int] = attrs.field(converter=lambda v: list(v))
def evolve_my_list(self, new_list: list[int]):
return self.__class__(my_list=new_list)
mypy output:
9: error: Argument "my_list" to "ClassConverter" has incompatible type "list[int]"; expected "Iterable[_T]" [arg-type]
And no typing error on LambdaConverter.
Python 3.11.7 on Debian WSL2 attrs 23.1.0 mypy 1.8.0 (compiled: yes)
I don't know if this is a good or right way to address this, but it's semantically correct and makes mypy happy, sooooo.... ¯\_(ツ)_/¯
Hi @bskinn, you need to use a dedicate converter function for typing to work correctly, because list is not Callable[[Any], list[int]]. You should also use the @classmethod decorator for your factory functions.
The following code works with Python 3.12 and mypy 1.8:
from typing import Any, Self, Type
import attrs
def to_int(v: Any) -> list[int]:
return [int(item) for item in v]
@attrs.define(kw_only=True)
class ClassConverter:
my_list: list[int] = attrs.field(converter=to_int)
@classmethod
def evolve_my_list(cls: Type[Self], new_list: list[int]) -> Self:
return cls(my_list=new_list)
@attrs.define(kw_only=True)
class LambdaConverter:
my_list: list[int] = attrs.field(converter=lambda v: list(v))
@classmethod
def evolve_my_list(cls: Type[Self], new_list: list[int]) -> Self:
return cls(my_list=new_list)
Thanks, @sscherfke!
I was wondering if a function with an explicit internal int cast would be the most correct way to do it.
And you're right, for that toy example, @classmethod is the way to go. For my actual use-case (private code, unfortunately), I'm defining custom __add__ and __mul__, so as best I understand I do need instance methods and self.__class__(...).
(FWIW I also need runtime checks to be sure that the incoming argument is actually isinstance(addend, int), so there's yet further difference there, but that's all implementation-specific. Didn't want to clutter my proposed solution with all that.)