Functions · about 45 min

Parameters and arguments

Why this matters

A function is only as useful as its interface: the way callers hand it data. Python gives you a rich toolkit here: optional parameters with defaults, arguments passed by name, functions that accept any number of values, and markers that force callers to be explicit. You meet all of these constantly, for example print(*items, sep=", ") and sorted(data, key=len, reverse=True). Knowing how arguments are matched to parameters, and when default values are created, lets you read any library's documentation and design clear functions yourself.

Vocabulary

When the call happens, Python binds each parameter name to an argument object in the new frame. No copying of the object happens; the parameter is just another name for the same object, exactly as in assignment. (Lesson 5 of the collections module shows why that matters for lists.)

Positional and keyword arguments

Arguments can be matched by position or by name:

def describe(name, age, city):
    return name + " (" + str(age) + ") from " + city

print(describe("Ada", 36, "London"))                 # positional
print(describe(city="Paris", name="Marie", age=66))  # keyword: any order
print(describe("Alan", city="Wilmslow", age=41))     # mixed
Ada (36) from London
Marie (66) from Paris
Alan (41) from Wilmslow

Rules: positional arguments come first, keyword arguments after; each parameter may be filled only once. Keyword arguments make calls self-documenting: connect(host, timeout=5) is clearer than connect(host, 5).

Default values

A parameter can have a default, used when the caller omits it. Parameters with defaults must come after those without:

def power(base, exponent=2):
    return base ** exponent

print(power(5))       # 25
print(power(2, 10))   # 1024
print(power(2, exponent=3))  # 8

When defaults are evaluated

Here is a crucial detail about what happens underneath: the default expression is evaluated once, when the def statement executes, and the resulting object is stored on the function object. Every call that omits the argument gets that same object.

For immutable values like numbers and strings that is harmless. For a mutable object such as a list it is a famous bug:

def add_item(item, basket=[]):
    basket.append(item)       # mutates the one shared default list
    return basket

print(add_item("apple"))
print(add_item("pear"))
print(add_item.__defaults__)
['apple']
['apple', 'pear']
(['apple', 'pear'],)

__defaults__ shows the default list living on the function object and growing with every call. (A list is written [...]; .append(x) adds x to its end. The next module covers lists properly.)

The idiom that fixes it uses None as a "nothing passed" marker and makes a fresh list inside the function, on every call:

def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

Use is None rather than == None or if not basket: an empty list passed by the caller is falsy, and you do not want to throw it away.

Any number of arguments: *args

Prefixing a parameter with * collects all extra positional arguments into a tuple (an immutable sequence, written with parentheses):

def total(*amounts):
    print("got", amounts)
    result = 0
    for a in amounts:
        result += a
    return result

print(total(1, 2, 3))
print(total())
got (1, 2, 3)
6
got ()
0

The name args is a convention, not a keyword; *amounts is clearer here. This is how print accepts any number of things to print.

The * also works in the other direction, at the call site: *sequence unpacks a sequence into separate positional arguments:

values = [4, 5, 6]
print(total(*values))   # same as total(4, 5, 6)
print(*values, sep="-")
got (4, 5, 6)
15
4-5-6

Any keyword arguments: **kwargs

** collects extra keyword arguments into a dictionary mapping names to values (dictionaries are covered in the next module; for now, think "named values"):

def show_options(**options):
    for name in options:
        print(name, "=", options[name])

show_options(color="red", size=3)
color = red
size = 3

Keyword arguments keep the order you wrote them in. **mapping at a call site unpacks a dictionary into keyword arguments. You will see **kwargs mostly in code that forwards arguments to another function, such as decorators (module 13).

Keyword-only and positional-only parameters

Parameters that come after *args, or after a bare *, can only be passed by keyword:

def resize(image, *, width, height):
    ...

resize("cat.png", width=100, height=50)  # fine
resize("cat.png", 100, 50)
TypeError: resize() takes 1 positional argument but 3 were given

This is a design tool: for settings that are easily confused (which number is width?) force callers to name them.

Parameters before a / are positional-only: callers may not use their names. Many built-ins work this way; help(len) shows len(obj, /):

def distance(x, y, /):
    return (x * x + y * y) ** 0.5

print(distance(3, 4))     # 5.0
distance(x=3, y=4)
TypeError: distance() got some positional-only arguments passed as keyword arguments: 'x, y'

Positional-only parameters let a library rename them later without breaking anyone's code.

The full order in a definition is:

def f(pos_only, /, normal, *args, kw_only, **kwargs):

You will rarely need all of them at once; recognizing each piece is the important skill.

How Python matches a call

Conceptually, for each call CPython:

  1. Fills parameters from the positional arguments, left to right; extras go into *args (or it is a TypeError if there is no *args).
  2. Fills parameters from the keyword arguments by name; unknown names go into **kwargs (or TypeError: got an unexpected keyword argument). A name already filled positionally is TypeError: got multiple values for argument.
  3. Uses defaults for anything still empty; anything still missing is TypeError: missing ... required argument.

Common mistakes

Recap

Going further: read the "More on Defining Functions" section of the official Python tutorial (docs.python.org/3/tutorial/controlflow.html). Then run help(sorted) and help(print) and identify which parameters are positional-only and which are keyword-only.

Exercises

Exercise 1 · quiz

Which calls are valid?

Given

def box(width, height=1, *, fill="#"):
    ...

which calls are valid? Select all that apply.

Answer
  • box(3)

  • box(height=2, width=3)

  • box(3, fill=".")

box(3) uses the default height. Keyword arguments may come in any order, so box(height=2, width=3) is fine. fill comes after the bare *, so it is keyword-only: box(3, 2, "*") fails with TypeError: box() takes from 1 to 2 positional arguments but 3 were given. And box(width=3, 2) is a SyntaxError: positional arguments cannot follow keyword arguments.

Exercise 2 · quiz

The mutable default

What does this print?

def add_item(item, basket=[]):
    basket.append(item)
    return basket

add_item("apple")
print(add_item("pear"))
Answer
  • ['apple', 'pear']

The default value [] is evaluated once, when the def statement runs, and the same list object is reused by every call that omits basket. The first call appended "apple" to that shared list; the second appended "pear" to the same list. The fix is basket=None and, inside, if basket is None: basket = [].

Exercise 3 · write code

Defaults and keywords

Define make_greeting(name, greeting="Hello", punctuation="!") returning a string like "Hello, Ada!".

File: solution.py

Starting point

def make_greeting(name):
    return "Hello, " + name + "!"
Hint 1

Parameters with defaults are written param=value in the def line, after those without.

Hint 2

Build the result as greeting + ", " + name + punctuation.

Reference solution
def make_greeting(name, greeting="Hello", punctuation="!"):
    return greeting + ", " + name + punctuation

Exercise 4 · write code

Any number of arguments

Define average(*numbers) that returns the arithmetic mean of any number of numeric arguments, and returns None when called with no arguments at all.

Do not use the statistics module; use a loop (or sum and len) over the tuple numbers.

File: solution.py

Starting point

def average(numbers):
    pass
Hint 1

*numbers collects all positional arguments into a tuple; len(numbers) tells you how many.

Hint 2

Check for zero arguments first and return None, to avoid dividing by zero.

Hint 3

/ always produces a float, so 21 / 3 is 7.0.

Reference solution
def average(*numbers):
    if len(numbers) == 0:
        return None
    total = 0
    for n in numbers:
        total += n
    return total / len(numbers)

Exercise 5 · write code

Fix the default trap

The starter's add_tag(tag, tags=[]) has the mutable default bug: tags from earlier calls leak into later ones. Fix it so that:

Keep the parameter names tag and tags.

File: solution.py

Starting point

def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags
Hint 1

Defaults are evaluated once, at def time. Use a default that cannot be mutated.

Hint 2

Use tags=None, then inside: if tags is None: tags = [].

Reference solution
def add_tag(tag, tags=None):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags