Fundamentals · Lesson 18 of 94

Variable arguments and unpacking

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

Concept

Some functions cannot know in advance how many values they will get. print takes one, three, or twenty. max takes a whole list or several separate values. Python supports this with a star in the parameter list: def total(*numbers): collects every positional argument into a tuple called numbers. The name args is only a convention; the star is what matters.

Variable arguments and unpacking: total(3, 5, 8) → numbers = (3, 5, 8) → total() → numbers = ()
Variable arguments and unpacking · Figure 1 · Open for full size

Two stars do the same for keyword arguments: def show(**options): collects show(color="red", size=3) into a dictionary {"color": "red", "size": 3}. A function can have ordinary parameters, then *args, then **kwargs, in that order. This is the shape of every wrapper function you will read in libraries, which is why it is worth knowing early even though you will write it rarely.

Variable arguments and unpacking: show(a=1, b=2) → options = {'a': 1, 'b': 2}
Variable arguments and unpacking · Figure 2 · Open for full size

A bare * in a parameter list, with no name, marks everything after it as keyword-only. def connect(host, *, timeout=30, retries=3) means callers must write timeout=10, never a stray 10 whose meaning you would have to look up. A / marks everything before it as positional-only, which library authors use to keep parameter names private. You will use * in your own code; / you will mostly read.

Signature Valid call Invalid call
def f(*args) f(1, 2, 3) f(a=1)
def f(**kwargs) f(a=1, b=2) f(1)
def f(host, *, timeout=30) f("db", timeout=5) f("db", 5)
def f(x, /, y) f(1, 2) or f(1, y=2) f(x=1, y=2)

The same stars work in the other direction, at the call site. total(*[3, 5, 8]) spreads a list into separate arguments, and line(**{"name": "tea", "price": 2}) spreads a dictionary into keyword arguments. This is how you forward a bundle of settings from one function to another, and how you turn data that arrived as a list into a call that wanted separate values.

Variable arguments and unpacking: values = [3, 5, 8 → total(*values) → total(3, 5, 8) → opts = {'color': 'red'} → draw(**opts) → draw(color='red')
Variable arguments and unpacking · Figure 3 · Open for full size

Code

Collecting positional arguments. numbers is an ordinary tuple inside the function, so sum and len work on it, and calling with nothing gives an empty tuple.

# star_args.py
def total(*numbers):
    print("got", numbers)
    return sum(numbers)

print(total(3, 5, 8))
print(total())
print(total(1.5, 2.5))
Output
got (3, 5, 8)
16
got ()
0
got (1.5, 2.5)
4.0

Collecting keyword arguments, and the full order of parameter kinds in one signature.

# star_kwargs.py
def describe(title, *tags, **options):
    print("title:", title)
    print("tags:", tags)
    print("options:", options)

describe("Python course", "beginner", "video", level=1, free=True)
Output
title: Python course
tags: ('beginner', 'video')
options: {'level': 1, 'free': True}

Keyword-only parameters. The first call is clear at a glance; the second is refused, which is exactly the protection you wanted.

# keyword_only.py
def connect(host, *, timeout=30, retries=3):
    return f"{host} timeout={timeout} retries={retries}"

print(connect("db.local", timeout=5))
print(connect("db.local", 5))
Output
db.local timeout=5 retries=3
Traceback (most recent call last):
  File "keyword_only.py", line 5, in <module>
    print(connect("db.local", 5))
          ~~~~~~~^^^^^^^^^^^^^^^
TypeError: connect() takes 1 positional argument but 2 were given

Unpacking at the call site. A list becomes positional arguments and a dictionary becomes keyword arguments, so settings can travel as data and land as a normal call.

# unpack_call.py
def line(name, price, currency="$"):
    return f"{name:<8}{currency}{price:>6.2f}"

row = ["tea", 2.0]
settings = {"currency": "€"}

print(line(*row))
print(line(*row, **settings))
print(max(*[4, 9, 2]))
Output
tea     $  2.00
tea     €  2.00
9

Try it yourself

Create logger.py with a function log(level, *messages, sep=" ", **context) that prints the level in upper case, the messages joined with sep, and then each context key and value on its own indented line. Call it as log("info", "server", "started", port=8000, debug=False). Then store a message list and a context dictionary in variables and call log by unpacking both.

Common mistakes

What's next

Functions can now take any shape of input. Next you will let a function call itself, and learn when that is the clearest solution and when a loop is better.

Course overview