Function Definition with Catch-All Parameters

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.

Positional Catch-All 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'
>>>

Keyword Catch-All Parameters

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'

Conclusion

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!

Ready to Learn More Web Development?

Join my Mailing List to receive 1-2 useful Articles per week.


I send two weekly emails on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


Function Definition with Position-Only Parameters

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

Function Definition with Simple Parameters

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

Function Definition with Default Parameters

Learn about Python functions with default parameters. Understand how default parameters work and some essential restrictions and evaluation rules.

By Christoph Schiessl on Python

Christoph Schiessl

Christoph Schiessl

Independent Consultant + Full Stack Developer


If you hire me, you can rely on more than a decade of experience, which I have collected working on web applications for many clients across multiple industries. My involvement usually focuses on hands-on development work using various technologies like Python, JavaScript, PostgreSQL, or whichever technology we determine to be the best tool for the job. Furthermore, you can also depend on me in an advisory capacity to make educated technological choices for your backend and frontend teams. Lastly, I can help you transition to or improve your agile development processes.