Day 7 / 25 Functions II: Scope, Recursion, Composition 0/0 exercises Exercises ↓

AmouAI Hub/Courses/Programming Fundamentals/Day 7

Week 2 · Functions & Data Structures · Day 7

Functions II: Scope, Recursion, Composition

Where names live, why side effects hurt, and how a function that calls itself can be easier to read than a loop.

Study time
4 hours
Reading
Think Python, Ch. 5.8–5.10, 6.5–6.9
Focus
LEGB · purity · recursion

By the end of today you can

  1. Apply the LEGB rule to predict which name Python will find
  2. Say what a pure function is, and why purity makes testing trivial
  3. Recognise a side effect, and know when one is legitimate
  4. Write a lambda, and say when a def is better
  5. Identify the base case and the recursive case in a problem
  6. Trace a recursive call down and back up, frame by frame
  7. Convert simple recursion to iteration and back
  8. Compose small functions into a pipeline

Today's videos

Watch each video, then work the matching sections below. Watching alone will not do it.

7A
Scope, Purity & the LEGB Rule (120 min)
The four scopes and the order Python searches them · shadowing · why UnboundLocalError happens · pure functions and side effects · where lambda earns its place and where it does not.
7B
Recursion & Composition (120 min)
Base case and recursive case · tracing a call down and back up, frame by frame · when recursion beats a loop and when it does not · composing small functions into a pipeline · the readability limit of nested calls.

1The LEGB rule

Four places Python looks for a name, always in the same order.

Yesterday you learned that locals are local. That is the first letter of a four-letter rule which explains every name lookup in Python.

LetterScopeWhich names
LLocalAssigned inside the current function
EEnclosingLocals of a function that wraps this one
GGlobalAssigned at the top level of the module
BBuilt-inprint, len, range — Python's own names

Python checks them in that order and stops at the first hit. It never searches backwards.

Shadowing is not an error

Three different size values coexisted happily. That is not a bug — it is what makes functions safe to write without reading the rest of the program. But it does mean a typo in a name can silently pick up something from an outer scope instead of failing loudly.

2Pure functions and side effects

Same input, same output, no surprises.

A function is pure when two things are true: it always returns the same result for the same arguments, and it changes nothing outside itself.

Pure functions are…Because
Easy to testOne call, one assertion. No setup, no teardown.
Easy to reason aboutYou only read the function, not the program around it.
Safe to moveNo hidden dependencies to leave behind.
Safe to reorderCalling them in a different order changes nothing.
Side effects are not evil — they are just concentrated

Something has to print, write files and read input, or your program does nothing observable. The craft is to keep side effects at the edges — in main, in collect_numbers, in save_report — and keep the middle of your program pure. That is exactly the shape yesterday's refactor produced.

lambda — a function with no name

lambda.py
double = lambda n: n * 2        # works, but...

def double(n):                  # ...prefer this
    return n * 2

# where lambda genuinely earns its place:
people = [("Amin", 34), ("Ada", 36), ("Grace", 45)]
print(sorted(people, key=lambda person: person[1]))
[('Amin', 34), ('Ada', 36), ('Grace', 45)]
Never assign a lambda to a name

double = lambda n: n * 2 is strictly worse than def: the function's __name__ becomes <lambda>, so tracebacks stop telling you which function failed. ruff will flag it (E731). Use lambda only as a throwaway argument — a key=, a sort, a one-line callback.

3Base case, recursive case

A function that calls itself, and the one thing that stops it.

Recursion is not a trick. It is what you write when a problem contains a smaller copy of itself. Every recursive function needs exactly two things:

  1. A base case — an input small enough to answer without recursing.
  2. A recursive case — reduce the problem, call yourself, combine.
Miss the base case and you get RecursionError

RecursionError: maximum recursion depth exceeded. Python stops you at around 1000 frames. That limit is not the bug — it is the symptom. The bug is that nothing was ever going to stop.

That last observation is the one people miss. The n part of return n factorial(n - 1) cannot run until the inner call has answered. All four multiplications happen during the unwind.

The same thing as a loop

When recursion is genuinely the right answer

Not for factorial. Recursion earns its place when the data is nested rather than flat — a folder containing folders, a comment thread with replies, an expression containing sub-expressions. You will meet real cases on Day 10 (walking a directory tree) and Day 11 (binary search and merge sort).

4Composition

Small functions, joined end to end.

Once functions return values, they can be plugged into each other. That is the practical reward for section 3 of yesterday.

pipeline.py
def clean(text):
    """Strip whitespace and lowercase."""
    return text.strip().lower()


def words_in(text):
    """Split into words."""
    return text.split()


def longest(words):
    """The longest word. Assumes at least one."""
    best = words[0]
    for w in words:
        if len(w) > len(best):
            best = w
    return best


raw = "   The QUICK brown fox jumped   "
print(longest(words_in(clean(raw))))
jumped

Read the last line inside out: clean the text, split it into words, take the longest. Three functions you can test separately, joined in one expression.

Why this matters more than it looks

Each stage can be tested with one line, replaced without touching the others, and reused in a different pipeline. On Day 15 you will write automated tests, and functions shaped like this are the ones that are pleasant to test. Functions that print are not testable at all.

Readability has a limit

too_far.py
# technically fine, practically unreadable
print(longest(words_in(clean(read_file(path_for(user_id))))))

# name the intermediate steps instead
raw = read_file(path_for(user_id))
words = words_in(clean(raw))
print(longest(words))

Two or three nested calls read well. Five do not. Naming the intermediate values costs two lines and buys back the ability to read the code at speed — and to put a breakpoint between the stages when something goes wrong.

>_Python playground

A real Python interpreter running inside your browser. Nothing is installed, nothing is uploaded, nothing can break.

scratch.pypython not loaded
Values for input(), comma separated →
Output appears here. The first run takes a few seconds while Python loads.

Exercise set

Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.

All Warm-up Core Challenge Reset day

Day 8 — Lists & Tuples

The structure Week 1 kept making you want — and where the box metaphor breaks.

Continue →