← Course Home
Module 7

Functions & the Chain

How real scripts are actually organized — small named pieces that call each other, not one long block of code.

Concept

What a Function Is

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!
Lab 1

Write a function called wave that prints "Hi there!". Call it.

Hi there!
Lab 2

Write a function shout that prints "LOUD!". Call it twice in a row.

LOUD! twice
Lab 3

Write a function that prints your favorite number. Call it.

Lab 4

Write a function line that prints "---". Call it, then call print("done") after — confirm the order.

--- then done
Lab 5

Define a function but don't call it. What prints?

Nothing — defining a function doesn't run it, calling it does
Now go practice

Open problems/01_what_is_a_function.py and do all 5 problems locally.

Concept

Defining a Function With Parameters

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}!")
Lab 1

Write a function greet_dept(dept) that prints f"Department: {dept}". Don't call it yet — just define it.

no output — defining, not calling
Lab 2

Write a function double(n) that prints n + n.

no output yet — still just defining
Lab 3

Write a function with two parameters, first and last, that prints them combined with a space.

Lab 4

Write a function show_status(status) that prints f"Status: {status}".

Lab 5

Write a function with zero parameters and one with one parameter — notice the empty () vs a named one inside.

Now go practice

Open problems/02_parameters.py and do all 5 problems locally.

Concept

Calling a Function With Arguments

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!
Lab 1

def greet(name): print(f"Hello, {name}!"). Call it with "Sam".

Hello, Sam!
Lab 2

Same function. Call it three times with three different names.

three greetings
Lab 3

def double(n): print(n + n). Call it with 5, then with 10.

10 then 20
Lab 4

Print "before", call a function that prints "during", then print "after" — confirm the pause order.

before\nduring\nafter
Lab 5

Write a two-parameter function show_pair(a, b) that prints both. Call it with "IT" and 5.

IT 5 (or similar, your format)
Now go practice

Open problems/03_arguments.py and do all 5 problems locally.

Concept

Return Values

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
Lab 1

def add(a, b): return a + b. Call it with 4, 6, store the result, print it.

10
Lab 2

Write double(n) that returns n + n. Call it with 4, print the result.

8
Lab 3

Write 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 False
Lab 4

Write a function that returns a list [1, 2, 3]. Call it, print the result, print its len().

[1, 2, 3] then 3
Lab 5

Call add(1, 2) directly inside a print() — no variable needed.

3
Now go practice

Open problems/04_return_values.py and do all 5 problems locally.

Concept

A Real Function: 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
Lab 1

Call fetch_active_users(USERS), store the result, print its len().

8
Lab 2

Print the first result's id.

00u001
Lab 3

Write a similar function fetch_suspended_users(users) for SUSPENDED status. Call it, print the count.

2
Lab 4

Call fetch_active_users on just a slice, USERS[0:3]. Print the count.

2 (record 3, Bina, is SUSPENDED — not all 3 are ACTIVE)
Lab 5 · Checkpoint

Call fetch_active_users(USERS), then loop the result printing each email — combines sub-skills 1–4.

8 emails
Now go practice

Open problems/05_fetch_active_users.py — 5 problems, #5 is a checkpoint combining sub-skills 1–4.

Concept

What Happens Without 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.

Predict first

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?

Check
None

The function still filters and builds the list — it just never hands it back. Nothing crashes yet.

Predict first

Now pass that None into count_by_dept(active) (which loops for u in users:). What happens?

Check
CrashesTypeError: 'NoneType' object is not iterable

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

Lab 1

Write a function that's supposed to return a number but has no return. Call it, print the result.

None
Lab 2

Print result is None for that same call.

True
Lab 3

Write a function with return only inside an if — call it in a way that skips the if. What comes back?

None — no return ran
Now go practice

Open problems/06_missing_return.py and do all 3 problems locally.

Concept

Local vs. Global Scope

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
Lab 1

Run the example above yourself. Confirm both lines print what's shown.

inside then outside
Lab 2

Create a variable inside a function, print it inside, then try printing it right after the call — outside the function. What happens?

CrashesNameError, it doesn't exist out there
Lab 3

Create a parameter named count inside a function, and a completely separate global variable also named count. Print both, confirm they don't collide.

Lab 4

A function with a parameter users — is that parameter local or global to the function?

Local
Lab 5

Write two separate functions that each use a local variable named total for something different. Confirm they don't interfere with each other.

Now go practice

Open problems/07_scope.py and do all 5 problems locally.

Concept

The Accumulator Trap

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.

Predict first

counts = {}. Inside a loop over USERS, counts[dept] = counts[dept] + 1 for each department. What happens?

Check
CrashesKeyError: 'IT'

The very first department it sees isn't in counts yet — you can't add 1 to something that doesn't exist.

Lab 1

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}
Lab 2

Write this as a function count_by_dept(users) that returns the dict. Call it, print the result.

same dict as Lab 1
Lab 3

Call count_by_dept on an empty list. What comes back?

{} — no crash
Lab 4

Write a similar accumulator counting by status instead of department. Print the result.

{'ACTIVE': 8, 'SUSPENDED': 2, 'DEPROVISIONED': 2}
Lab 5

Confirm your department counts from Lab 1 add up to len(USERS).

True (sums to 12)
Now go practice

Open problems/08_accumulator_bug.py and do all 5 problems locally.

Concept

Return a Value, or Do a Thing — Not Both

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
Lab 1

Run the example above. Confirm result really is None.

None
Lab 2

Write a print-only function for a status report (loop, print each line, no return). Call it with counts from the accumulator sub-skill.

printed lines, function itself returns nothing
Lab 3

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.

Lab 4

Call your build_report from Lab 3, then print() the result yourself, outside the function.

the combined string, printed once
Lab 5

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

Now go practice

Open problems/09_side_effect_functions.py and do all 5 problems locally.

Concept

The Full Chain

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.

Lab 1

Trace it: at the line active = fetch_active_users(USERS), what type and length does active hold?

a list, length 8
Lab 2

At counts = count_by_dept(active), what type does counts hold, and how many keys?

a dict, 4 keys (IT, Security, Engineering, Unknown — only among ACTIVE users)
Lab 3

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 effect
Lab 4

Run the whole chain yourself, from fetch_active_users through main(). Confirm the printed output.

IT: 2\nSecurity: 2\nEngineering: 3\nUnknown: 1
Lab 5 · Final checkpoint

Write your own 4-function chain: fetch (filter SUSPENDED users instead), analyze (count by department), report (print), main (ties them together). Run it.

This is the big one — take your time.
Now go practice

Open problems/10_full_chain.py — 5 problems, #5 is the final checkpoint for all of Module 7.

Complete

Module 7 done

what a function is → parameters → arguments (the pause) → return values → real fetch function → missing-return bug → scope → accumulator trap → side-effect-only functions → the full chain.

Run python check.py in problems/ to grade everything.