Functions and scope
- Module
- M1.10
- Lesson
- 1 of 1
- Reading time
- 4 min
In this lesson
- Wrap repeated logic in a function with
def, taking parameters and returning a result. - Tell a local variable from a global one, and use
globalto change a global from inside a function. - Avoid the mutable-default-argument trap, where a default list or dict persists between calls.
- Write a short one-line function with
lambda.
Every program so far ran as one block of code, top to bottom, with no way to reuse a piece of logic without writing it out again. Many CCC problems repeat the same small calculation on several different values. This lesson covers wrapping that logic in a function with def, and the rules for which variables a function can see and change.
Defining a function
def starts a function definition: a name, parameters in parentheses, and an indented block that runs each time the function is called. return sends a value back to wherever the function was called from.
def square(x): return x * x
print(square(3))print(square(5))Output
9
25square(3) runs the function's block with x set to 3, and return x * x sends 9 back. Calling square(5) runs the same block again with a different x, giving 25. The function's body is written once, but runs however many times it is called.
Local and global variables
A variable assigned inside a function is localA variable that exists only inside the function call that created it, separate from any variable of the same name outside that function.In the glossary to it by default: it exists only while that call runs, separate from any variable of the same name outside the function.
total = 0
def add(x): total += x return total
print(add(5))Error: UnboundLocalError
Traceback (most recent call last):
File "unbound_local.py", line 7, in <module>
print(add(5))
File "unbound_local.py", line 4, in add
total += x
UnboundLocalError: local variable 'total' referenced before assignmentadd assigns to total inside its own block, which makes Python treat total as local to add for its entire body, even on the line that reads it. Since a local total has no value yet at that point, Python raises UnboundLocalError, even though a global total already exists outside the function.
The global keyword tells Python that a name inside a function refers to the global variable, not a new local one.
total = 0
def add(x): global total total += x
add(5)add(3)print(total)Output
8global total inside add means total += x changes the same total defined outside the function. Calling add(5) then add(3) leaves the global total at 8, changed by both calls in turn.
A trap: mutable default arguments
A parameter's default value is created once, when the function is defined, not fresh on every call.
def add_item(item, basket=[]): # noqa: B006 (the mutable default is the bug this example shows) basket.append(item) return basket
print(add_item("apple"))print(add_item("pear"))Output
['apple']
['apple', 'pear']basket=[] looks like a fresh empty list every time, but it is the same list object reused on every call that does not pass its own basket. add_item("apple") and add_item("pear") both append to that one shared list, so the second call's result already holds "apple" too.
def add_item(item, basket=None): if basket is None: basket = [] basket.append(item) return basket
print(add_item("apple"))print(add_item("pear"))Output
['apple']
['pear']basket=None avoids sharing a list across calls. Checking if basket is None and building [] inside the function body gives every call its own fresh list, so add_item("pear") no longer carries "apple" along with it.
Default and keyword arguments
A parameter can have a default value, written right in the def line, so a caller can leave it out and get that value instead. def greet(name, greeting="Hello"): lets greet("Ana") fall back to "Hello", while greet("Ana", "Hi") overrides it. A parameter with a default must come after every parameter without one, so Python always knows which value belongs to which name.
Calling a function with name=value instead of plain position is a keyword argument. greet(name="Ana", greeting="Hi") reads clearly and works no matter what order you write the two arguments in, since each one names the parameter it fills. This matters most once a function takes several parameters, where a plain positional call like build_grid(5, 3, 0) forces a reader to remember which number means rows, which means columns, and which is the fill value, while build_grid(rows=5, cols=3, fill=0) says so directly.
Passing a function as an argument
A function is a value like any other, and one function can take another function as an argument. sorted(words, key=len) passes the built-in len function itself, not a call to it (no parentheses after len), so sorted can call len on each word to decide the order, without sorted needing to know anything about how lengths are computed.
This is where lambda earns its keep: instead of defining a whole named function just to hand to key, sorted(words, key=lambda w: len(w)) builds the function on the spot, right where it is needed. Any place that expects a small function used exactly once, and thrown away right after, is a natural fit for lambda instead of a full def.
A short function with lambda
lambda writes a small, unnamed function in one line: lambda parameters: expression. It can only hold a single expression, whose value it returns automatically, with no return keyword.
square = lambda x: x * xprint(square(4))print(square(6))Output
16
36lambda x: x * x behaves like square() from earlier, just written without a name or a return. Assigning it to square lets you call square(4) the same way as a function defined with def.
Tracing the call stack
(nothing printed yet)- Just changed
Figure 1One function calling another, and returning back through the calls
Read the steps as text
add_one calls double, which finishes and returns before add_one adds 1 and returns its own result.
- Python starts at the top of the file. Line 1 runs first.
- Line 1 runs: the name
doublenow refers to a function. - Line 4 runs: the name
add_onenow refers to a function. - Line 7 calls
add_onewithx = 3. A new frame foradd_onegoes on top of the call stack. add_one()starts running its body at line 5.- Line 5 calls
doublewithx = 3. A new frame fordoublegoes on top of the call stack. Callingdouble(x)pushes a new frame onto the call stack;add_onewaits, paused, untildoublereturns. double()starts running its body at line 2.- The call of
doublewithx = 3returns6. Its frame leaves the call stack next.doublereturns its result here, popping its frame off the stack and handing control back toadd_one. - The call with
x = 3handed back6. The call ofadd_onewithx = 3returns7. Its frame leaves the call stack next. - Line 7 runs: it prints
7. The program has finished: no lines are left to run.
add_one(3) calls double(3) before it can finish its own + 1. Calling double pushes a new frame onto the call stack, on top of add_one's own frame, and add_one waits there, paused, until double returns. Once double returns 6, its frame is popped, and add_one picks up where it left off, adding 1 to get 7.
Wrapping a whole program's main logic in def main(): ... followed by a single call to main() is a common CCC habit. Variables inside main() are local, and local variables run a little faster than global ones, on top of keeping the top level of the file short and easy to read.
Getting these right
Forgetting global when you meant to change a variable defined outside a function is the single most common scope mistake. Python quietly creates a new local variable instead of raising an error, so the bug does not announce itself; the global value simply never changes, and the symptom shows up somewhere else, far from the line that caused it.
Recap
This lesson covered defining a function with def, parameters, and return. It also covered local versus global variables, the global keyword, and the UnboundLocalError an unavailable local can raise. Last, it covered the mutable-default-argument trap and its fix, a one-line function with lambda, and wrapping a program in def main(): ...; main(). The problem below needs a helper function to check one direction at a time.
Practice
Try this on the judge. The link opens the problem on WMOJ.
- 2023 J5CCC Word Hunt (opens on WMOJ in a new tab) WMOJ
Search a small grid for a hidden word along a single horizontal line, the easiest of several scoring tiers.