Function Arguments
In the last lesson, every function took a fixed number of arguments in a fixed order. Python gives you much more flexibility: optional arguments with defaults, arguments passed by name, and functions that accept any number of values.
Default values
Give a parameter a default value with =. If the caller leaves that argument out, the default is used:
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Ada")
greet("Ada", "Good morning")The first call passes only one argument, so name is "Ada" and greeting falls back to its default, "Hello". The second call passes both, and "Good morning" replaces the default for that call only. The default is back for the next call.
A parameter with a default is optional; one without a default is required, and leaving it out is a TypeError. Parameters with defaults must come after the ones without, otherwise Python couldn't tell which argument fills which parameter. This is a SyntaxError:
def greet(greeting="Hello", name):
print(f"{greeting}, {name}!")You've already used defaults: print() has sep=" " and end="\n", and round() has ndigits=None.
A trap: mutable defaults
Never use a list or dictionary as a default value. The default is created once, when the function is defined, and then shared by every call:
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("apple"))
# surprise: the apple is still there!
print(add_item("pear"))The second call prints ['apple', 'pear']. Both calls used the same list object as their default, so the first call's append is still in it. Numbers, strings, True/False and None can't be changed in place, so they're always safe as defaults.
Use None as the default and create a fresh list inside the function instead:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("apple"))
print(add_item("pear"))Now basket = [] runs inside the function, so each call that doesn't pass a basket gets a brand-new empty list.
Keyword arguments
You can pass arguments by name. Then the order doesn't matter, and the call explains itself:
def book_flight(origin, destination, passengers):
print(f"{passengers} passenger(s): {origin} -> {destination}")
book_flight("London", "Paris", 2)
book_flight(destination="Rome", passengers=1, origin="Berlin")
book_flight("Madrid", passengers=3, destination="Lisbon")| Call | How the values are matched |
|---|---|
book_flight("London", "Paris", 2) |
by position: 1st, 2nd, 3rd |
book_flight(destination="Rome", passengers=1, origin="Berlin") |
by name, in any order |
book_flight("Madrid", passengers=3, destination="Lisbon") |
first by position, the rest by name |
The names must match the parameter names exactly: book_flight(start="Oslo", ...) is a TypeError because there's no start parameter. Giving the same parameter twice, once by position and once by name, is also a TypeError.
Positional arguments must come before keyword arguments in a call:
def book_flight(origin, destination, passengers):
print(origin, destination, passengers)
book_flight(origin="Oslo", "Paris", 2)Keyword arguments really shine with several optional settings. You set only the ones you need:
def make_coffee(size="medium", milk=False, sugar=0):
extras = []
if milk:
extras.append("milk")
if sugar:
extras.append(f"{sugar} sugar")
order = f"A {size} coffee"
if extras:
order += " with " + " and ".join(extras)
print(order)
make_coffee()
make_coffee(sugar=2)
make_coffee("large", milk=True)make_coffee(sugar=2) skips straight to the third setting and leaves size and milk at their defaults. Without keyword arguments you'd have to write make_coffee("medium", False, 2), repeating the defaults just to reach sugar.
Any number of arguments: *args
Put a * before a parameter name to accept any number of positional arguments. They arrive as a tuple. By convention the parameter is called args:
def total(*args):
print(args)
return sum(args)
print(total(1, 2))
print(total(5, 10, 15, 20))
print(total())Inside the function, args is an ordinary tuple: (1, 2), then (5, 10, 15, 20), then () when nothing is passed, so sum(()) gives 0. It's the * that does the collecting; the name args is only a convention, and *numbers would work the same.
You can combine normal parameters with *args:
def introduce(greeting, *names):
for name in names:
print(f"{greeting}, {name}!")
introduce("Hi", "Ada", "Grace", "Alan")Any number of keyword arguments: **kwargs
Two stars collect any number of keyword arguments into a dictionary. By convention it's called kwargs:
def print_profile(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_profile(name="Ada", job="Mathematician", born=1815)Inside the function, kwargs is a dictionary: {'name': 'Ada', 'job': 'Mathematician', 'born': 1815}. Each argument name becomes a key, written without quotes in the call.
In the def line |
Collects | Arrives as |
|---|---|---|
*args |
extra positional arguments | a tuple |
**kwargs |
extra keyword arguments | a dictionary |
Order of parameters
When you combine them, the order is: normal parameters, *args, keyword parameters with defaults, then **kwargs:
def report(title, *values, sep=", ", **options):
print(title + ": " + sep.join(str(v) for v in values))
print("options:", options)
report("Scores", 90, 85, 77, sep=" | ", color="blue", bold=True)Going deeper: Keyword-only arguments optional
A bare * in the parameter list means "everything after this must be passed by name". It stops callers from passing settings by position, where it's easy to forget which number means what:
def connect(host, *, timeout=10, retries=3):
print(f"{host}: timeout={timeout}, retries={retries}")
connect("example.com", timeout=5)
# TypeError: timeout or retries?
connect("example.com", 5)You'll spot this in Python's own documentation. help(sorted) shows sorted(iterable, /, *, key=None, reverse=False): the * means key and reverse must be named, and the / means iterable must be passed by position, never as iterable=....
Unpacking when you call a function
The stars also work the other way round. * spreads a list or tuple into separate positional arguments, and ** spreads a dictionary into keyword arguments:
def describe(name, age, city):
print(f"{name}, {age}, from {city}")
person = ["Ada", 36, "London"]
describe(*person)
details = {"name": "Grace", "age": 85, "city": "New York"}
describe(**details)You've seen this with print(), which takes any number of arguments:
numbers = [1, 2, 3, 4] print(numbers) print(*numbers) print(*numbers, sep=" + ")
Exercises
Exercise 1: Power with a default
Write power(base, exponent=2) that returns base raised to exponent. The calls below should print 25 and 125.
# define power here print(power(5)) print(power(5, 3))
Give the second parameter a default value: def power(base, exponent=2):.
Return base ** exponent.
Exercise 2: Average of any numbers
Write average(*numbers) that returns the average of however many numbers are passed, or 0 if none are.
# define average here print(average(2, 4, 6)) print(average(10)) print(average())
*numbers collects all the arguments into a tuple, so sum() and len() work on it.
Return 0 first when the tuple is empty: if not numbers:.
Exercise 3: Build a URL
Write build_url(domain, path="", **params) that returns a URL. Query parameters are joined as key=value with &, after a ?. The call below should print https://example.com/search?q=python&page=2.
# define build_url here
print(build_url("example.com", "search", q="python", page=2))Start with url = f"https://{domain}/{path}". params is a dictionary of the extra keyword arguments.
Build key=value pieces from params.items(), join them with "&".join(...), and add them after a ? only if params isn't empty.
Exercise 4: Fix the shared list
This function should start a fresh list each time, but the second call prints ['a', 'b']. Fix it so it prints ['a'] and then ['b'].
def collect(item, items=[]):
items.append(item)
return items
print(collect("a"))
print(collect("b"))A default list is created once and shared by every call. Use None as the default instead.
Inside the function, create a new empty list when items is None.
Parameter kinds at a glance
A quick reference for later.
| In the definition | What it means | Example call |
|---|---|---|
def f(a, b) |
Required, passed by position or by name | f(1, 2) or f(b=2, a=1) |
def f(a, b=10) |
b is optional and defaults to 10 |
f(1) or f(1, 5) |
def f(items=None) |
Safe default for a list or dict: create it inside the function | f() |
def f(*args) |
Any number of positional arguments, collected in a tuple | f(1, 2, 3) |
def f(**kwargs) |
Any number of keyword arguments, collected in a dictionary | f(x=1, y=2) |
def f(a, *, b) |
Everything after * must be passed by name |
f(1, b=2) |
def f(a, /, b) |
Everything before / must be passed by position |
f(1, 2) or f(1, b=2) |
And when calling a function:
| In the call | What it does | Example |
|---|---|---|
f(name=value) |
Passes an argument by name, in any order | f(b=2, a=1) |
f(*items) |
Unpacks a list or tuple into positional arguments | f(*[1, 2]) is f(1, 2) |
f(**options) |
Unpacks a dictionary into keyword arguments | f(**{"b": 2}) is f(b=2) |
Summary
param=valuegives a parameter a default. Defaults go after required parameters.- Don't use a list or dictionary as a default. Use
Noneand create one inside. - Keyword arguments (
name=value) can be passed in any order and make calls clearer. - A starred parameter like
*argscollects extra positional arguments into a tuple. - A double-starred parameter like
**kwargscollects extra keyword arguments into a dictionary. - In a call,
*listand**dictunpack values into separate arguments.
Know something worth teaching? Write your own tutorial with runnable examples and share it.