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!

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast Web Applications.


I'm available for hire as a freelance web developer, so you can take advantage of my more than a decade of experience working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm an avid writer and educator who takes pride in breaking technical concepts down into the simplest possible terms.

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 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

Function Overloading with the @overload Decorator

Python doesn't support true function overloading, but can fake it using the typing module's @overload decorator.

By Christoph Schiessl on Python

Web App Reverse Checklist

Ready to Build Your Next Web App?

Get my Web App Reverse Checklist first ...


Software Engineering is often driven by fashion, but swimming with the current is rarely the best choice. In addition to knowing what to do, it's equally important to know what not to do. And this is precisely what my free Web App Reverse Checklist will help you with.

Subscribe below to get your free copy of my Reverse Checklist delivered to your inbox. Afterward, you can expect one weekly email on building resilient Web Applications using Python, JavaScript, and PostgreSQL.

By the way, it goes without saying that I'm not sharing your email address with anyone, and you're free to unsubscribe at any time. No spam. No commitments. No questions asked.