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:
- A parameter is the name in the definition (
n). An argument is the value you pass in the call (100). - Variables assigned inside a function are local: they only exist during the call. To assign to a module-level variable from inside a function, you must declare it
global(Chapter 11 explains scopes). - Arguments are passed by object reference. The function gets the same object, not a copy. If you change a list that was passed in, the caller sees the change.
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:
| Call | Error |
|---|---|
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:
- Everything before
/is positional-only: callers cannot use its name. - Everything after
*is keyword-only: callers must use its name.
| 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)) |
| neither | most everyday functions |
Positional-only parameters also let a function accept **kwargs containing a key with the same name:
Arbitrary argument lists: *args and **kwargs
*argscollects any extra positional arguments into a tuple.**kwargscollects any extra keyword arguments into a dict.
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:
- Indent with 4 spaces, never tabs.
- Keep lines to 79 characters (many teams allow 88–100).
- Use blank lines to separate functions and classes (two blank lines at the top level).
- Put comments on their own line when possible. Use docstrings.
- Put spaces around operators and after commas:
a = f(1, 2) + g(3, 4). - Name things
snake_casefor functions and variables,CamelCasefor classes, andUPPER_CASEfor constants. Always call the first method parameterself(Chapter 11). - Use UTF-8, and plain ASCII identifiers in code that is shared internationally.
❌ 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. Withoutreturn, a function returnsNone. Functions are objects.- Defaults are evaluated once. Never use a mutable default; use
Noneand create the object inside. - Keyword arguments come after positional ones.
/marks positional-only parameters and*marks keyword-only ones. *argsis a tuple of extras and**kwargsis 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".