Attrs: Best Practice in Python 3 for Type Annotation?

Created on 6 Oct 2019  路  7Comments  路  Source: python-attrs/attrs

@attr.s
class A:
    foo = attr.ib(type=int, init=False, default=10)

vs:

@attr.s(auto_attribs=True)
class B:
    foo: int = attr.ib(init=False, default=10)

The latter may be the best practice? Because IDEs are mostly written to understand Python 3 type hints (foo: int) for auto-completion in the IDE... Any IDE which wants to support foo = attr.ib(type=int... to understand that self.foo is an int, would need special code to parse attr.ib parameters...

What do you think @hynek ?

Most helpful comment

At our company we write all our attrs python 3 code as:

@attr.dataclass
class C:
   foo: x  # no default
   bar: int = 6  # default
   baz: str = attr.ib(init=False, default="hi")   # Any other option.

Se we don't waste out time with attr.ib unless absolutely necessary.

All 7 comments

Looking at @hynek 's code for handling foo: int = 5, I saw that it turns it into an attr.ib (_CountingAttr) internally if its value is not already that type:

https://github.com/python-attrs/attrs/blob/754fae0699e52dc7d05819c1a3f4c4749e804e4c/src/attr/_make.py#L324-L342

That code says: If foo: int = 5, then turn 5 into attr.ib(default=5) (it creates an attr.ib object internally and replaces "5" with that object). Else if foo: int = attr.ib(default=5, init=False), keep the attr.ib as-is.

So internally, in attrs, everything looks good: If you annotate as foo: <type> = ..., and something is not already an attr.ib, then that's created. If it's already an attr.ib, then it's kept as-is.

Therefore there seems to be no downsides to the foo: str = attr.ib(default="bar", init=False) style, internally in attrs. In fact there's even testcases that ensure that the annotated type is properly written to the field's "type" in attrs, and that there's never a mix of both styles (ie x: int = attr.ib(type=int) will throw an error):

https://github.com/python-attrs/attrs/blob/dc1b5a01e98a9ba49d45ad07ba6ae0ee2d9c1b8e/tests/test_annotations.py#L23-L55

And that brings me back to the original question: Is the foo: <type> = ... PEP style preferable code-style these days? I believe it would help most IDEs infer the type better, since any IDE that wants to support foo = attr.ib(type=<type>, ...) would need special parsing that understands the attrs library.

So the more universal way to write Python 3+ code from now on is foo: int = attr.ib(init=False, default=10) ?

Alright, I just checked in VS Code with the Python extension:

@attr.s
class C:
    one = attr.ib(type=int)
    two: int = attr.ib()

    def foo(self):
        self.one # if you hover over this line it says nothing
        self.two # if you hover over this line a tooltip says "two: int"

That seems to settle it. I'm going to rewrite all code to use foo: <type> = attr.ib() style, and @attr.s(auto_attribs=True).

At our company we write all our attrs python 3 code as:

@attr.dataclass
class C:
   foo: x  # no default
   bar: int = 6  # default
   baz: str = attr.ib(init=False, default="hi")   # Any other option.

Se we don't waste out time with attr.ib unless absolutely necessary.

@euresti Hehe, that's amazing that you use @attr.dataclass... it was just meant as a joke (see https://github.com/python-attrs/attrs/issues/408), and is an alias for @attr.s(auto_attribs=True).

Anyway, the code you wrote above makes the @attr decorator auto-generate attr.ib objects anytime you didn't write it explicitly. So yeah your code is exactly equivalent to this:

@attr.s(auto_attribs=True)
class C:
   foo: x = attr.ib()  # no default
   bar: int = attr.ib(default=6)  # default
   baz: str = attr.ib(init=False, default="hi")   # Any other option.

But, for some reason even though auto_attribs auto-generates the attr.ib objects, I don't like how such mixed code looks... and I think it looks cleaner to fully write attr.ib like I did here.

I am really curious what the author/members think about all of this...

On a sidenote, I wanted a quick way to apply my defaults in my whole project, so I made a classtools.py module and added this code to it:

from functools import partial

from attr import attrs

attrclass = partial(attrs, auto_attribs=True, slots=True)

And then tried the following:

import attr

from classtools import attrclass

@attrclass
class What:
    x: int = attr.ib()
    y: str = attr.ib(default="hi")

foo = What("z", "y")

But MyPy does not understand that @attrclass is an alias for @attr.s(auto_attribs=True, slots=True), so I get code errors on foo = What("z", "y") with the claim that I am passing too many arguments to the class. Sad.

They mention the problem here:
https://mypy.readthedocs.io/en/latest/additional_features.html#id1

_"The detection of attr classes and attributes works by function name only. This means that if you have your own helper functions that, for example, return attr.ib() mypy will not see them."_

It also mentions that MyPy scans the @attr.s(blahblah) parameters and only understands explicitly written stuff...

So I'll delete that whole "alias" module idea, and write @attr.s(auto_attribs=True, slots=True) explicitly above all of my classes. It's no big deal, I just wanted to save typing and reduce maintenance.

@VideoPlayerCode While this is an interesting discussion, I'm not sure it's an actionable issue.

I'd suggest that foo: int annotations are a best practice in Py3 because it's what the Py3 docs tell you to do, but I don't think attrs is trying to tell you what the best practice is. I do what @euresti does, but I understand why you like consistency. YYMV.

Is there a thing you want done here, os can we close the ticket?

@wsanchez Thanks for your additional comment. I've come to the same conclusion about the best practice being foo: int since that's what Python 3's own docs demand, and all IDEs expect that (for type hints in tooltips/autocompletion). That was basically what settled everything for me. So the most pythonic code is foo: int = <..........> and I've switched all attrs classes over to that style.

As for explicitly writing the attr.ib() object for consistency, or only using that when extra parameters are needed, that's up to individual choice. But the _type annotation_ part should always be in the Python 3 style.

So this issue is solved. Thanks to both of you for your help! :-)

Was this page helpful?
0 / 5 - 0 ratings