2.3 Functions, scope and arguments
Checked against the Python 3 documentation, August 2026
What this is and why it exists
Functions are where code stops being a script and starts being something other people — including you in three months — can call safely. Python's argument system is richer than most languages', and understanding scope explains a whole family of otherwise baffling behaviours. This topic also contains the single most famous Python trap, which bites almost everybody exactly once.
The vocabulary
- Parameter and argument — the name in the definition; the value at the call.
- Positional argument — matched by order.
- Keyword argument — matched by name, which makes a call readable at the point of use.
- Default value — used when the caller supplies nothing.
- Extra positional arguments — a parameter written
*argscollects them into a tuple. - Extra keyword arguments — a parameter written
**kwargscollects them into a dict. - Scope — the region in which a name is visible.
- Closure — a function that remembers names from the scope it was defined in.
- Docstring — the string at the top of a function, which is its documentation.
The mental model
A call matches arguments to parameters: positionally first, then by name, with defaults filling anything left. The reference describes the two collectors precisely — extra positional arguments "will be wrapped up in a tuple", and a final parameter of the form with two stars "receives a dictionary containing all keyword arguments except for those corresponding to a formal parameter". Note the ordering rule that follows: the single-star parameter must come before the double-star one, and anything written after the single-star collector can only be passed by keyword.
That gives you a design tool worth using deliberately. A function with three or more parameters is much harder to call correctly by position than by name, so making the later ones keyword-only turns a mysterious call into a readable one — and it stops a caller silently swapping two arguments of the same type.
Then the trap, and the documentation states it as an "Important warning: the default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes." Written as a function taking a list default, the same list is shared by every call that does not supply one, so results accumulate across calls that have nothing to do with each other. The documentation's own remedy is the pattern to memorise: default to None, and create the real value inside the function when it is None.
Scope in Python is looked up in a fixed order: the local function, then any enclosing functions, then the module, then the built-ins. Assigning to a name anywhere in a function makes it local for the whole function, which is why reading a module-level variable works fine until you also assign to it somewhere below, and then the read fails. A closure is what you get when an inner function refers to a name from the enclosing one: the inner function keeps that name alive after the outer call has returned, which is the mechanism behind decorators in a later topic.
Finally, lambda, map and filter. A lambda is a one-expression function without a name, and it is genuinely useful as a sort key. Beyond that, a comprehension usually reads better than map or filter — and a lambda longer than a line is a function that has not been given a name yet, which is a loss for everyone reading it.
In code
Checked against the Python tutorial's "More on Defining Functions".
def summarise(rows, *, top=3, label="results"):
"""Return a short line describing the highest scores.
rows: a list of dicts with a "score" key.
top: how many to include.
"""
best = sorted(rows, key=lambda r: r["score"], reverse=True)[:top]
names = ", ".join(r["name"] for r in best)
return f"{label}: {names}"
def log_all(prefix, *messages, **fields):
for m in messages:
print(prefix, m, fields)summarise takes top and label by keyword only, because everything after the bare star is keyword-only — so no caller can pass three by position and get them in the wrong order.
And the trap, with the documentation's own fix beside it:
def collect_bad(item, acc=[]): # one list, shared by every call
acc.append(item)
return acc
def collect(item, acc=None): # a new list per call
if acc is None:
acc = []
acc.append(item)
return accWhat you should now be able to explain or do
Write a function whose later parameters can only be passed by name, and say what that prevents. Explain what *args and **kwargs collect and in which order they must appear. State the mutable-default rule and write the None fix from memory. Name Python's four scopes in lookup order, and explain why assigning to a module-level name inside a function breaks reading it. Say when a lambda earns its place and when it does not.
Check yourself
What does the bare star in a parameter list do?
Makes everything after it keyword-only. Callers must name those arguments, which stops two same-typed values being passed in the wrong order.
Why does a function with acc=[] accumulate results across unrelated calls?
Because the default value is evaluated once, when the function is defined — so every call that does not pass one gets the same list. Default to None and create the list inside.
You read a module-level counter inside a function and it works; you add one line that assigns to it and now the read fails. Why?
Assigning to a name anywhere in a function makes it local for the entire function, so the earlier read now refers to a local that does not exist yet.
What is a closure?
An inner function that refers to names from the function it was defined in, and keeps them alive after that outer call has returned. It is the machinery decorators are built on.
When is a lambda the right choice?
As a small key or callback — a sort key, an argument to something expecting a function. Longer than a line, it is a function that has not been given a name, and naming it helps everyone reading the code.
Go deeper
We haven't checked most of these for screen reader use yet.
- Week 0 Functions · Harvard CS50 · Videovideo, with transcript
Back to Functions, scope and arguments: work through the checklist