Session 10 - Functions, Deeper (Scope & Design)

Session 10 of the Python Fundamentals series, covered in depth. It designs programs out of small functions, covering local variables and scope, parameters as local names, reading outer variables, and keeping one job per function. It then covers composition, returning several values as a tuple and unpacking them, and builds a menu program in which each option is its own function. The traps are using a local variable outside its function, which raises a NameError, and expecting a reassigned parameter to change the caller's variable. Every snippet and error message was copied verbatim from CPython 3.12.

Subject: Python Fundamentals · 95 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Functions, Deeper

Title

Python Fundamentals - Session 10

One job per function - scope, composition, and clean design

2. What you will be able to do

Objectives

Functions take inputs and return outputs. Now we combine many small ones into a real program. By the end you can:

  1. Explain that variables made inside a function are local to it.
  2. Read an outer variable, and know why a local one is invisible outside.
  3. Design small, single-job functions and compose them.
  1. Return several values at once and unpack them.
  2. Build a menu program where each option is its own function.
  3. Avoid the 'local variable outside' and 'reassigned parameter' traps.

3. What survived from Session 9 - Functions?

Warm-up

Discussion prompt

Before we open Session 10 - Functions, Deeper (Scope & Design): without looking back, what was the main idea of Session 9 - Functions, and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

That session covers def, parameters, return against print, the None you get from a function with no return, default and keyword arguments, composition, and refactoring.

4. Local Variables & Scope

Section

Part 1

5. Variables inside a function are local

Concept

A variable created inside a function exists only inside that function. When the function ends, it is gone.

scope — Where a name is visible. A variable made inside a function has local scope - it cannot be seen from outside.

6. Break it if you can: Variables inside a function are local

Counterexample

Discussion prompt

A variable created inside a function exists only inside that function. When the function ends, it is gone.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

7. Restore the missing line: A local variable at work

Fill the middle

Fill in the blanks

From A local variable at work — one line has had its right-hand side removed. Put it back.

def f():
msg = "inside"
print(msg)

f()

Why: msg is what everything below it consumes, so the wrong expression here fails later and somewhere else. It is created when f runs and used right there.

8. A local variable at work

Worked example

def f():
    msg = "inside"
    print(msg)

f()

msg lives inside f

Why: It is created when f runs and used right there.

Read the output

Why: Verified by execution: prints inside. msg does not exist anywhere else.

namewhere it exists
msginside f only

9. Inspect it line by line: A local variable at work

Error analysis

Annotate

Walk the callouts on A local variable at work. Each one is a place this is easy to get subtly wrong.

  • It is created when f runs and used right there.
  • Verified by execution: prints inside. msg does not exist anywhere else.

10. Something is wrong here: using a local outside

Anomaly

Predict first

A student writes this, and it looks reasonable:

Trying to read a function's local variable after the call.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: It was local to f. Outside, the name is unknown, so Python raises a NameError.

return it so the outside can have it.

Why: It was local to f. Outside, the name is unknown, so Python raises a NameError.

11. Trap: using a local outside

Trap

The trap

Trying to read a function's local variable after the call.

def f():
    secret = 1

f()
print(secret)

secret is gone once f returns

Why: It was local to f. Outside, the name is unknown, so Python raises a NameError.

you writeresult
print(secret)NameError: name 'secret' is not defined

The fix

return it so the outside can have it.

def f():
    secret = 1
    return secret

value = f()
print(value)

return carries the value out

Why: The caller stores it in value. Real output: 1. A return is the door a local value leaves through.

you writeprints
print(value)1

12. Break it on purpose: using a local outside

Break the constraint

Discussion prompt

The rule this trap just fixed:

The caller stores it in value. Real output: 1. A return is the door a local value leaves through.

Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?

Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.

Answer:

It was local to f. Outside, the name is unknown, so Python raises a NameError.

13. Parameters are local too

Concept

The parameters in a def are local names, created fresh for each call and gone when it ends.

So def add(a, b) cannot leak a or b to the rest of the program.

14. By analogy: Parameters are local too

Analogy

Discussion prompt

Explain Parameters are local too by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

The parameters in a def are local names, created fresh for each call and gone when it ends.

15. Each call gets its own workspace

Intuition

Picture each function call setting up a private desk. Its local variables live on that desk and are cleared away when the call finishes.

This is a feature: two functions can both use a variable named i without interfering, because each has its own desk.

16. Teach it back: Each call gets its own workspace

Explain it

Discussion prompt

Explain Each call gets its own workspace to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

Picture each function call setting up a private desk. Its local variables live on that desk and are cleared away when the call finishes.

17. A local name shadows the outside

Concept

If a function assigns to a name that also exists outside, it makes a new local one. Changing it does not touch the outer variable.

18. Why is this step legal: The outer x is untouched

Explain it to yourself

Discussion prompt

In Inner x, outer x this move is made:

The outer x is untouched

Why is that legal? Name the rule or definition it rests on before you read on.

Hint: If you can only say "because that is what you do", the rule is the thing to go and find.

Answer:

Verified by execution: inner: 99, then outer: 10. Two different x's.

19. Inner x, outer x

Worked example

x = 10
def g():
    x = 99
    print("inner:", x)

g()
print("outer:", x)

The x inside g is its own local

Why: Assigning x = 99 inside g creates a separate local x.

The outer x is untouched

Why: Verified by execution: inner: 99, then outer: 10. Two different x's.

wherex
inside g99
outside10

20. Fill in: x for Inner x, outer x

Comparison

Comparison matrix

From Inner x, outer x: refill the x column from what you know. The rest of the table is as it appeared.

wherex
inside g99
outside10

21. Functions can read outer variables

Concept

A function may read a variable from the surrounding program if it does not assign to it locally.

Still, passing values in as parameters is clearer than reaching out to outside variables - it makes the function self-contained.

22. Reading an outer value

Worked example

total = 5
def show_total():
    print("reads outer:", total)

show_total()

show_total reads total from outside

Why: It never assigns total, so Python looks outward and finds 5.

Read the output

Why: Verified by execution: reads outer: 5. Prefer a parameter for this - it is easier to test.

total (outer)prints
5reads outer: 5

23. Draw the shape of it: Reading an outer value

Blank canvas

Draw it

Draw what Reading an outer value just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

24. One Job per Function

Section

Part 2

25. Each function should do one thing

Concept

A good function has a single, clear job. If you catch yourself joining two jobs with 'and', that is two functions.

Small functions are easier to name, test, and reuse.

26. The name reveals the job

Concept

ask_number, average, is_passing - each name tells you exactly what its function does and returns.

If a name needs 'and' (read_and_average_and_print), split it.

27. Finish it with less help: Split reading from computing

Faded example

Fill in the blanks

Split reading from computing, with the scaffolding fading: two lines are gone now — fill both.

def ask_number(prompt):
return int(input(prompt))

def average(a, b):
return (a + b) / 2

x = ask_number("First: ")
y = ask_number("Second: ")
print(average(x, y))

Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. ask_number handles input; average handles the math.

28. Split reading from computing

Worked example

def ask_number(prompt):
    return int(input(prompt))

def average(a, b):
    return (a + b) / 2

x = ask_number("First: ")
y = ask_number("Second: ")
print(average(x, y))

One function reads, one computes

Why: ask_number handles input; average handles the math. Each is simple.

The main code just wires them together

Why: Verified by execution: typing 8 and 4 prints 6.0. Small pieces, clear flow.

functionits one job
ask_numberread a number
averagecompute the mean

29. What each one costs: Split reading from computing

Trade off

Comparison matrix

From Split reading from computing: every row here is a choice with a cost. Fill the its one job column, then say which row you would actually pick and what you give up for it.

functionits one job
ask_numberread a number
averagecompute the mean

30. Small tools that snap together

Intuition

Think of functions as building blocks. Each does one job well; you snap them together to build something bigger.

A big block that does everything is hard to reuse. Small blocks fit many projects.

31. Helper functions

Concept

A helper is a small function used by other functions. It hides a fiddly detail behind a clear name.

For example, a clean(text) helper that trims and lowercases input, used wherever you compare user text.

32. Functions Calling Functions

Section

Part 3

33. Build big from small

Concept

A function's body can call other functions. Complex behavior becomes a short list of well-named calls.

34. One function using another

Worked example

def average(a, b):
    return (a + b) / 2

def midpoint_label(a, b):
    return "midpoint is " + str(average(a, b))

print(midpoint_label(8, 4))

midpoint_label calls average

Why: It reuses average instead of re-deriving the math.

Read the output

Why: Verified by execution: midpoint is 6.0.

callusesreturns
midpoint_label(8, 4)average(8, 4) = 6.0midpoint is 6.0

35. A helper chain

Worked example

def clean(s):
    return s.strip().lower()

def is_yes(s):
    return clean(s) == "yes"

print(is_yes("  YES "))

is_yes leans on clean

Why: clean trims spaces and lowercases; is_yes just compares the cleaned text to "yes".

Read the output

Why: Verified by execution: ' YES ' cleans to 'yes', so is_yes is True.

inputclean(input)is_yes
YES yesTrue

36. Where does each piece belong: Session 10 - Functions, Deeper (Scope &…

Sorting

Sort into buckets

These are the pieces of Session 10 - Functions, Deeper (Scope & Design), out of order. Put each one back under the part of the lesson it belongs to.

Local Variables & Scope
Variables inside a function are local; A local variable at work; Parameters are local too
One Job per Function
Each function should do one thing; The name reveals the job; Split reading from computing
Functions Calling Functions
Build big from small; One function using another; A helper chain
s1
Local Variables & Scope is where Session 10 - Functions, Deeper (Scope & Design) puts Variables inside a function are local, A local variable at work, Parameters are local too. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
One Job per Function is where Session 10 - Functions, Deeper (Scope & Design) puts Each function should do one thing, The name reveals the job, Split reading from computing. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Functions Calling Functions is where Session 10 - Functions, Deeper (Scope & Design) puts Build big from small, One function using another, A helper chain. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

37. Read composition inside-out

Concept

str(average(a, b)) runs the inner call first: compute the average, then convert it to text.

When calls nest, work from the innermost parentheses outward - just like arithmetic.

38. Returning More than One

Section

Part 4

39. Return several values

Concept

Separate values with commas in a return: return min(nums), max(nums). The function hands back both, as a pair.

40. min and max together

Worked example

def min_max(nums):
    return min(nums), max(nums)

print(min_max([3, 9, 1, 7]))

Two values come back as a pair

Why: min is 1, max is 9, returned together.

Read the output

Why: Verified by execution: (1, 9) - a tuple holding both.

callreturns
min_max([3, 9, 1, 7])(1, 9)

41. Unpack the results into names

Concept

Catch the two values in two variables: lo, hi = min_max(nums). Each name gets one of the returned values, in order.

42. Unpacking

Worked example

def min_max(nums):
    return min(nums), max(nums)

lo, hi = min_max([3, 9, 1, 7])
print("low", lo, "high", hi)

lo and hi split the returned pair

Why: lo gets the first (1), hi the second (9).

Read the output

Why: Verified by execution: low 1 high 9.

namegets
lo1
hi9

43. A Menu Program

Section

Part 5

44. One function per menu option

Concept

In a menu program, give each option its own function. The main loop shows the menu and calls the right one.

This keeps each action small and the main loop short and readable.

45. Predict the next row: Menu handlers

Pattern

Predict first

The table runs: show_menu | print options · greet | say hello

In Menu handlers, given the rows so far: what is the next one — the row where function is add?

Correct: add | print 2 + 2

functionjob
show_menuprint options
greetsay hello
addprint 2 + 2

Why: The relationship between the columns, not the individual numbers, is what generates the next row. show_menu prints choices; greet and add each do one thing.

46. Menu handlers

Worked example

def show_menu():
    print("1) Greet  2) Add  3) Quit")

def greet():
    print("Hello!")

def add():
    print(2 + 2)

show_menu()
greet()

Each option is a named function

Why: show_menu prints choices; greet and add each do one thing.

Read the output

Why: Verified by execution: the menu line, then Hello!

functionjob
show_menuprint options
greetsay hello
addprint 2 + 2

47. Watch it run: Menu handlers

Pattern

Step through it

Step through Menu handlers one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: function is show_menu
  2. Step 2: function is greet
  3. Step 3: function is add

48. The main loop dispatches

Concept

A while loop shows the menu, reads the choice, and calls the matching function - then repeats until the user quits.

The loop stays tiny because all the real work is inside the handler functions.

49. Finish it with less help: The dispatch loop

Faded example

Fill in the blanks

The dispatch loop, with the scaffolding fading: two lines are gone now — fill both.

def greet():
print("Hello!")

running = True
while running:
choice = input("1) Greet 3) Quit: ")
if choice == "1":
greet()
elif choice == "3":
running = False

Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. Choice 1 calls greet; choice 3 flips the flag to stop.

50. The dispatch loop

Worked example

The user types 1, then 3:

def greet():
    print("Hello!")

running = True
while running:
    choice = input("1) Greet  3) Quit: ")
    if choice == "1":
        greet()
    elif choice == "3":
        running = False

The loop reads a choice and calls a handler

Why: Choice 1 calls greet; choice 3 flips the flag to stop.

Trace the passes

Why: Verified by execution: prints Hello! then quits.

choiceaction
1greet() -> Hello!
3running = False -> stop

51. Fill in: action for The dispatch loop

Comparison

Comparison matrix

From The dispatch loop: refill the action column from what you know. The rest of the table is as it appeared.

choiceaction
1greet() -> Hello!
3running = False -> stop

52. Refactor: One Job at a Time

Section

Part 6

53. Break a big function into small ones

Concept

If a function reads input, does math, AND prints, split it into a reader, a calculator, and a display step.

Each piece is then testable on its own, and reusable elsewhere.

54. Restore the missing line: Before: one function does it all

Fill the middle

Fill in the blanks

From Before: one function does it all — one line has had its right-hand side removed. Put it back.

def do_everything(a, b):
result = (a + b) / 2
print("The average is", result)
return result

do_everything(8, 4)

Why: result is what everything below it consumes, so the wrong expression here fails later and somewhere else. Three jobs tangled together - hard to reuse the math without also printing.

55. Before: one function does it all

Worked example

def do_everything(a, b):
    result = (a + b) / 2
    print("The average is", result)
    return result

do_everything(8, 4)

It computes, prints, and returns

Why: Three jobs tangled together - hard to reuse the math without also printing.

Read the output

Why: Verified by execution: prints The average is 6.0. It works, but it is not flexible.

jobs in one function
compute + print + return

56. After: small, focused functions

Worked example

def average(a, b):
    return (a + b) / 2

result = average(8, 4)
print("The average is", result)

average just computes; the caller decides to print

Why: Now average is reusable anywhere - in a comparison, a total, or silently.

Read the output

Why: Verified by execution: same result, but the pieces are separate and reusable.

functionone job
averagecompute the mean
(main code)decide to print

57. Pitfalls

Section

Part 7

58. Something is wrong here: reassigning a parameter

Anomaly

Predict first

A student writes this, and it looks reasonable:

Expecting a function to change the caller's variable by reassigning its parameter.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Reassigning n inside only changes the local n.

return the new value and assign it back.

Why: Reassigning n inside only changes the local n. The caller's x is untouched, so it stays 5.

59. Trap: reassigning a parameter

Trap

The trap

Expecting a function to change the caller's variable by reassigning its parameter.

def add_one(n):
    n = n + 1

x = 5
add_one(x)
print(x)

n is a local copy of the value

Why: Reassigning n inside only changes the local n. The caller's x is untouched, so it stays 5.

stepx
before5
after add_one(x)5 (unchanged)

The fix

return the new value and assign it back.

def add_one(n):
    return n + 1

x = 5
x = add_one(x)
print(x)

The caller stores the returned value

Why: x = add_one(x) rebinds x to 6. Real output: 6. To change a value, return it - do not rely on side effects.

stepx
after x = add_one(x)6

60. Prefer parameters and returns over globals

Concept

A function that takes what it needs as parameters and hands back a result is easy to understand and test in isolation.

Reaching out to outside variables or relying on side effects makes functions harder to reuse and reason about.

61. Scope & Design in Practice

Section

Part 8

62. Constants at the top

Concept

Values that never change - a limit, a tax rate - are often set once near the top in ALL_CAPS, then read by functions below.

Reading a constant is a fine use of an outer variable, because it is a fixed setting, not shifting state.

63. Why is this step legal: remaining reads the constant

Explain it to yourself

Discussion prompt

In A shared limit this move is made:

remaining reads the constant

Why is that legal? Name the rule or definition it rests on before you read on.

Hint: If you can only say "because that is what you do", the rule is the thing to go and find.

Answer:

MAX_TRIES is a fixed setting the function can rely on.

64. A shared limit

Worked example

MAX_TRIES = 3

def remaining(used):
    return MAX_TRIES - used

print(remaining(1))

remaining reads the constant

Why: MAX_TRIES is a fixed setting the function can rely on.

Read the output

Why: Verified by execution: 3 - 1 = 2.

usedremaining
12

65. Draw the shape of it: A shared limit

Blank canvas

Draw it

Draw what A shared limit just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

66. A function can build and return a list

Concept

Build a list locally with [] and append, then return it. The caller receives the finished list.

67. Predict the next row: Return a built list

Pattern

Predict first

The table runs: 1 | [2] · 2 | [2, 4] · 3 | [2, 4, 6]

In Return a built list, given the rows so far: what is the next one — the row where i is 4?

Correct: 4 | [2, 4, 6, 8]

iout
1[2]
2[2, 4]
3[2, 4, 6]
4[2, 4, 6, 8]

Why: The relationship between the columns, not the individual numbers, is what generates the next row. The list is built inside, then handed back whole.

68. Return a built list

Worked example

def make_evens(n):
    out = []
    for i in range(1, n + 1):
        out.append(i * 2)
    return out

print(make_evens(4))

out is local; return carries it out

Why: The list is built inside, then handed back whole.

Read the output

Why: Verified by execution: [2, 4, 6, 8].

iout
1[2]
2[2, 4]
3[2, 4, 6]
4[2, 4, 6, 8]

69. What each one costs: Return a built list

Trade off

Comparison matrix

From Return a built list: every row here is a choice with a cost. Fill the out column, then say which row you would actually pick and what you give up for it.

iout
1[2]
2[2, 4]
3[2, 4, 6]
4[2, 4, 6, 8]

70. Methods change a list in place

Concept

A list passed to a function is shared, so a method like .append() on it does affect the caller's list.

This is different from a number: changing a list's contents in place is visible outside, because both names point at the same list.

71. Teach it back: Methods change a list in place

Explain it

Discussion prompt

Explain Methods change a list in place to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

A list passed to a function is shared, so a method like .append() on it does affect the caller's list.

72. Restore the missing line: append inside persists

Fill the middle

Fill in the blanks

From append inside persists — one line has had its right-hand side removed. Put it back.

def add_item(lst, x):
lst.append(x)

items = [1, 2]
add_item(items, 3)
print(items)

Why: items is what everything below it consumes, so the wrong expression here fails later and somewhere else. append changes the shared list's contents in place.

73. append inside persists

Worked example

def add_item(lst, x):
    lst.append(x)

items = [1, 2]
add_item(items, 3)
print(items)

lst and items are the same list

Why: append changes the shared list's contents in place.

The change is visible outside

Why: Verified by execution: prints [1, 2, 3].

stepitems
before[1, 2]
after add_item[1, 2, 3]

74. Fill in: items for append inside persists

Comparison

Comparison matrix

From append inside persists: refill the items column from what you know. The rest of the table is as it appeared.

stepitems
before[1, 2]
after add_item[1, 2, 3]

75. Something is wrong here: reassigning the list parameter

Anomaly

Predict first

A student writes this, and it looks reasonable:

Reassigning the parameter to a new list, expecting the caller to see it.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: It points the local lst at a brand-new list; the caller's r still points at the old one.

Change it in place, or return the new list.

Why: It points the local lst at a brand-new list; the caller's r still points at the old one. Same rule as with numbers.

76. Trap: reassigning the list parameter

Trap

The trap

Reassigning the parameter to a new list, expecting the caller to see it.

def replace(lst):
    lst = [9, 9]

r = [1, 2]
replace(r)
print(r)

lst = [9, 9] rebinds only the local name

Why: It points the local lst at a brand-new list; the caller's r still points at the old one. Same rule as with numbers.

stepr
after replace[1, 2] (unchanged)

The fix

Change it in place, or return the new list.

def replace(lst):
    return [9, 9]

r = [1, 2]
r = replace(r)
print(r)

Return and reassign

Why: r = replace(r) rebinds r to the new list. Real output: [9, 9]. (Or clear-and-extend the same list in place.)

stepr
after r = replace(r)[9, 9]

77. Which of these survive contact with Session 10 - Functions, Deeper (Scope &…?

Two truths and a lie

Sort into buckets

Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.

Holds up
A variable created inside a function exists only inside that function. When the function ends, it is gone.; The parameters in a def are local names, created fresh for each call and gone when it ends.; Picture each function call setting up a private desk. Its local variables live on that desk and are cleared away when the call finishes.
Breaks
Trying to read a function's local variable after the call.; Expecting a function to change the caller's variable by reassigning its parameter.
sound
These are stated as this lesson states them — each one survives the edge cases Session 10 - Functions, Deeper (Scope & Design) puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

78. Change-in-place vs rebind

Intuition

Two names on the same list are two labels on one box. Editing the box's contents (append) is seen by both labels.

But lst = [9, 9] moves only that label to a different box - the other label stays on the original. Rebinding never reaches the caller.

79. By analogy: Change-in-place vs rebind

Analogy

Discussion prompt

Explain Change-in-place vs rebind by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

Two names on the same list are two labels on one box. Editing the box's contents (append) is seen by both labels.

80. Describe what a function does

Concept

A short comment above a function - or a line of text just under the def - explains its purpose to the next reader.

Say what it takes, what it returns, and any surprise. Future you will be grateful.

81. Break it if you can: Describe what a function does

Counterexample

Discussion prompt

Say what it takes, what it returns, and any surprise. Future you will be grateful.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

82. A documented helper

Worked example

def average(a, b):
    # returns the mean of two numbers (a float)
    return (a + b) / 2

print(average(8, 4))

One comment states the job

Why: The reader knows what average does without tracing the math.

Read the output

Why: Verified by execution: 6.0. The comment is for humans; Python ignores it.

functiondocumented job
averagemean of two numbers

83. Inspect it line by line: A documented helper

Error analysis

Annotate

Walk the callouts on A documented helper. Each one is a place this is easy to get subtly wrong.

  • The reader knows what average does without tracing the math.
  • Verified by execution: 6.0. The comment is for humans; Python ignores it.

84. Patterns & Checks

Section

Part 9

85. Designing functions

Pattern

1. One job per function; name it after that job

Why: If the name needs 'and', split it into two.

2. Take inputs as parameters, hand results back with return

Why: Self-contained functions are easy to test and reuse.

3. Compose: let functions call smaller functions

Why: Build big behavior from small, tested pieces; read nested calls inside-out.

4. Return several values with commas, unpack with lo, hi = ...

Why: One call can hand back a pair (or more).

86. Why is this step legal: Made inside a function -> local, invisible outside

Explain it to yourself

Discussion prompt

In Scope rules of thumb this move is made:

Made inside a function -> local, invisible outside

Why is that legal? Name the rule or definition it rests on before you read on.

Hint: If you can only say "because that is what you do", the rule is the thing to go and find.

Answer:

Reading it outside is a NameError; return it to carry it out.

87. Scope rules of thumb

Pattern

Made inside a function -> local, invisible outside

Why: Reading it outside is a NameError; return it to carry it out.

Assigning inside makes a new local (shadowing)

Why: It does not change a same-named outer variable.

Reassigning a parameter does not change the caller

Why: Return the new value and let the caller reassign.

88. Where this shows up: Session 10 - Functions, Deeper (Scope & Design)

Real world

Discussion prompt

Outside this lesson: where does Session 10 - Functions, Deeper (Scope & Design) actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Scope rules of thumb is doing the work in it.

Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.

Answer:

Session 10 of the Python Fundamentals series, in depth. Designing programs from small functions: local variables and scope, parameters as local names, reading outer variables, one-job-per-function, composition (functions calling functions), returning several values as a tuple and unpacking them, and a menu program where each option is its own function.

89. Check: local scope

Check

Is msg visible outside?

def f():
    msg = "hi"

f()
print(msg)
msg existsoutside?
inside f?

Check your understanding

What happens?

  • A. NameError: name 'msg' is not defined (correct)
  • B. It prints hi
  • C. It prints None
  • D. It prints nothing

Answer: A

Why: msg is local to f and gone after the call, so print(msg) outside raises a NameError. Returning msg would carry it out. Verified by execution.

Why B tempts people
hi never leaves f - msg is local, so the outside cannot see it.
Why C tempts people
It is not None; the name msg simply does not exist outside f.
Why D tempts people
It does not silently print nothing - it raises an error.

90. Check: shadowing

Check

Does the inner assignment change the outer x?

x = 1
def g():
    x = 2

g()
print(x)
inner xouter x
2?

Check your understanding

What does this print?

  • A. 1 (correct)
  • B. 2
  • C. None
  • D. Error

Answer: A

Why: Assigning x = 2 inside g creates a separate local x. The outer x stays 1. Verified by execution.

Why B tempts people
The inner x is a different, local variable; it does not change the outer one.
Why C tempts people
The outer x was set to 1 and never removed, so it is 1, not None.
Why D tempts people
This is valid code - shadowing is allowed, not an error.

91. Check: reassigned parameter

Check

Trace x.

def bump(n):
    n = n + 10

x = 5
bump(x)
print(x)
stepx
after bump?

Check your understanding

What does this print?

  • A. 5 (correct)
  • B. 15
  • C. None
  • D. 10

Answer: A

Why: bump reassigns its local n, which does not affect the caller's x. Since bump does not return anything and x is never reassigned, x stays 5. Verified by execution.

Why B tempts people
15 would require returning n + 10 and writing x = bump(x). Reassigning the parameter alone does not change x.
Why C tempts people
x was set to 5 and never overwritten, so it is 5, not None.
Why D tempts people
n + 10 is 15 inside bump, but it never leaves the function; x is unchanged at 5.

92. Check: unpacking

Check

What do a and b get?

def two():
    return 1, 2

a, b = two()
print(b)
ab
1?

Check your understanding

What does this print?

  • A. 2 (correct)
  • B. 1
  • C. (1, 2)
  • D. Error

Answer: A

Why: two returns the pair 1, 2. Unpacking gives a = 1 and b = 2, so print(b) shows 2. Verified by execution.

Why B tempts people
1 is what a gets - the first value. b gets the second value, 2.
Why C tempts people
Unpacking splits the pair into a and b, so b is a single value, not the whole tuple.
Why D tempts people
Returning and unpacking two values is valid - no error.

93. Check: composition

Check

Work inside-out.

def dbl(n):
    return n * 2

def inc(n):
    return n + 1

print(inc(dbl(3)))
dbl(3)inc(...)
??

Check your understanding

What does this print?

  • A. 7 (correct)
  • B. 8
  • C. 6
  • D. 9

Answer: A

Why: Inner call first: dbl(3) is 6. Then inc(6) is 7. Verified by execution.

Why B tempts people
8 would be dbl(inc(3)) = dbl(4). Here inc wraps dbl, not the other way around.
Why C tempts people
6 is only dbl(3); the outer inc still adds 1.
Why D tempts people
9 does not match either order; inc(dbl(3)) is 6 + 1 = 7.

94. Connect it up: Session 10 - Functions, Deeper (Scope & Design)

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — Local Variables & Scope · One Job per Function · Functions Calling Functions · Returning More than One · A Menu Program · Refactor: One Job at a Time. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

95. What you can do now

Recap

Design programs from small, single-job functions. Variables inside are local; carry results out with return. Compose small functions into big behavior.

IdeaIn short
scopemade inside = local, invisible outside
shadowinginner assignment makes a new local
compositionfunctions call smaller functions
multiple returnsreturn a, b then lo, hi = f()
change a valuereturn it; do not reassign a parameter

One job per function, named for that job. Next session we sharpen a tool you have used all along: strings - cleaning and shaping text.

Sources

  1. Python 3 Tutorial - Defining Functions & Scopes
  2. Python 3 Reference - Naming and binding (scope)
  3. All snippets and error messages executed and copied from CPython 3.12. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 10).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108