Skip to content

[RFC] Inconvenient defaults? #487

Closed

Description

@hynek

As part of “Projectimport attrs” (see also #408) we get a unique opportunity to change default options/behaviors that grew over the years but couldn't be fixed due to backward-compatibility restrictions.

The rough plan is create a new, friendlier API on top of @attr.s and attr.ib() that won't go anywhere.

The following is cast in stone:

The following would be really nice to have:

What else is bugging you?


One thing that I just can't make up my mind is related to #223: should we make slots=True the default? I’m quite confident that in 99,9% of cases it's the right thing to do and will guide people to write better classes.

However on the other hand, our approach of rewriting classes breaks in certain scenarios, usually involving metaclasses.

So the question is, whether we want to tolerate a higher rate of bogus bug reports/help requests or make the default case nicer?

I welcome your input.


Finally a controversial idea: we could make import attrs Python 3 only. There isn't much baggage we'd get rid of but there is some and 2020 is less than a year ahead. It would also allow us to embrace enums as part of our API.

Activity

  1. sfermigier commented on Jan 20, 2019

    @sfermigier

    "we could make import attrs Python 3 only." -> +1.

    "It would also allow us to embrace enums as part of our API." -> That would be awesome.

  2. berislavlopac commented on Jan 20, 2019

    @berislavlopac

    I absolutely agree with your point on slots=True, but I would recommend not making it the default. I'm always surprised by how many people have no idea that this amazing feature exists; you need to at the very least explain in the documentation what it is anyway, so it can be used to recommend its use and guide people to write better classes. 🙂

    I'm also in favour of Python3-only import attrs.

    What else is bugging you?

    A question: My main issue with attrs has always been its decorator-based syntax and inability to instantiate using __init__. To be clear, it's my personal preference and not a general opinion on How Things Should Be Done [TM], but I've always liked the subclassing approach found in e.g. schematics. Also, I've often explained the concept of data classes (regardless of the implementation) as "ORM without the R", as it maps nicely with people's experience with Django and others. So I just have to ask, it there a possibility to have an optional method of implementing attrs as a parent/abstract class and/or a mixin?

    Thanks for all the amazing work!

  3. hynek commented on Jan 20, 2019

    @hynek
    MemberAuthor

    So I just have to ask, it there a possibility to have an optional method of implementing attrs as a parent/abstract class and/or a mixin?

    No, but you could probably use metaclasses and our programmatic interface (make_class) to build it yourself?

  4. ncoghlan commented on Jan 20, 2019

    @ncoghlan

    In a world of AWS Lambda and other RAM-second based billing systems, making "slots=True" the default will literally save people money (since it uses less RAM, and avoids pointless dict creation for each instance).

    If it fails, then folks are likely to get noisy exceptions that a few asked-and-answered questions on Stack Overflow will teach them to resolve with "slots=False".

  5. bskinn commented on Jan 20, 2019

    @bskinn

    If it fails, then folks are likely to get noisy exceptions that a few asked-and-answered questions on Stack Overflow will teach them to resolve with "slots=False".

    Could even seed SO with a couple of self-answered Q&A covering common error cases where slots=False is the fix.

  6. hynek commented on Jan 20, 2019

    @hynek
    MemberAuthor

    If it fails, then folks are likely to get noisy exceptions that a few asked-and-answered questions on Stack Overflow will teach them to resolve with "slots=False".

    I tend to agree; it just goes a bit against attrs’s principle of “just adding some stuff” to your classes.

  7. jml commented on Jan 20, 2019

    @jml

    My feelpinion: frozen=True should be the default. Probably the option should be renamed to mutable, so you say mutable=True for when you know what you are doing.

  8. bskinn commented on Jan 20, 2019

    @bskinn

    My feelpinion: frozen=True should be the default. Probably the option should be renamed to mutable, so you say mutable=True for when you know what you are doing.

    Disagree here. frozen=True only works for me in situations where (1) the entire contents of all instances will be known at instantiation-time in all cases, and (2) where no derived/calculated members will be needed (i.e., where @propertys are sufficient for all derived members because the calculations are fast).

    I use attrs classes a lot in my data analysis work, and often I'll have something expensive I need to do with the arguments passed in at initialization. I absolutely want to do that expensive thing exactly once for each instance, and AFAIK I have to do it as something like:

    @attr.s(slots=True)
    class Foo:
    
        input_var = attr.ib()
        expensive_derived_var = attr.ib(init=False)
    
        def __attrs_post_init__(self):
            self.expensive_derived_var = self.calculate_expensive_thing()
    
        def calculate_expensive_thing(self):
            ...
            {expensive stuff with self.input_var}
            ...
    

    With frozen=True, I can't set self.expensive_derived_var in __attrs_post_init__.

    That being said, I have no idea whether my needs here at all represent the majority of use-cases.

  9. hynek commented on Jan 20, 2019

    @hynek
    MemberAuthor

    I love myself some value types but frozen=True ain’t happening. :)

    It just doesn’t fit Python too well. But IIRC we’ve decided on an own function for it?

  10. berislavlopac commented on Jan 20, 2019

    @berislavlopac

    I agree with frozen=False as the default, but kinda like using the term mutable, unless there is a significant difference between the meaning in attrs and Python in general. So I guess I would vote for the mutable=True as default.

    Oh, and I think that @bskinn's argument against frozen=True equally applies to slots=True; I still think they should both be off by default. Unless I'm mistaken, slots applies to the attributes defined via attrs and no additional attributes can't be added, is that right?

  11. berislavlopac commented on Jan 20, 2019

    @berislavlopac

    No, but you could probably use metaclasses and our programmatic interface (make_class) to build it yourself?

    Possibly, will look into it.

  12. hynek commented on Jan 21, 2019

    @hynek
    MemberAuthor

    The reason why it's called frozen is because that's how we call these things in Python (frozenset etc) and I don't want to open a new nomenclature out of purity. I've seen good things fail too often because of the stubbornness of its maintainers to be a good Python citizen.

    And yes, slots=True prevents new attributes and you really shouldn't add ad-hoc attributes so that's a feature. If you need it, you can always say slots=False and the class has a warning attached to itself, that the attribute list is not comprehensive.

  13. jml commented on Jan 21, 2019

    @jml

    we’ve decided on an own function for it?

    Did we? Where can I read about that?

  14. hynek commented on Jan 21, 2019

    @hynek
    MemberAuthor

    Where can I read about that?

    #408 (comment)

    It's gonna be @attrs.define and @attrs.frozen. Depending on https://hynek.me/about/#gratitude I might add an attrs.mutable alias for define. ;)

  15. 33 remaining items

  16. oakkitten commented on Jun 25, 2020

    @oakkitten

    You can simply use partial:

    >>> from functools import partial
    >>>
    >>> auto = partial(attrs, auto_attribs=True, auto_exc=True)
    >>>
    >>> @auto(frozen=True)  # additional parameters
    ... class Foo:
    ...     foo: int
    ...
    >>> @auto               # no parameters and no parentheses
    ... class Bar:
    ...     bar: str
    ...
    >>> Foo?
    Init signature: Foo(foo: int) -> None
      ...
    >>> Bar?
    Init signature: Bar(bar: str) -> None
      ...

    You can even stack these:

    >>> frozen = partial(auto, frozen=True)

    Given how easy and readable and pythonic this is, I think having the least magical defaults is very reasonable. Perhaps with the exception of cache_hash, are there reasons to not enable this by default for frozen instances?

    Mypy is the bigger problem here, as it currently doesn't understand partial, and doesn't even understand auto = attrs; @auto; class ...

    P.S. I personally would like to be able to drop parentheses on attrib like this:

    @attrs
    class Foo:
        foo = attrib
  17. hynek commented on Jun 29, 2020

    @hynek
    MemberAuthor

    Sadly the mypy angle ruined this approach that I used to use all the time. :(

  18. petergaultney commented on Jun 29, 2020

    @petergaultney

    Mypy is the bigger problem here, as it currently doesn't understand partial, and doesn't even understand auto = attrs; @auto; class ...

    This partial application decorator-forwarding usecase can be managed with a micro-plugin for mypy - you basically need something that says:

    import typing as ty
    from mypy.plugin import Plugin, ClassDefContext
    from mypy.plugins.attrs import attrs_class_maker_callback
    
    MODULE_PATH_TO_YOUR_DECORATOR = "your.module.path.autoattrs"
    
    class AutoAttrsDecoratorPlugin(Plugin):
        def get_class_decorator_hook(
            self, fullname: str
        ) -> ty.Optional[ty.Callable[[ClassDefContext], None]]:
            def fwd_auto(cls_def_ctx: ClassDefContext):
                if fullname == MODULE_PATH_TO_YOUR_DECORATOR:
                    attr_class_maker_callback(cls_def_ctx, True)
            if fullname == MODULE_PATH_TO_YOUR_DECORATOR:
                return fwd_auto
            return None
    
    def plugin(_version):
        return AutoAttrsDecoratorPlugin
    

    What this doesn't support is things like frozen - however the mypy attrs plugin itself could be trivially rewritten to expose those parameters directly for these sorts of mypy plugins.

    @hynek do you think mypy would accept a PR to make the built-in plugin more directly reusable? This whole mess of plugin code could probably be rewritten to be a one-liner that 'registers' a decorator with the mypy plugin.

  19. hynek commented on Jun 29, 2020

    @hynek
    MemberAuthor

    @hynek do you think mypy would accept a PR to make the built-in plugin more directly reusable? This whole mess of plugin code could probably be rewritten to be a one-liner that 'registers' a decorator with the mypy plugin.

    I've opened an issue asking for this almost two years ago: python/mypy#5406

    FWIW, #650 is of interest here too.

  20. oakkitten commented on Jun 30, 2020

    @oakkitten

    doesn't even understand auto = attrs;

    by the way, while assignment doesn't make mypy happy, importing under a different name does:

    if TYPE_CHECKING:
        from attr import attrs as slotted
    else:
        slotted = functools.partial(attrs, slots=True)
  21. bionicles commented on Jul 19, 2020

    @bionicles

    one thing which breaks some functional programming stuff, is the fact you can't do bracketed lookup with attrs objects

    a = {"name": "a"} 
    a["name"] 
    # a
    
    @attrs
    class Person:
        name: str = attrib()
    
    a = Person(name="a")
    a["name"]
    # TypeError: 'Person' object is not subscriptable
    

    means you can't do functional lenses, lookPath, assocPath, dissocPath, evolvePath, are all legit, makes me miss Ramda!
    how can we make attrs better for functional programming?

  22. berislavlopac commented on Jul 19, 2020

    @berislavlopac

    You can always implement your own __getitem__...

  23. bionicles commented on Jul 19, 2020

    @bionicles
        def __getitem__(self, key):
            return self.__getattribute__(key)
    

    worked like a charm, thanks for reminding me, that would be a fun default to add!

  24. hynek commented on Aug 14, 2020

    @hynek
    MemberAuthor

    Please check out #666 y'all.

  25. unpinned this issue on Aug 20, 2020
  26. hynek commented on May 5, 2021

    @hynek
    MemberAuthor

    So FTR, NG APIs will ship as stable in 21.1, now is the last chance to ask for changes in APIs that are annoying.

    So far there's #705 but I'm sure there might be more?

    To be clear: define/mutable/frozen/field are set in stone now. But there's a buttload of other APIs that may benefit from polish?

  27. pinned this issue on May 6, 2021
  28. botant commented on May 6, 2021

    @botant
    Contributor

    I just want to say thanks for order=False by default. 🙏

  29. unpinned this issue on Dec 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions