Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for keyword only arguments #106

Closed
aragilar opened this issue Oct 27, 2016 · 17 comments · Fixed by #411
Closed

Add support for keyword only arguments #106

aragilar opened this issue Oct 27, 2016 · 17 comments · Fixed by #411
Labels

Comments

@aragilar
Copy link

There was a suggestion in #38 that there be an option to make arguments keyword only. The suggested API was to have something like:

    @attr.s
    class A:
        a = attr.ib()
        b = attr.ib(init='kwonly')

        # __init__ signature is:
        # def __init__(self, a, *, b):
        #    pass

Would support for this be accepted? What about

    @attr.s(kwonly=True)
    class A:
        a = attr.ib()
        b = attr.ib()

        # __init__ signature is:
        # def __init__(self, *, a, b):
        #    pass

which may be nicer when subclassing?

@exarkun
Copy link

exarkun commented Nov 23, 2016

This functionality would be nice for the "big bag of configuration" use case. I don't really want to be able to pass my 19 different configuration items positionally. It would be totally unreadable.

Currently I have to make sure to carefully order my attribute definitions to avoid running into limitations about what can have a default and what cannot. If everything were keyword-only, this would go away.

This use-case would be better served by an API like the latter suggested above (so the setting doesn't have to be repeated 19 times). The existence of both of the proposed APIs wouldn't hurt this use case, though.

@Insoleet
Copy link
Contributor

I can already use keywords arguments to build attrs objects. Why do you need a parameter to enforce it ? (i/e : disable positionnal arguments)

@Tinche
Copy link
Member

Tinche commented Nov 23, 2016

Kwonly attributes would have to have defaults, no? How does that work with with the class decorator approach?

Currently I have to make sure to carefully order my attribute definitions to avoid running into limitations about what can have a default and what cannot. If everything were keyword-only, this would go away.

If kwonly attributes must have defaults, this wouldn't solve anything for you. You can set defaults today on everything and not care about ordering attributes.

I don't really want to be able to pass my 19 different configuration items positionally. It would be totally unreadable.

Not sure what you mean here, could you clarify? You'd like to disable positional arguments on your __init__ altogether? I mean, that's fine, but you don't actually have to use this feature.

I thought you might be disagreeing with having to carefully position arguments to __init__, but that's not really the case currently, or C(**attr.asdict(C())) wouldn't work.

@exarkun
Copy link

exarkun commented Nov 23, 2016

I want to be able to write this:

    @attr.s
    class A(object):
        a = attr.ib(default="abc")
        b = attr.ib()

This is not currently possible because:

ValueError: No mandatory attributes allowed after an attribute with a default value or factory.  Attribute in question: Attribute(name='b', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None)

As a work-around, I can re-arrange the attributes on my class every time I add or remove a default - but I'd much rather just say something like @attr.s(positional=False) and not worry about the order.

@aragilar
Copy link
Author

@Tinche No, keyword only is different to having a default.
Default argument:

def a(a=1):
    pass

is different to keyword only with default:

def a(*, a=1):
    pass

is different to keyword only

def a(*, a):
    pass

My original question was what should the API be?

@Tinche
Copy link
Member

Tinche commented Nov 24, 2016

Huh, guess I learned something today. None of the examples in the PEP 3102 show a kw-only argument without a default, but the text mentions it (I just skimmed the text :).

Anyway, continuing the discussion. Currently the definition order of attributes is the same as the order of the attributes in the generated __init__. This is strictly by @hynek's executive order and I personally agree with the rationale. If it wasn't intentional we would just sort the attributes and you wouldn't need to put arguments with defaults last. The order of definition is used in other places too: the __repr__, comparison methods, attr.fields.

Keeping with this design decision, if your attributes were a mix of normal and kwonly attributes, you'd still need to define your kwonly attributes after the normal ones.

@attr.s
class A:
    a = attr.ib()
    b = attr.ib(init='kwonly')

The idea all attributes could be marked as kwonly is also being floated around:

@attr.s(init='kwonly')
class A:
    a = attr.ib(default=0)
    b = attr.ib()

The attribute-level kwonly needs support from attrs core. The class level could be handled by an additional decorator (that you can write yourself):

@attr.s
@kwonly
class A:
    a = attr.ib(default=0)
    b = attr.ib()

I think this is where we are with the proposals, currently.

@hynek
Copy link
Member

hynek commented Nov 25, 2016

First of all: as long as I breathe, there won’t be any strings part of any API. :)

Secondly, I guess we could implement kw-only so mixing is possible by moving default values into __init__ as it used to be in characteristic and simply set all attributes to NOTHING. Adding a *, on Py3 is more of a nifty goodie.

Precondition is that it must not have any performance implications for common use cases.

Thirdly, I’m totally not implementing it. :)

@hynek hynek added the Feature label Nov 25, 2016
@Tinche
Copy link
Member

Tinche commented Nov 27, 2016

@hynek

from enum import Enum

class InitType(Enum):
    PRESENT = True
    ABSENT = False
    KWONLY = "kwonly"

attr.ib(init=True) === attr.ib(init=InitType.PRESENT)
attr.ib(init='kwonly') === attr.ib(init=InitType.KWONLY)

It's backward compatible, and supports not using the enum when you don't feel like it (i.e. the REPL or using timeit).

Secondly, I guess we could implement kw-only so mixing is possible by moving default values into init as it used to be in characteristic and simply set all attributes to NOTHING. Adding a *, on Py3 is more of a nifty goodie.

Is this a response to the issue of having to order the attributes so attributes with defaults are last? I think a separate issue should be created for commenting on this, since it's almost orthogonal to kwonly arguments. (We could implement kwonly args without touching the rule, or the rule could be changed without implementing kwonly args at all, and kwonly args would be useful even if we change the rule.)

@hynek
Copy link
Member

hynek commented Nov 28, 2016

I feel like kwonly is a red herring. I don’t see any reason to add any complexity just so people can’t pass argument by order.

OTOH being able to mix default with non-defaults is an actual use case that might be mitigated by a kwonly design. But focusing on the former seems backward to me.

@toejough
Copy link

toejough commented Jul 16, 2017

I've used kw-only arguments pretty regularly since their addition. My main reasons for doing so:

  • raising the difficulty of calling code incorrectly (it's not impossible to say Foo(should_be_bar=quux, should_be_quux=bar), but it's more obvious in coding and review that that's wrong than if you're allowed to write Foo(quux, bar))
  • faster failure in error cases (Foo(quux, bar) fails on call with a nice error, rather than having some logic/type error occur several calls later). (yes, Foo(bar, quux) is 'correct' and also fails with kw-only args, but the gains from fast failure and obvious code outweigh this, IMO)
  • readability/explicitness. yep, it's more to type, but it's also faster to comprehend. You don't need kw-only args to init classes with keywords, but if using keywords for code clarity is important to the project, you want some kind of enforcement. You could try to enforce that when doing code review or with static analysis, but those are never going to be as reliable as language-level support.

You don't have to agree, of course, but since nobody else responded after you said you don't see any reason, I thought I'd list the ones I find compelling in the projects/teams I've been working on.

There's also the official python reason for including them in the language, which I also appreciate from time to time for functions, and actually isn't about preventing people from passing arguments by order - it's about allowing people to specify arguments by keyword after using varargs:

The current Python function-calling paradigm allows arguments to be specified either by position or by keyword. An argument can be filled in either explicitly by name, or implicitly by position.

There are often cases where it is desirable for a function to take a variable number of arguments. The Python language supports this using the 'varargs' syntax ( *name ), which specifies that any 'left over' arguments be passed into the varargs parameter as a tuple.

One limitation on this is that currently, all of the regular argument slots must be filled before the vararg slot can be.

This is not always desirable. One can easily envision a function which takes a variable number of arguments, but also takes one or more 'options' in the form of keyword arguments. Currently, the only way to do this is to define both a varargs argument, and a 'keywords' argument ( **kwargs ), and then manually extract the desired keywords from the dictionary.

While I appreciate that use case from time to time for functions... I don't know that I've ever actually wanted to do keywords after varargs in a class init, but ¯\_(ツ)_/¯.

@hynek
Copy link
Member

hynek commented Jul 17, 2017

FWIW there’s two levels to it:

  1. are we gonna allow the user to make certain args kwonly and others not? I find this introduces too much complexity for too little gain and we’d regret it later.
  2. @attr.s(init_kwonly=True) that only works on Python 3 by adding a * after self is something I could get behind. Although that leads to weirdness if you want to write cross-platform code. So we’d have to change the signature to __init__(self, **kw) and extract the arguments within init which is also kind of tedious. :| I might merge a good PR for this one but I’m not gonna write it.

@wbolster
Copy link
Member

fwiw, a trick for py2 (who uses that anyway nowadays?) is to add a _please_use_kwargs=None arg as the first arg after self. i have seen this in the wild but cannot recall where.

@wbolster
Copy link
Member

while one might think the extra "marker arg" is ugly, it actually gives much better info in help() output (and sphinx autoclass docs) than **kwargs does.

@hynek
Copy link
Member

hynek commented Jul 17, 2017

I think I could live with such an hack on Python 2.

@wsanchez
Copy link

wsanchez commented Aug 2, 2017

I'm in favor of the class-wide @attr.s(kwonly=True) option for reasons similar to @exarkun. I don't want reordering of attributes to be an API-incompatible change, but I have no good way to prevent called for using them positionally.

The per-attribute constraint seems like it might add more complexity than it's worth, but that may be simply because it's not a use case I have at the moment… perhaps as a compatibility thing where you have an existing class and you want all new attributes to be kwarg-only?

But I'd love to see the class-level constraint.

@bskinn
Copy link

bskinn commented Sep 26, 2017

I'd love to see this, too. And, while the granularity of the per-attr.ib() kwonly specification would be nice to have, the class-wide option would still be tremendously useful.

malinoff added a commit to malinoff/attrs that referenced this issue Nov 1, 2017
malinoff added a commit to malinoff/attrs that referenced this issue Nov 1, 2017
malinoff added a commit to malinoff/attrs that referenced this issue Nov 1, 2017
malinoff added a commit to malinoff/attrs that referenced this issue Nov 1, 2017
malinoff added a commit to malinoff/attrs that referenced this issue Nov 1, 2017
malinoff added a commit to malinoff/attrs that referenced this issue Nov 1, 2017
malinoff added a commit to malinoff/attrs that referenced this issue Nov 1, 2017
malinoff added a commit to malinoff/attrs that referenced this issue Nov 1, 2017
malinoff added a commit to malinoff/attrs that referenced this issue Nov 2, 2017
@akhilman
Copy link

One more use case for keyword-only arguments is subclassing:

For example I have two classess:

@attr.s
class A:
    required_a = attr.ib(None)
    optional = attr.ib(None)

@attr.s
class B(A):
    required_b = attr.ib(None)

With kw-only I can mark the optional attribute of the A class as kw-only and pass required attributes as positional arguments: B(required_a, required_b)

asford added a commit to uw-ipd/attrs that referenced this issue Jul 24, 2018
hynek pushed a commit that referenced this issue Aug 11, 2018
* Added support for keyword-only attributes. Closes #106, and closes #38

(Rebases #281)

Co-authored-by: Alex Ford <fordas@uw.edu>

* Add `attr.s`-level `kw_only` flag.

Add `kw_only` flag to `attr.s` decorator, indicating that all class
attributes should be keyword-only in __init__.

Minor updates to internal interface of `Attribute` to support
evolution of attributes to `kw_only` in class factory.

Expand examples with `attr.s` level kw_only.

* Add `kw_only` to type stubs.

* Update changelog for rebased PR.

Hear ye, hear ye. A duplicate PR is born.

* Tidy docs from review.

* Tidy code from review.

* Add explicit tests of PY2 kw_only SyntaxError behavior.

* Add `PythonToOldError`, raise for kw_only on PY2.

* `Attribute._evolve` to `Attribute._assoc`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants