Imagine making a greeting card for everyone in your group. Copy-pasting one card over and over is slow, and one change means many repairs.
Today you build a card factory: define a machine once, then call it whenever you need it. Each idea comes as a short ladder — simplest first, then one step up — with a Notice naming the one new thing. At the end of each lesson you apply the same factory idea to turtle stamps.
You will learn
- Define a function with
defand run it by calling its name. - Pass values into a function through parameters.
- Hand a result back with
returninstead of only printing it. - Tell a local name from a global name (scope).
Lesson 1: Build a Card Factory
A function is a named block of code you define once with def and run later by calling its name. Defining it does not run it — a call does.
def blank_card():
print("+----------------------+")
print("| |")
print("+----------------------+")
blank_card()
Output
+----------------------+
| |
+----------------------+Notice
def blank_card(): defines the machine; the last line blank_card() calls it, and only the call prints a card.
def blank_card():
print("+----------------------+")
print("| |")
print("+----------------------+")
blank_card()
blank_card()
blank_card()
Output
+----------------------+
| |
+----------------------+
+----------------------+
| |
+----------------------+
+----------------------+
| |
+----------------------+Notice
Define once, call many — three calls reuse the same body with no copy-paste.
A parameter is a blank the function fills in when it is called. greeting_card(name) has one parameter, name, that can hold a different value on every call.
def greeting_card(name):
print("+----------------------+")
print(f"Happy coding, {name}!")
print("+----------------------+")
greeting_card("Maya")
greeting_card("Leo")
Output
+----------------------+
Happy coding, Maya!
+----------------------+
+----------------------+
Happy coding, Leo!
+----------------------+Notice
The parameter name is a blank — each call ("Maya", "Leo") fills it with a different value.
def greeting_card(name, message):
print("+----------------------+")
print(f"{message}, {name}!")
print("+----------------------+")
greeting_card("Ada", "Well done")
greeting_card("Sam", "Happy birthday")
Output
+----------------------+
Well done, Ada!
+----------------------+
+----------------------+
Happy birthday, Sam!
+----------------------+Try it yourself
def greeting_card(name, message):
print("+----------------------+")
print(f"{message}, {name}!")
print("+----------------------+")
first_name = input("First name? ")
first_message = input("First message? ")
second_name = input("Second name? ")
second_message = input("Second message? ")
greeting_card(first_name, first_message)
greeting_card(second_name, second_message)
Notice
(It reads live input, so it does not run here.)
Notice
One step up — two parameters, name and message, each a separate blank the call fills in order.
Put it together: the same factory idea draws turtle stamps. Below is a simplified version of the idea in assets/l1_cards.py (turtle drawings run in their own window):
def stamp(size):
for edge in range(4):
turtle.forward(size)
turtle.right(90)
for side in range(4):
size = 18 + side * 4
stamp(size)
turtle.right(90)
Here stamp is a USER-DEFINED function built from forward/right — one call draws one square stamp. Run the full drawing, assets/l1_cards.py, the same way as the Unit 3 turtle programs: from a terminal in this unit folder (py on Windows, python3 on a Mac), or with Thonny's Run button.
Program
# Bring in Python's turtle drawing tools.
import turtle
def stamp(size):
"""Draw one closed square stamp and finish where it began."""
for edge in range(4):
turtle.forward(size)
turtle.right(90)
# Set up the pen for a greeting-card design made from strokes.
turtle.color("teal")
turtle.pensize(3)
# Draw a closed card border. No text command is needed.
for edge in range(2):
turtle.forward(220)
turtle.right(90)
turtle.forward(140)
turtle.right(90)
# Move from the card's top-left corner to a starting spot inside the card.
turtle.penup()
turtle.right(90)
turtle.forward(25)
turtle.left(90)
turtle.pendown()
# Make four pen-stroke stamps inside the card.
# The loop counter changes every stamp's size and horizontal starting position.
sides = 4
for side in range(sides):
size = 18 + side * 4
position = side * 45
turtle.penup()
turtle.forward(position)
turtle.pendown()
stamp(size)
turtle.penup()
turtle.backward(position)
turtle.pendown()
# Return from the inside starting spot to the card's top-left corner.
turtle.penup()
turtle.left(90)
turtle.forward(25)
turtle.right(90)
turtle.pendown()
# Keep the drawing window open until you close it.
turtle.done()
Lesson 2: Hand a Value Back with return
Some machines show their work; others hand a result back so the rest of the program can use it. return sends a value back to the line that called the function.
def area(w, h):
return w * h
card_area = area(7, 4.5)
message = f"The card area is {card_area} square units."
print(message)
Output
The card area is 31.5 square units.Try it yourself
def area(w, h):
return w * h
w = float(input("Width? "))
h = float(input("Height? "))
card_area = area(w, h)
message = f"The card area is {card_area} square units."
print(message)
Notice
(It reads live input, so it does not run here.)
Notice
area does not print — it returns w * h, so card_area can store the result and the f-string can use it.
def polygon_points(n):
return 360 / n
square_turn = polygon_points(4)
seven_side_turn = polygon_points(7)
print(f"A square turns {square_turn} degrees.")
print(f"A seven-sided shape turns {seven_side_turn} degrees.")
Output
A square turns 90.0 degrees.
A seven-sided shape turns 51.42857142857143 degrees.Notice
A returned value can be computed — 360 / n is a float, and the returned number flows straight into the f-string.
Printing only DISPLAYS something; returning HANDS a value back for another job. That is why polygon_points can feed a turtle loop instead of repeating the division.
Put it together: the shape factory uses the returned angle. Read this excerpt from assets/l2_shapes.py:
def polygon_points(n):
return 360 / n
def polygon(n, side_length):
turn_angle = polygon_points(n)
for side in range(n):
turtle.forward(side_length)
turtle.right(turn_angle)
Run the full shape factory, assets/l2_shapes.py, the same way, and find the three calls that build three different shapes.
Program
# Bring in Python's turtle drawing tools.
import turtle
def polygon_points(n):
"""Return the turn angle for a regular polygon with n sides."""
return 360 / n
def polygon(n, side_length):
"""Draw one closed regular polygon and finish where it began."""
turn_angle = polygon_points(n)
for side in range(n):
turtle.forward(side_length)
turtle.right(turn_angle)
turtle.color("purple")
turtle.pensize(3)
# Each function call uses the returned float as its turn angle.
polygon(3, 90)
turtle.right(120)
polygon(4, 70)
turtle.right(120)
polygon(5, 55)
turtle.right(120)
# Keep the drawing window open until you close it.
turtle.done()
Lesson 3: Keep Track of Scope
A name made INSIDE a function is local to that function. A name made outside functions is global. Scope is the part of a program where a name is available.
factory_name = "Function Factory"
def pack_card(name):
local_label = f"Card for {name}"
print(factory_name)
return local_label
first_card = pack_card("Ari")
print(f"Packed: {first_card}")
Output
Function Factory
Packed: Card for AriTry it yourself
factory_name = "Function Factory"
def pack_card(name):
local_label = f"Card for {name}"
print(factory_name)
return local_label
name = input("Name? ")
first_card = pack_card(name)
print(f"Packed: {first_card}")
Notice
(It reads live input, so it does not run here.)
Notice
Inside pack_card, local_label is local (made inside) while factory_name is global (made outside) — the function can read the global and return its own local.
Now meet a deliberate bug. Study the next cell, but do not run it — predict the last line of the traceback first.
def make_label(name):
card_label = f"Made for {name}"
print(card_label)
make_label("Nia")
print(card_label)
NameError: name 'card_label' is not defined
Notice
card_label lives only INSIDE make_label; the final print runs outside that local scope, so the name is gone. The repair is to return the label and store it outside, exactly as Lesson 2 returned an area.
Put it together: the final turtle factory nests loops, and each stamp(size) call keeps its own local edge counter. Below is a simplified version of the idea in assets/l3_stamps.py:
def stamp(size):
for edge in range(4):
turtle.forward(size)
turtle.right(90)
def stamp_gallery(rings, stamps_per_ring):
for ring_number in range(rings):
size = 18 + ring_number * 5
for stamp_number in range(stamps_per_ring):
stamp(size)
turtle.right(360 / stamps_per_ring)
Each stamp call finishes its own square before the next begins. Run the full gallery, assets/l3_stamps.py, the same way.
Functions let the outer design say what to build while the function body remembers how.
Program
# Bring in Python's turtle drawing tools.
import turtle
def stamp(size):
"""Draw one closed square stamp and finish where it began."""
for edge in range(4):
turtle.forward(size)
turtle.right(90)
def stamp_gallery(rings, stamps_per_ring):
"""Draw nested rings of stamps and restore the starting state."""
for ring_number in range(rings):
radius = 45 + ring_number * 25
size = 18 + ring_number * 5
for stamp_number in range(stamps_per_ring):
turtle.penup()
turtle.forward(radius)
turtle.pendown()
stamp(size)
turtle.penup()
turtle.backward(radius)
turtle.pendown()
turtle.right(360 / stamps_per_ring)
turtle.right(360 / rings)
turtle.color("darkgreen")
turtle.pensize(2)
# This first self-contained call marks the drawing's start position.
stamp(24)
# The gallery function contains an outer loop and an inner loop.
# Every stamp call returns to its own starting position and direction.
stamp_gallery(3, 6)
# Keep the drawing window open until you close it.
turtle.done()
Algorithm Extension
Extra loop practice — optional enrichment: these drills reuse the same loop shapes on small fixed data, so try as many as you like.
A function can keep a running total
Sometimes a factory needs to combine several values before it returns. Start total at 0, update it once for each value, and return the finished total.
def total_stamp_size(number_of_stamps):
total = 0
for stamp_number in range(number_of_stamps):
size = 10 + stamp_number * 5
total = total + size
return total
three_stamp_total = total_stamp_size(3)
print(three_stamp_total)
Output
45Try it yourself
def total_stamp_size(number_of_stamps):
total = 0
for stamp_number in range(number_of_stamps):
size = 10 + stamp_number * 5
total = total + size
return total
number_of_stamps = int(input("How many stamps? "))
three_stamp_total = total_stamp_size(number_of_stamps)
print(three_stamp_total)
Notice
(It reads live input, so it does not run here.)
Notice
total is an accumulator. It begins at 0, and total = total + size adds one stamp size on every loop turn. The function returns the final total after the loop.
Recap
- Defining a function does not run it; each call runs its body.
- A parameter is a blank that each call fills in, in order.
returnhands a value back, so the caller can save it and use it again.- A name made inside a function is local and is gone after the call; return it to keep it.
You can now build a factory of cards and stamps from functions you call again and again.