Chapter 5 · Control Flow & Functions

Functions in Depth

Python functions are flexible: default values, keyword arguments, any number of arguments, and markers that control how callers may pass them.

You will learn

  • How to define functions, return values, and write docstrings.
  • Default argument values, and the mutable-default trap.
  • Keyword arguments, and the / and * parameter markers.
  • *args, **kwargs, and unpacking with * and ** at the call site.
  • Lambdas, type annotations, and the PEP 8 coding style.

Defining functions

def creates a function. The body is indented. A docstring (a string on the first line) documents what the function does.

Words to know:

Returning values

return value sends a result back. A function without a return, or with a bare return, returns None, Python's "no value" object.

(The empty line is what fib(0) prints by itself: its loop never runs, so only the final print() does.)

Functions are objects. You can assign them to other names, store them in lists, and pass them to other functions:

☕ Java: there is no overloading. A second def with the same name simply replaces the first. Default arguments and *args cover what overloading does in Java. 🟨 JS: calling with too few or too many arguments is an error in Python (TypeError), not a silent undefined.

Default argument values

A parameter can have a default, so callers may leave it out.

Default values are evaluated once, when the def line runs, not on each call:

⚠️ The mutable-default trap. Because the default is created only once, a mutable default (a list or dict) is shared between all calls.

❌ Shared list grows forever

✅ Use None and create a fresh list

is None checks whether something is the None object. That is the standard way to test for None (Appendix A explains is vs ==).

Keyword arguments

Callers can name arguments: name=value. Keyword arguments can come in any order, but they must come after all positional ones.

Invalid calls:

CallError
parrot()required argument voltage missing
parrot(voltage=5.0, 'dead')positional argument after a keyword argument (a SyntaxError)
parrot(110, voltage=220)got multiple values for voltage
parrot(actor='John Cleese')unexpected keyword argument actor

💡 Tip: keyword arguments make calls self-documenting. resize(img, 200, 100, True) is a puzzle. resize(img, width=200, height=100, keep_ratio=True) explains itself.

Special parameters: / and *

By default, any parameter can be passed either by position or by keyword. Two markers in the parameter list restrict that:

Use…When…
positional-only (/)the name has no meaning for callers, or you may want to rename it later (len(obj), abs(x))
keyword-only (*)the meaning would be unclear without a name, especially boolean flags (sorted(xs, reverse=True))
neithermost everyday functions

Positional-only parameters also let a function accept **kwargs containing a key with the same name:

Arbitrary argument lists: *args and **kwargs

The names args and kwargs are only a convention. The stars are what matter.

Parameters after *args are automatically keyword-only:

☕ Java: *args is like varargs, String... args. 🟨 JS: it is like rest parameters, ...args. **kwargs has no direct Java equivalent; in JS you would pass an options object.

Unpacking argument lists

The opposite direction: you already have a list or a dict, and you want to spread it into separate arguments. Use * for sequences and ** for dicts at the call site.

Lambda expressions

A lambda is a tiny anonymous function written as one expression: lambda params: expression. It is mostly used where a short function is passed as an argument, like a sort key.

❌ Don't name a lambda

✅ Use def for named functions

A def function gets a real name in tracebacks and can have a docstring. The PEP 8 style guide says not to assign lambdas to names.

☕ Java / 🟨 JS: like x -> x + n or x => x + n, but the body must be a single expression: no statements and no blocks.

Documentation strings

The conventions are: the first line is a short summary that starts with a capital letter and ends with a period. If there is more, leave a blank line and then the details.

help(my_function) shows the docstring with the indentation cleaned up.

Function annotations (type hints)

You can add optional type hints to parameters (name: type) and to the return value (-> type). Python itself does not check them at run time. Editors and tools like mypy or pyright use them to catch mistakes before you run the code.

Common hints: int, float, str, bool, list[str], dict[str, float], tuple[int, int], int | None ("an int or None").

📝 Changed in 3.14: annotations are now evaluated lazily (PEP 649), only when something asks for them. So a hint can name a class that is defined further down the file, without the quotes older code needed (-> "Node"). You'll still see those quotes, and from __future__ import annotations, in older code.

Intermezzo: coding style (PEP 8)

Python has an official style guide, PEP 8. Most projects follow it, and tools like ruff and black apply it for you. The key points:

❌ Java habits

✅ PEP 8

More examples

1. A flexible logger

2. Forwarding arguments with *args and **kwargs

3. Sorting expenses with a key function

4. Functions that make functions

5. A typed, documented helper

Recap

  • def name(params): plus an indented body. Without return, a function returns None. Functions are objects.
  • Defaults are evaluated once. Never use a mutable default; use None and create the object inside.
  • Keyword arguments come after positional ones. / marks positional-only parameters and * marks keyword-only ones.
  • *args is a tuple of extras and **kwargs is a dict of extras. At the call site, * and ** unpack.
  • A lambda is a one-expression function, ideal for key=. Annotations are hints, not checks. Follow PEP 8.

Exercises

Exercise 1 🟢 easy build it

Write area(width, height=None). If height is missing, compute the area of a square.

Show solution

None is the usual "not given" marker, tested with is None.

Exercise 2 🟢 easy predict the output

What does print(greet("Ana")) print?

Show solution

Two lines: Hi Ana, then None. greet prints but returns nothing, so the outer print shows None.

Exercise 3 🟢 easy explain why

Which of these calls are valid for def f(a, b=2, *, c=3):? f(1), f(1, 2, 3), f(1, c=5), f(a=1, b=1), f(b=1)

Show solution

Valid: f(1), f(1, c=5), f(a=1, b=1). f(1, 2, 3) fails because c is keyword-only. f(b=1) fails because the required a is missing.

Exercise 4 🟢 easy refactor it

Make this PEP 8 compliant: def GetTotal( Items ):return sum(Items)

Show solution

Use a snake_case name, no spaces inside the parentheses, and put the body on its own indented line.

Exercise 5 🟡 medium fix the bug

Every new cart contains the items of previous carts. Fix it.

Show solution

The default list is created once, when def runs, and then shared by every call that relies on it.

Exercise 6 🟡 medium predict the output

What does this print?

Show solution

rest is always a tuple, possibly empty. (6,) is a one-item tuple; the comma is what makes it a tuple.

Exercise 7 🟡 medium build it

Write average(*numbers) that returns the mean, or 0 when it is called with no arguments.

Show solution

numbers is a tuple, so sum, len and truthiness all work on it.

Exercise 8 🟡 medium build it · playground

Create playground/money.py with format_money(cents, /, *, currency="€"). It returns strings like "€12.05" and "-€3.50". Make currency keyword-only and cents positional-only.

Show solution

Using abs() before divmod avoids the floor-division surprise with negatives (Chapter 2). :02d pads the cents to two digits (Chapter 9).

Exercise 9 🔴 hard predict the output

What does this print, and why?

Show solution

[2, 2, 2]. Each lambda looks up i when it is called, and by then the loop has finished with i == 2. To capture the current value, use a default argument, which is evaluated at definition time: lambda i=i: i gives [0, 1, 2]. JS behaves the same way with var.

Exercise 10 🔴 hard explain why

Why does def f(**kwargs, *args): fail, while def f(a, /, b, *args, c, **kwargs): is valid? Write down the rule for the order of parameters.

Show solution

The order must be: positional-only → / → normal → *args (or a bare *) → keyword-only → **kwargs. **kwargs must always be last, because it collects whatever is left. Putting *args after it is a SyntaxError.

Exercise 11 🔴 hard mini-project · playground

Create playground/report.py with summarize(*expenses, by="category", top=None). Each expense is a tuple (category, amount). Return a list of (category, total) sorted by total, highest first, keeping only the first top items if top is given. Use a helper function and a lambda.

Show solution

This uses *args collection and unpacking, keyword-only options, a None default and a lambda key. Dictionaries (.get, .items) are covered properly in Chapter 7. The leading underscore in _totals means "internal helper".

Official docs