Skip to content
CCC Python Course

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 global to 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.

examples/def_basics.py
def square(x):    return x * x
print(square(3))print(square(5))

Output

9
25
A function that squares a number, called twice

square(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.

examples/unbound_local.py
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 assignment
Assigning to a name Python treats as local, before it has a value

add 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.

examples/global_keyword.py
total = 0
def add(x):    global total    total += x
add(5)add(3)print(total)

Output

8
Declaring total global so the function can change it

global 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.

examples/mutable_default_bug.py
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']
A default list that keeps growing across calls

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.

examples/mutable_default_fixed.py
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']
Using None as the default, and building a fresh list inside the function

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.

examples/lambda_basics.py
square = lambda x: x * xprint(square(4))print(square(6))

Output

16
36
A one-line function stored in a variable

lambda 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

1def double(x):
2 return x * 2
3
4def add_one(x):
5 return double(x) + 1
6
7print(add_one(3))
Output so far
(nothing printed yet)
Frames and objects, step 1 of 10FramesObjectsGlobal frame
Speed
Python starts at the top of the file. Line 1 runs first.
  • 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.

  1. Python starts at the top of the file. Line 1 runs first.
  2. Line 1 runs: the name double now refers to a function.
  3. Line 4 runs: the name add_one now refers to a function.
  4. Line 7 calls add_one with x = 3. A new frame for add_one goes on top of the call stack.
  5. add_one() starts running its body at line 5.
  6. Line 5 calls double with x = 3. A new frame for double goes on top of the call stack. Calling double(x) pushes a new frame onto the call stack; add_one waits, paused, until double returns.
  7. double() starts running its body at line 2.
  8. The call of double with x = 3 returns 6. Its frame leaves the call stack next. double returns its result here, popping its frame off the stack and handing control back to add_one.
  9. The call with x = 3 handed back 6. The call of add_one with x = 3 returns 7. Its frame leaves the call stack next.
  10. 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.

  1. 2023 J5
    CCC 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.