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")Parameters with defaults must come after the ones without:
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"))
print(add_item("pear")) # surprise: the apple is still there!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"))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")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")
print(f"A {size} coffee" + (" with " + " and ".join(extras) if extras else ""))
make_coffee()
make_coffee(sugar=2)
make_coffee("large", milk=True)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())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)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)
connect("example.com", 5) # TypeError: is 5 the timeout or the retries?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.