by Christoph Schiessl on Python
I have previously written several articles about various aspects of function definitions in Python. There's still much more to say about this topic, so today, I want to continue the series by introducing catch-all parameters. As before, we have to distinguish between positional and keyword parameters.
If you have read my previous articles, you may recall that you can add a *
to force keyword notation for all parameters that come after the *
in your function's parameter list.
def foo(a, *, b):
pass
In the example above, the parameter a
can be provided using positional or keyword notation. But, due to the *
in the parameter list, b
must be provided using keyword notation. What's new is that you can optionally promote the *
to a catch-all parameter by appending an identifier. It's not enforced, but the convention is to use the identifier args
. You can also see this in the grammar in the line defining parameter_list_starargs
.
funcdef ::= [decorators] "def" funcname [type_params] "(" [parameter_list] ")"
["->" expression] ":" suite
decorators ::= decorator+
decorator ::= "@" assignment_expression NEWLINE
parameter_list ::= defparameter ("," defparameter)* "," "/" ["," [parameter_list_no_posonly]]
| parameter_list_no_posonly
parameter_list_no_posonly ::= defparameter ("," defparameter)* ["," [parameter_list_starargs]]
| parameter_list_starargs
parameter_list_starargs ::= "*" [parameter] ("," defparameter)* ["," ["**" parameter [","]]]
| "**" parameter [","]
parameter ::= identifier [":" expression]
defparameter ::= parameter ["=" expression]
funcname ::= identifier
So, to summarize, you can define a function as follows:
def bar(a, *args, b):
pass
As before, the parameter a
can still be provided using positional or keyword notation. Also, due to the *
in the parameter list, b
must still be provided using keyword notation. However, if the caller provides excess positional parameters (in addition to a
), they are made available as args
inside the function. Semantically, args
will be tuple
, containing the extra positional parameters in the order the caller provided them.
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo(a, *args, b):
... print(a, args, b)
...
>>> foo(1, b=2) # empty tuple because there are no excess params
1 () 2
>>> foo(a=1, b=2) # `a` can still be provided using keyword notation
1 () 2
>>> foo(1, 2, b=3) # single excess params is available in the tuple
1 (2,) 3
>>> foo(1, 2, 3, b=4) # two excess params are available in the tuple
1 (2, 3) 4
>>> foo(2, 3, a=1, b=4) # excess params prevent keyword notation for `a`
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'a'
>>>
If you look again at the grammar and focus on the line defining parameter_list_starargs
, you'll see that there's also a **
notation. In this case, the **
must be followed by an identifier, and using the identifier kwargs
is customary for this purpose. So, you now know how to define a function with a catch-all parameter that captures excess keyword parameters and makes them available inside your function.
def foo(a, **kwargs):
pass
Semantically, kwargs
is a dictionary (i.e., an instance of dict
) with str
keys, which means the following code is correct (in the sense that it does not raise an AssertionError
exception).
def bar(**kwargs):
assert len(kwargs) == 1
assert kwargs["this_is_a_string"] == 123
bar(this_is_a_string=123)
Ever since Python 3.7, dictionaries preserve the order in which keys/values have been inserted.
Changed in version 3.7: Dictionary order is guaranteed to be insertion order. This behavior was an implementation detail of CPython from 3.6.
Therefore, in this context, function calls like foo(a=1, b=2)
and foo(b=2, a=1)
must be considered different because the value of the function's kwargs
parameter is different in both cases.
That's enough theory for now. Let's try it out:
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo(a, *, b, **kwargs):
... print(a, b, kwargs)
...
>>> foo(1, b=2) # empty dict because there are no excess params
1 2 {}
>>> foo(a=1, b=2, c=3, d=4) # excess params are available in a dict
1 2 {'c': 3, 'd': 4}
>>> foo(1, b=2, d=4, c=3) # order of excess params is retained
1 2 {'d': 4, 'c': 3}
>>> foo(1, b=2, a=3) # providing the same params twice is never allowed
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'a'
On a personal note, I have to say that I don't recommend catch-all parameters unless you have a very good reason for them. The problem with catch-all parameters is that they obscure your functions' API — it's no longer possible to tell from the outside what your functions need to work properly. Anyway, that's everything for today. Thank you for reading, and see you soon!
I send two weekly emails on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.
Learn about positional parameters in Python and a special syntax that allows functions to declare certain parameters as position-only.
By Christoph Schiessl on Python
Learn about functions with simple parameters in Python, including how the called can decide to use positional or keyword notation.
By Christoph Schiessl on Python
Learn about Python functions with default parameters. Understand how default parameters work and some essential restrictions and evaluation rules.
By Christoph Schiessl on Python