Arguments in Depth
Python gives you fine control over how functions accept arguments: by position or by name, with defaults, with any number of extras via *args and **kwargs, and with markers that force some arguments to be named. This lesson also covers the most famous Python gotcha — mutable default values.
Positional and keyword arguments
def book_flight(origin, destination, passengers=1): return f"{passengers} × {origin} → {destination}" print(book_flight("LIS", "OPO")) print(book_flight("LIS", "OPO", 2)) print(book_flight(destination="NYC", origin="LIS")) print(book_flight("LIS", passengers=3, destination="PAR"))
1 × LIS → OPO 2 × LIS → OPO 1 × LIS → NYC 3 × LIS → PAR
Positional arguments are matched by order; keyword arguments by name, in any order. Once you use a keyword argument in a call, everything after it must be a keyword argument too.
*args: any number of positional arguments
def average(*numbers): if not numbers: return 0 return sum(numbers) / len(numbers) print(average(4, 8, 15)) print(average()) values = [1, 2, 3, 4] print(average(*values))
9.0 0 2.5
A parameter starting with * collects extra positional arguments into a tuple. At the call site, *values does the opposite, spreading a list into separate arguments.
**kwargs: any number of keyword arguments
def make_tag(tag, text, **attributes): attrs = "".join(f' {name}="{value}"' for name, value in attributes.items()) return f"<{tag}{attrs}>{text}</{tag}>" print(make_tag("a", "Home", href="/", title="Go home")) print(make_tag("p", "Hello")) options = {"id": "intro", "lang": "en"} print(make_tag("p", "Hi", **options))
<a href="/" title="Go home">Home</a> <p>Hello</p> <p id="intro" lang="en">Hi</p>
**attributes collects extra keyword arguments into a dictionary. The names args and kwargs are only conventions — the stars are what matter.
Keyword-only arguments
Parameters after a bare * (or after *args) must be passed by name. This makes calls with boolean flags readable:
def connect(host, port=5432, *, timeout=10, ssl=True): return f"{host}:{port} timeout={timeout} ssl={ssl}" print(connect("db.local", timeout=30)) print(connect("db.local", 5432, 30))
db.local:5432 timeout=30 ssl=True
Traceback (most recent call last):
File "main.py", line 5, in <module>
print(connect("db.local", 5432, 30))
~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
TypeError: connect() takes from 1 to 2 positional arguments but 3 were givenWithout the *, a call like connect("db.local", 5432, 30, False) would be legal but impossible to understand at a glance.
Positional-only arguments
Parameters before a / can only be passed by position. Library authors use this so they can rename parameters later without breaking callers:
def distance(x1, y1, x2, y2, /): return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 print(distance(0, 0, 3, 4))
5.0
Putting it all together, a signature can have up to five kinds of parameters: def f(pos_only, /, normal, *args, kw_only, **kwargs).
The mutable default trap
Default values are evaluated once, when the function is defined — not each time it's called. With a mutable default like a list, every call shares the same object:
def add_item_buggy(item, basket=[]): basket.append(item) return basket print(add_item_buggy("apple")) print(add_item_buggy("pear")) def add_item(item, basket=None): if basket is None: basket = [] basket.append(item) return basket print(add_item("apple")) print(add_item("pear"))
['apple'] ['apple', 'pear'] ['apple'] ['pear']
The fix is the standard idiom: default to None and create the new list inside the function. Never use a list, dict or set as a default value.
Arguments are passed by reference to objects
A function receives the same object the caller passed. Reassigning the parameter has no effect outside, but mutating the object does:
def reassign(numbers): numbers = [0] def mutate(numbers): numbers.append(99) data = [1, 2] reassign(data) print(data) mutate(data) print(data)
[1, 2] [1, 2, 99]
Functions that quietly change their arguments surprise callers. Prefer returning a new object, or make the mutation obvious from the function's name (add_to_cart(cart, item)).
Forwarding arguments
*args and **kwargs together let a wrapper pass everything through to another function — the basis of decorators (lesson 34):
def logged(fn, *args, **kwargs): print(f"calling {fn.__name__} with {args} {kwargs}") return fn(*args, **kwargs) print(logged(round, 3.14159, ndigits=2))
calling round with (3.14159,) {'ndigits': 2}
3.14