Lesson 3: functions that take more than one thing
Three short sessions, building on lesson 1’s parity recipe
My drawing, made to talk with Runway Act-Two. Later in the course: how.
Lesson 1 gave you def and return with one input (parity). Lesson 2 gave you while, input() and f-strings. This lesson uses all of them. What is new: a function’s name, several inputs, how to pass those inputs (by position or by name), the difference between return and print, one function that calls another, and a function that wraps a call to Claude.
Name it, then call it (15 min session)
Left pane, you work. Right pane, Claude is your tutor.
A function is a name for a computation
A function is a name for a computation: a recipe, a process, a rule you want to use again without writing it out again. Naming it well is the most important habit in this lesson. A good name says what the step is for, so the reader does not have to work out how it was made.
Lesson 1’s recipe took one input, n, and gave back a word. A recipe usually needs more than one input: the amount, the servings it was written for, and the servings you want. The three names in the parentheses after def are parameters: the boxes the function fills when you call it.
Type this into ~/course/scale.py. You will keep using this file:
def scaled(amount, base_servings, servings):
return amount * servings / base_servings
print(f"Flour: {250 * 6 / 2} g")
print(f"Flour: {scaled(250, 2, 6)} g")Both lines print 750.0 g. The first only says how the number was made; the second says what it means.
To check your understanding, ask Claude in the right pane: “Suggest three names for a function that turns a recipe’s base amount into the amount for any number of servings. Which name is best, and why?” Guess its top pick first.
Names or positions for the inputs
A call can pass its inputs in two ways:
- by position:
scaled(250, 2, 6). Each value goes to the next parameter, in order. - by name:
scaled(amount=250, base_servings=2, servings=6). Each value goes to the parameter with that name, in any order.
Use names when a wrong order would still run and still give an answer. Here the program does not stop when you swap two numbers. It prints 83.33333333333333 instead of 750.0, and nothing tells you it is wrong. The names make that mistake hard to make.
Use positions when the order is obvious and the call is short. parity(7) from lesson 1 has one input, so there is no order to get wrong.
Rule of thumb: if the reader would need to look up the function to know which value is which, name the inputs.
Which would you use, names or positions, for scaled with its three numbers, and for parity with its one? Then check your answers against the rule above.
A recipe that scales (20 min session)
Now name the inputs in every call. Keep scale.py from the start of the lesson, and add these lines to it:
print(f"{scaled(amount=250, base_servings=2, servings=4)} g flour")
print(f"{scaled(amount=250, base_servings=2, servings=1)} g flour")Guess what the first line prints. Note that / always gives a decimal (lesson 1), so check the second line too.
To check your understanding: what does scaled(amount=250, base_servings=4, servings=2) print, and why is it different from the first line? Decide before you read on.
It prints 125.0. The recipe is now written for 4 servings, and you want 2, so the amount halves. Moving the names around changes nothing; changing the numbers does. Run it, or ask Claude in the right pane why.
Write a second function, flour_for, that calls scaled with 250 grams for 2 servings and a number of servings you choose as its one input. It should return the amount of flour. Guess its output before you run it.
Reuse with input in the middle
One function can serve many ingredients, and the number can come from the user while the program is running. Add these lines to scale.py, below scaled:
servings = int(input("How many servings? "))
print(f"Flour: {scaled(amount=250, base_servings=2, servings=servings)} g")
print(f"Sugar: {scaled(amount=100, base_servings=2, servings=servings)} g")Guess the two lines for 6, then run it and check. Then type 1. You wrote the rule once, in scaled, and it worked for two ingredients, with a number the program didn’t know until you typed it.
Bug hunt: a recipe that prints instead
Claude wrote this version. It has one bug. Type it in scale_bug.py, run it, and find the bug. Claude will not say where it is.
def scaled(amount, base_servings, servings):
print(amount * servings / base_servings)
total = scaled(amount=250, base_servings=2, servings=4) + 100
print(f"total: {total}")For the bug:
- Write down the error message.
- Guess what is wrong, in one sentence.
- Fix it, run it, and check the output by hand.
Then tell Claude, in the right pane, what you found, and ask only why the error happens.
Functions that call functions (25 min session)
A function can call another function, the same way your program calls input and print. Open ~/course/frame.py and type this:
def perimeter(width, height):
return 2 * width + 2 * height
def frame_cost(width, height, price_per_cm):
return perimeter(width, height) * price_per_cm
print(f"Frame: {frame_cost(width=40, height=50, price_per_cm=3)} euros")Guess perimeter(40, 50) on its own, then what the last line prints. Run it and check.
To check your understanding, ask Claude in the right pane: “Where does frame_cost get the perimeter from, and why is that useful?” Guess the gist of its answer first.
The answer: it comes from perimeter. If the rule for a perimeter changes, you change it in one place, and frame_cost still works.
Python runs a function body only when the function is called, but the functions must be defined above the line that calls them. Try moving print(...) above def perimeter. What error do you expect? Then run it.
AI thread: wrap the call in a function
Each time your program talks to Claude, the same lines repeat. Put them in one function, so the rest of the program only asks a question and gets a reply. Add this to frame.py, above the prints. Guess what the last line prints, then run it.
import subprocess
def ask(question):
reply = subprocess.run(
["claude", "-p", question, "--tools", "",
"--no-session-persistence"],
capture_output=True, text=True)
return reply.stdout.strip()
print(ask("Reply with only the word hello."))ask is the one place the program talks to Claude. If the call needs to change, for example a new flag, you change it here and nowhere else. ask returns the reply and does not print it: the caller decides what to do with it.
A function that calls ask
Lesson 2’s shape program asked Claude for a turn angle, then turned that text into a number. Put that step in a function, and it can be reused by any program that needs an angle:
def angle_for(shape):
reply = ask(f"How many degrees does a turtle turn at each corner of a {shape}? Reply with only the number.")
return float(reply)
print(f"Pentagon turn: {angle_for('pentagon')} degrees")
sides = round(360 / angle_for("pentagon"))
print(f"Sides: {sides}")Guess what the first line prints, and what sides comes to. The first call to angle_for has the whole chain behind it: angle_for calls ask, and ask calls Claude.
To check your understanding, ask Claude in the right pane: “Why does angle_for call ask instead of calling subprocess itself?” Guess the gist of its answer first.
If Claude’s reply is a sentence, float fails inside angle_for, and the error points at float, not at the question. A function that hides a step also hides where it went wrong, so read the traceback to the bottom to see which function failed.
Write your own program
Write a program of your own with at least two functions, one of which calls the other. Claude writes none of it.
Pick a theme from cooking, art, music, or fashion, and build something that takes at least two inputs, returns a value, and prints a line at the end in your theme’s words. One of your functions must ask Claude for a reply and return it, using ask from the AI thread above.
Simple ideas to start from, pick one:
- a function that prices a frame from its width and height, and a second one that adds tax;
- a function that scales a dough or a paint mix for any number of batches;
- a function that asks Claude for a short name for any number you give it, and returns the name.
Before you run it, guess the output for two different sets of inputs. Name the inputs in any call where a wrong order would still run.
When it works, walk Claude through it, line by line. Claude will ask why each function takes the inputs it takes, and where it could fail, for example with a zero or a negative number. Claude will not fix anything.