Functions, Scope & Modules
- Define functions with parameters, defaults, *args and **kwargs, and call them correctly
- Explain the difference between returning a value and printing it, and choose the right one
- Predict which variable a name refers to using the LEGB scope rule
- Import and use standard-library modules (math, random, datetime)
- Write a module with a docstring and an if __name__ == "__main__" block
| Spaced-rep warm-up: Day 1β2 flashcards | 10 min |
| Concept study: ELI5 + tech (return vs print, LEGB, modules) | 20 min |
| Guided: recipe cards, scope experiments, stdlib tour | 45 min |
| Practice: refactor the path quiz into functions | 20 min |
| Project: the toolbox module | 15 min |
| Quiz + flashcards | 10 min |
Builds on: Day 1 β Running scripts and reading errors Β· Day 2 β Variables, types, and control flow
A recipe is instructions written once and cooked many times. You don't rewrite "how to make stock" inside every soup recipe β you write it on its own card, give it a name, and say "make stock" whenever you need it. A function is that card: def make_stock(bones, hours): names the recipe and lists its ingredient slots (parameters). Calling make_stock(chicken, 3) fills the slots and runs the card. Crucially, a good recipe *hands you the dish* at the end β that is return. Printing is different: it's the cook shouting "the stock is done!" from the kitchen. Nice to hear, but you can't pour a shout into tomorrow's soup. Return hands the value back so other code can use it.
Each time a recipe runs, it gets a clean kitchen counter: the bowls and spoons it uses (local variables) exist only during that cooking session and are cleared afterward. That's scope β names inside a function don't leak out and don't collide with yours. And a module is a whole recipe *book*: someone else's tested collection (import math) or your own file of recipes that any script can open.
Functions are the unit of everything you will ship. Every API endpoint (Day 41), every tool you hand an LLM agent (Day 111), every test you write (Day 18) is "a function with a clear input and a returned output." The return-vs-print distinction is the first hard line between toy scripts and real software: code that prints can only be watched, code that returns can be tested, composed, and called by other systems. And FDEs live in other people's modules β on-site you'll read an unfamiliar codebase and your first question is always "what does this function take and what does it return?"
Guided practice
Recipe cards β define, call, return
15 min- Create
week-01/recipes.pyand type the starter code. Run it, then read the output against the code β match each printed line to the call that produced it. - The
shout_areavsareapair is today's core lesson. Add the linex = shout_area(3, 4)thenprint(x)β it prints None, because shouting isn't returning. Write one comment explaining why. - Call
greetthree ways:greet("Ada"),greet("Ada", "?"),greet(punct="...", name="Ada"). Predict each output first. - Add a new function
total_minutes(days, hours_per_day=2)that RETURNS days * hours_per_day * 60. Use it to print the minutes in this 180-day program without typing the answer yourself. - Delete the return line from
area, rerun, and watch the arithmetic downstream break with a TypeError about NoneType β meet this error today so you recognize it forever.
Whose kitchen? β scope experiments
15 min- Create
scope_lab.pywith the starter code, but DO NOT run it yet. Write your prediction for each numbered print as a comment. - Run it. Section 1 shows locals dying with the call β the NameError at the end is correct behavior, not a bug. Comment out that line after reading the error.
- Section 2 is the shadowing trap: assigning
score = ...inside the function created a brand-new local; the global never changed. Confirm by reading the two printed values. - Section 3: fix
add_bonusthe professional way β takescoreas a parameter and return the new value, then rebind at the call site (score = add_bonus(score)). Noglobalkeyword needed, today or (almost) ever. - In one sentence at the bottom: what does LEGB stand for, and which wins?
The recipe books β stdlib tour and the main guard
15 min- Create
toolbox.pywith the starter code and run it directly:python toolbox.py. The demo lines under the guard print. - Now create a second file
use_toolbox.pycontaining just:import toolboxandprint(toolbox.roll(20)). Run it β notice the demo lines did NOT print. Explain in a comment why the guard made that happen. - In the REPL, take a 5-minute stdlib field trip:
import maththen trymath.pi,math.sqrt(2),math.ceil(3.2). Thenimport randomand runrandom.choice(["tea", "coffee"])three times. - Add a function
days_until_done(current_day)to toolbox.py that returns 180 minus current_day, with a one-line docstring. Add a demo call for it under the guard. - Run
python -m toolboxfrom the same folder β the -m flag runs a module by name instead of by filename; it behaves the same here and matters more once packages arrive on Day 17.
On your own
Refactor the path quiz
20 minOpen yesterday's pathquiz.py. It works, but it's one long scroll of repeated code β exactly what functions exist to fix.
Goal: refactor it so the quiz logic lives in functions: ask_question(prompt, answer) returns True/False for one question (case-insensitive comparison inside), verdict(score, total) returns the tier string, and a main() function runs the whole quiz. The bottom of the file should be nothing but the main guard calling main(). Behavior must be identical to yesterday's version.
Constraints: no global anywhere; the score is a local in main() updated from ask_question's return value; every function has a one-line docstring.
Hints (only if stuck): a function returning a bool can be used directly β if ask_question(...): score = score + 1. Test verdict() alone in the REPL by importing it: that's the payoff of the guard.
Your toolbox module
Grow toolbox.py into a real personal module you will import for weeks. It must contain at least five functions, each with a docstring and a return value (no print-only functions): roll(sides=6), today_stamp(), days_until_done(current_day), clamp(value, low, high) (returns value pinned into the range), and one function of your own invention. Add a demo block under if __name__ == "__main__": that exercises all five. Prove it works both ways: run it directly, then write use_toolbox.py that imports it and uses two functions without triggering the demo. Log in journal.md which function was hardest and why.
Common mistakes & misconceptions
- Confusing printing with returning. print() shows a value to a human; return hands it to the calling code. A function that only prints cannot be tested, composed, or reused β and a forgotten return means the caller silently receives None.
- Assigning to a global inside a function and expecting it to change. Assignment creates a local; the global is shadowed, not updated. Pass values in as parameters and return the result instead.
- Using a mutable default like def add(item, bucket=[]). Defaults are evaluated ONCE at definition time, so every call shares the same list. Use bucket=None and create the list inside. (This bites harder after Day 4 β remember it.)
- Putting demo/test code at module top level with no main guard, so it runs on every import. Guard it with if __name__ == "__main__".
- Writing one 80-line function instead of several small ones. If you can't name what a function does in one short sentence, it's doing too much β split it.
- Naming a script after a stdlib module (random.py, math.py). Your file shadows the real module and imports break with confusing errors. Check names before saving.
Q1. def double(x): print(x * 2) β then y = double(4). What is y?
Q2. score = 10 at module level. Inside a function you write score = score + 1 and get UnboundLocalError. Why?
Q3. What does if __name__ == "__main__": accomplish?
Go deeper β curated resources
- Keyword-only and positional-only parameters β The / and * markers in signatures restrict how arguments may be passed β you will see them all over stdlib docs. Skim the "Special parameters" section of the functions tutorial once.
- All three guided scripts run; the None-from-print experiment explained in a comment
- pathquiz.py refactored: main() + guard, no globals, identical behavior
- toolbox.py has 5 documented returning functions and imports cleanly from use_toolbox.py
- Quiz β₯ 2/3 (reread the scope section and retake if lower)
β Back: Day 2's quiz project becomes today's refactoring material β the same behavior, restructured into named, reusable pieces. The TypeError-with-NoneType you triggered traces straight back to Day 1's error-reading ritual.
Forward β: toolbox.py grows for weeks and gets packaged properly on Day 17. Functions with clear inputs and returns are exactly what Day 18 tests, what Day 41 turns into API endpoints, and what Day 111 hands to an LLM as tools. Day 12 revisits functions as VALUES you can wrap and pass around.
Unlocks: D4 Collections Β· D7 Week 1 Checkpoint: CLI Task Tracker Β· D9 Object-Oriented Python I Β· D12 Closures, Decorators & Functional Style