AmouAI Hub/Courses/Programming Fundamentals/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.
By the end of today you can
- Apply the LEGB rule to predict which name Python will find
- Say what a pure function is, and why purity makes testing trivial
- Recognise a side effect, and know when one is legitimate
- Write a
lambda, and say when adefis better - Identify the base case and the recursive case in a problem
- Trace a recursive call down and back up, frame by frame
- Convert simple recursion to iteration and back
- Compose small functions into a pipeline
▶Today's videos
Watch each video, then work the matching sections below. Watching alone will not do it.
UnboundLocalError happens · pure functions and side effects · where lambda earns its place and where it does not.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.
| Letter | Scope | Which names |
|---|---|---|
| L | Local | Assigned inside the current function |
| E | Enclosing | Locals of a function that wraps this one |
| G | Global | Assigned at the top level of the module |
| B | Built-in | print, len, range — Python's own names |
Python checks them in that order and stops at the first hit. It never searches backwards.
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 test | One call, one assertion. No setup, no teardown. |
| Easy to reason about | You only read the function, not the program around it. |
| Safe to move | No hidden dependencies to leave behind. |
| Safe to reorder | Calling them in a different order changes nothing. |
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
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]))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:
- A base case — an input small enough to answer without recursing.
- A recursive case — reduce the problem, call yourself, combine.
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
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.
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))))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.
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
# 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.
input(), comma separated →
✓Exercise set
Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.
Day 8 — Lists & Tuples
The structure Week 1 kept making you want — and where the box metaphor breaks.