7

I was going trough some code, and I came across the following function:

def foo(self, arg1=1, *, arg2=2):
    pass

I was surprised to see keyword arguments on the left side of the *, the positional arguments. I notice then that I can call foo in both of the following ways:

>>> foo(1)
>>> foo(arg1=1)

I think I would be expecting the second call to fail as I am calling foo using a named argument by providing the keyword arg1.

With that said, am I using positional arguments in both scenarios, or is the second call to foo a named argument?

5
  • the second call is a named argument. Commented Jul 8, 2019 at 17:12
  • 3
    Relevant info here: stackoverflow.com/a/14302007/3589609 Commented Jul 8, 2019 at 17:12
  • @JoshuaSchlichting I saw that topic but it does not answer the question Commented Jul 8, 2019 at 17:14
  • @RenatoDamas I agree, that's why it's just here in the comments. Thought it might be useful, as it helped me get a better understanding. ¯\_(ツ)_/¯ Commented Jul 8, 2019 at 17:20
  • 1
    @JoshuaSchlichting then I give you a thumbs up ! ;) Commented Jul 8, 2019 at 17:22

4 Answers 4

15

The best sentence that I found that best describes this is:

"The trick here is to realize that a “keyword argument” is a concept of the call site, not the declaration. But a “keyword only argument” is a concept of the declaration, not the call site."

Below is a concise explanation copied from here in case the link dies at some point.

def bar(a,    # <- this parameter is a normal python parameter
        b=1,  # <- this is a parameter with a default value
        *,    # <- all parameters after this are keyword only
        c=2,  # <- keyword only argument with default value
        d):   # <- keyword only argument without default value
    pass
Sign up to request clarification or add additional context in comments.

Comments

8

The arg1 argument is allowed to be called as either a positional argument, or a keyword argument.

As of Python 3.8, it is possible to specify some arguments as positional only. See PEP 570. Prior to 3.8, this isn't possible unless you write a python C extension.

The 3.8 syntax looks like this (directly from the PEP):

def name(positional_only_parameters, /, positional_or_keyword_parameters,
         *, keyword_only_parameters): ...

...prior to 3.8, the only legal syntax is this:

def name(positional_or_keyword_parameters, *, keyword_only_parameters): ...

Comments

1

Despite the similarity in syntax, there is no relationship between keyword arguments and parameters with default values.

key=value is used both to assign a default value to a parameter when the function is defined, and to pass an argument for a parameter when the function is called.

A parameter without a default value can be assigned using a keyword argument:

def foo(a):
    return a + 3

assert foo(a=5) == 8

A parameter with a default value can be assigned without a keyword argument:

def foo(a=5):
    return a + 3

assert foo() == 8
assert foo(1) == 4

Comments

0

Is a complex answer by using the * like the argument ( the Python version, define and use it ).

In your case:

>>> foo(1,2,)
>>> foo(1,2)

Note that: Try to use this and see the difference ( without self ):

>>> def foo( arg1=1, *, arg2=2):
...     pass
...
>>> foo(arg1=1,arg2=2)
>>> def foo( arg1=1, *, arg2=2):
...     pass
...
>>> foo(arg1=1,arg2=2)
>>> foo(arg1=1)
>>> foo(arg1=1,)
>>> foo(arg2=2)
>>> foo(arg2=2,1)
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
>>> foo(2,1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes from 0 to 1 positional arguments but 2 were given
>>> foo(arg1=1,2)
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
  • One star * defines positional arguments (you can receive any number of arguments and treat the passed arguments as a tuple);
  • Two stars ** define keywords arguments.

A good example comes from this tutorial (the keyword-only arguments without positional arguments part):

def with_previous(iterable, *, fillvalue=None):
    """Yield each iterable item along with the item before it."""
    previous = fillvalue
    for item in iterable:
        yield previous, item
        previous = item

>>> list(with_previous([2, 1, 3], fillvalue=0))
[(0, 2), (2, 1), (1, 3)]

That its value could be sent as positional arguments.

See this tutorial for more info about arguments.

Python 3.x introduces more intuitive keyword-only arguments with PEP-3102 (you can specify * in the argument list).

One good alternative answer is this:

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.