Search code examples
pythonpython-typingpython-attrs

attrs convert list[str] to list[float]


Given the following scenario:

import attrs

@attrs.define(kw_only=True)
class A:
    values: list[float] = attrs.field(converter=float)

A(values=["1.1", "2.2", "3.3"])

which results in

*** TypeError: float() argument must be a string or a real number, not 'list'

Obviously it's due to providing the whole list to float, but is there a way to get attrs do the conversion on each element, without providing a custom converter function?


Solution

  • As far as I know, attrs doesn't have a built-in option to switch conversion or validation to "element-wise", the way Pydantic's validators have the each_item parameter.

    I know you specifically did not ask for a converter function, but I don't really see much of an issue in defining one that you can reuse as often as you need to. Here is one way to implement a converter for your specific case:

    from attrs import define, field
    from collections.abc import Iterable
    from typing import Any
     
    def float_list(iterable: Iterable[Any]) -> list[float]:
        return [float(item) for item in iterable]
    
    @define
    class A:
        values: list[float] = field(converter=float_list)
    
    if __name__ == '__main__':
        a = A(values=["1.1", "2.2", "3.3"])
        print(a)
    

    It is not much of a difference to your example using converter=float.

    The output is of course A(values=[1.1, 2.2, 3.3]).


    You could even have your own generic converter factory for arbitrary convertible item types:

    from attrs import define, field
    from collections.abc import Callable, Iterable
    from typing import Any, TypeAlias, TypeVar
    
    T = TypeVar("T")
    ItemConv: TypeAlias = Callable[[Any], T]
    ListConv: TypeAlias = Callable[[Iterable[Any]], list[T]]
    
    def list_of(item_type: ItemConv[T]) -> ListConv[T]:
        def converter(iterable: Iterable[Any]) -> list[T]:
            return [item_type(item) for item in iterable]
        return converter
    
    @define
    class B:
        foo: list[float] = field(converter=list_of(float))
        bar: list[int] = field(converter=list_of(int))
        baz: list[bool] = field(converter=list_of(bool))
    
    if __name__ == '__main__':
        b = B(
            foo=range(0, 10, 2),
            bar=["1", "2", 3.],
            baz=(-1, 0, 100),
        )
        print(b)
    

    Output: B(foo=[0.0, 2.0, 4.0, 6.0, 8.0], bar=[1, 2, 3], baz=[True, False, True])

    The only downside to that approach is that the mypy plugin for attrs (for some reason) can not handle this type of converter function and will complain, unless you add # type: ignore[misc] to the field definition in question.