2

I'm trying to wrap my head around using args and kwargs in Python 3 (Python 3.7.0) but I'm running into some issues with my understanding.

Here is a simple function I have:

def add_args(y=10, *args, **kwargs):
    return y, args, kwargs

And test it to see what is returned:

    print(add_args(1, 5, 10, 20, 50))
    print(add_args())
    >>
    (1, (5, 10, 20, 50), {}) # What happened to my default y=10?
    (10, (), {})

What I don't understand is, what happened to y=10 in the first print statement? I can see it is being overridden by the 1 in args but I'm unsure why.

How can I rewrite this function so the default value is not overridden, or am I missing something with how the parameters are passed from the function signature to the return statement?

I tried looking here and here but did not find the answers I was looking for. As I thought putting the default values before the args and kwargs would prevent the overwriting.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Chef1075
  • 2,614
  • 9
  • 40
  • 57
  • 2
    If you don't want it to be overridable by the caller, why is it a parameter at all? Why not just do `y=10` as the first line fo the body? – abarnert Sep 11 '18 at 21:22
  • See my answer below, the accepted answer on the first link you checked does document this correctly: place `y=10` **after** the `*args` catch-all var-positional parameter. – Martijn Pieters Sep 11 '18 at 21:22

2 Answers2

6

*args only captures any positional arguments not otherwise defined; y=10 does not mean y can't be used as a positional argument. So y is assigned the first positional argument.

You can prevent y being used as a positional argument by making it a keyword-only argument. You do this by placing the argument after the *args var-positional catch-all parameter, or if you don't have a *name parameter, after a * single asterisk:

def add_args(*args, y=10, **kwargs):
    return y, args, kwargs

or

def keyword_only_args(*, y=10, **kwargs):
    return y, kwargs

Now y won't capture positional arguments any more:

>>> def add_args(*args, y=10, **kwargs):
...     return y, args, kwargs
...
>>> add_args(1, 5, 10, 20, 50)
(10, (1, 5, 10, 20, 50), {})          # y is still 10
>>> add_args(1, 5, 10, 20, 50, y=42)  # setting y explicitly 
(42, (1, 5, 10, 20, 50), {})

You don't have to have a **kwargs keyword catch-all either:

def add_args(*args, y=10):
    return y, args

but if it is present, it needs to be listed last.

Keyword-only arguments do not have to have a default value, the =10 can be omitted, but then the parameter becomes mandatory, and can only be specified in a call by using y=value:

>>> def add_args(*args, y):  # mandatory keyword-only argument
...     return y, args
...
>>> add_args(1, 5, 10, 20, 50)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: add_args() missing 1 required keyword-only argument: 'y'
>>> add_args(1, 5, 10, 20, 50, y=42)
(42, (1, 5, 10, 20, 50))
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
2

Hmm, y is the first positional argument in your function definition, so naturally it binds to the first actual positional argument at callsite, which is 1.

bipll
  • 11,747
  • 1
  • 18
  • 32