How real scripts are actually organized — small named pieces that call each other, not one long block of code.
A function is a named, reusable block of code. You write it once, then run it by name as many times as you want, instead of retyping the same lines.
def greet():
print("Hello!")
greet() # Hello!
Write a function called wave that prints "Hi there!". Call it.
Hi there!Write a function shout that prints "LOUD!". Call it twice in a row.
LOUD! twiceWrite a function that prints your favorite number. Call it.
Write a function line that prints "---". Call it, then call print("done") after — confirm the order.
--- then doneDefine a function but don't call it. What prints?
Open problems/01_what_is_a_function.py and do all 5 problems locally.
A parameter is a placeholder name inside a function's definition — a hole the function expects to be filled in each time it's called.
def greet(name):
print(f"Hello, {name}!")
Write a function greet_dept(dept) that prints f"Department: {dept}". Don't call it yet — just define it.
Write a function double(n) that prints n + n.
Write a function with two parameters, first and last, that prints them combined with a space.
Write a function show_status(status) that prints f"Status: {status}".
Write a function with zero parameters and one with one parameter — notice the empty () vs a named one inside.
Open problems/02_parameters.py and do all 5 problems locally.
An argument is the actual value you hand in when you call. Think of a call as a pause: your code stops at that line, the function runs start to finish, then control comes back to right where it paused.
def greet(name):
print(f"Hello, {name}!")
greet("Amy") # Hello, Amy!
def greet(name): print(f"Hello, {name}!"). Call it with "Sam".
Hello, Sam!Same function. Call it three times with three different names.
def double(n): print(n + n). Call it with 5, then with 10.
10 then 20Print "before", call a function that prints "during", then print "after" — confirm the pause order.
before\nduring\nafterWrite a two-parameter function show_pair(a, b) that prints both. Call it with "IT" and 5.
IT 5 (or similar, your format)Open problems/03_arguments.py and do all 5 problems locally.
return hands a value back to whatever called the function, instead of (or as well as) printing. The caller can then store that value or use it right away.
def add(a, b):
return a + b
result = add(2, 3)
print(result) # 5
def add(a, b): return a + b. Call it with 4, 6, store the result, print it.
10Write double(n) that returns n + n. Call it with 4, print the result.
8Write a function that returns True if a number is 0, else False (no if needed — just return n == 0). Call it with 0 and with 5.
True then FalseWrite a function that returns a list [1, 2, 3]. Call it, print the result, print its len().
[1, 2, 3] then 3Call add(1, 2) directly inside a print() — no variable needed.
3Open problems/04_return_values.py and do all 5 problems locally.
fetch_active_users()Now combine everything: a parameter, a loop, a filter, and a return — the first function that does real work on the fixture.
def fetch_active_users(users):
active = []
for u in users:
if u["status"] == "ACTIVE":
active.append(u)
return active
Call fetch_active_users(USERS), store the result, print its len().
8Print the first result's id.
00u001Write a similar function fetch_suspended_users(users) for SUSPENDED status. Call it, print the count.
2Call fetch_active_users on just a slice, USERS[0:3]. Print the count.
2 (record 3, Bina, is SUSPENDED — not all 3 are ACTIVE)Call fetch_active_users(USERS), then loop the result printing each email — combines sub-skills 1–4.
Open problems/05_fetch_active_users.py — 5 problems, #5 is a checkpoint combining sub-skills 1–4.
return?If a function never hits a return, calling it gives you back None — silently, no error at that point. The trouble shows up later, wherever you try to use that None.
def fetch_active_users_broken(users): — same body as before, but the return active line is deleted. What does print(fetch_active_users_broken(USERS)) show?
NoneThe function still filters and builds the list — it just never hands it back. Nothing crashes yet.
Now pass that None into count_by_dept(active) (which loops for u in users:). What happens?
TypeError: 'NoneType' object is not iterableThe real bug (forgetting return) happened in one function. The crash happened in a completely different one, several lines later. This gap between where a bug is caused and where it's finally noticed is the single most important idea in the next module.
Write a function that's supposed to return a number but has no return. Call it, print the result.
NonePrint result is None for that same call.
TrueWrite a function with return only inside an if — call it in a way that skips the if. What comes back?
None — no return ranOpen problems/06_missing_return.py and do all 3 problems locally.
A variable created inside a function only exists inside that function — it's local. A variable created outside is global, visible everywhere, but a function's own local variables don't leak back out.
x = "outside"
def show():
x = "inside"
print(x)
show() # inside
print(x) # outside -- unchanged
Run the example above yourself. Confirm both lines print what's shown.
inside then outsideCreate a variable inside a function, print it inside, then try printing it right after the call — outside the function. What happens?
NameError, it doesn't exist out thereCreate a parameter named count inside a function, and a completely separate global variable also named count. Print both, confirm they don't collide.
A function with a parameter users — is that parameter local or global to the function?
Write two separate functions that each use a local variable named total for something different. Confirm they don't interfere with each other.
Open problems/07_scope.py and do all 5 problems locally.
Building a count-by-key dict inside a function has a classic trap: counts[dept] = counts[dept] + 1 crashes the very first time a key shows up, since it isn't in the dict yet. .get(dept, 0) fixes it.
counts = {}. Inside a loop over USERS, counts[dept] = counts[dept] + 1 for each department. What happens?
KeyError: 'IT'The very first department it sees isn't in counts yet — you can't add 1 to something that doesn't exist.
Fix it: use counts[dept] = counts.get(dept, 0) + 1 instead. Run the full loop over USERS. Print counts.
{'IT': 3, 'Security': 2, 'Sales': 3, 'Engineering': 3, 'Unknown': 1}Write this as a function count_by_dept(users) that returns the dict. Call it, print the result.
Call count_by_dept on an empty list. What comes back?
{} — no crashWrite a similar accumulator counting by status instead of department. Print the result.
{'ACTIVE': 8, 'SUSPENDED': 2, 'DEPROVISIONED': 2}Confirm your department counts from Lab 1 add up to len(USERS).
True (sums to 12)Open problems/08_accumulator_bug.py and do all 5 problems locally.
A function that only prints (a side effect) and never returns anything useful gives you back None if you try to capture its result. That's fine — as long as you don't accidentally rely on the return value of a function that was only ever meant to print.
def print_report(counts):
for dept, count in counts.items():
print(f"{dept}: {count}")
result = print_report({"IT": 3}) # prints "IT: 3"
print(result) # None -- print_report never returns anything
Run the example above. Confirm result really is None.
NoneWrite a print-only function for a status report (loop, print each line, no return). Call it with counts from the accumulator sub-skill.
Write build_report(counts) that instead returns a single combined string (no printing inside the function) — a "do a thing" function turned into a "give me a value" function.
Call your build_report from Lab 3, then print() the result yourself, outside the function.
Which function is easier to reuse in a different context — one that prints internally, or one that returns a string? Write one sentence explaining why (as a comment, not printed).
Open problems/09_side_effect_functions.py and do all 5 problems locally.
Real scripts are built from a chain of small functions, tied together by one main() that calls them in order: fetch the data, analyze it, report it. This shape — main → fetch → analyze → report — is the spine of almost every script you'll write from here on.
def fetch_active_users(users):
return [u for u in users if u["status"] == "ACTIVE"]
def count_by_dept(users):
counts = {}
for u in users:
dept = u["profile"].get("department", "Unknown")
counts[dept] = counts.get(dept, 0) + 1
return counts
def print_report(counts):
for dept, count in counts.items():
print(f"{dept}: {count}")
def main():
active = fetch_active_users(USERS)
counts = count_by_dept(active)
print_report(counts)
if __name__ == "__main__":
main()
if __name__ == "__main__": means "only run main() if this file was run directly, not imported." Mention-level only for now — the full reason comes later.
Trace it: at the line active = fetch_active_users(USERS), what type and length does active hold?
At counts = count_by_dept(active), what type does counts hold, and how many keys?
What does print_report(counts) return, and does main() do anything with that return value?
None; main() ignores it — it's called only for its side effectRun the whole chain yourself, from fetch_active_users through main(). Confirm the printed output.
IT: 2\nSecurity: 2\nEngineering: 3\nUnknown: 1Write your own 4-function chain: fetch (filter SUSPENDED users instead), analyze (count by department), report (print), main (ties them together). Run it.
Open problems/10_full_chain.py — 5 problems, #5 is the final checkpoint for all of Module 7.
Run python check.py in problems/ to grade everything.