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.
-
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
defand run later by calling its name. Defining it does not run it — a call does.def blank_card(): print("+----------------------+") print("| |") print("+----------------------+") blank_card()Scroll the code for more ↓
Output
+----------------------+ | | +----------------------+def blank_card():defines the machine; the last lineblank_card()calls it, and only the call prints a card.def blank_card(): print("+----------------------+") print("| |") print("+----------------------+") blank_card() blank_card() blank_card()Scroll the code for more ↓
Output
+----------------------+ | | +----------------------+ +----------------------+ | | +----------------------+ +----------------------+ | | +----------------------+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")Scroll the code for more ↓
Output
+----------------------+ Happy coding, Maya! +----------------------+ +----------------------+ Happy coding, Leo! +----------------------+The parameter
nameis 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")Scroll the code for more ↓
Output
+----------------------+ Well done, Ada! +----------------------+ +----------------------+ Happy birthday, Sam! +----------------------+Try it 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)Scroll the code for more ↓
(It reads live input, so it does not run here.)
One step up — two parameters,
nameandmessage, 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
stampis a USER-DEFINED function built fromforward/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 (pyon Windows,python3on 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()Scroll the code for more ↓
Lesson 2: Hand a Value Back with
returnSome machines show their work; others hand a result back so the rest of the program can use it.
returnsends 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)Scroll the code for more ↓
Output
The card area is 31.5 square units.Try it 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)Scroll the code for more ↓
(It reads live input, so it does not run here.)
areadoes not print — it returnsw * h, socard_areacan 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.")Scroll the code for more ↓
Output
A square turns 90.0 degrees. A seven-sided shape turns 51.42857142857143 degrees.A returned value can be computed —
360 / nis 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_pointscan 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()Scroll the code for more ↓
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}")Scroll the code for more ↓
Output
Function Factory Packed: Card for AriTry it 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}")Scroll the code for more ↓
(It reads live input, so it does not run here.)
Inside
pack_card,local_labelis local (made inside) whilefactory_nameis 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 definedcard_labellives only INSIDEmake_label; the finalprintruns outside that local scope, so the name is gone. The repair is toreturnthe 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 localedgecounter. Below is a simplified version of the idea inassets/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
stampcall 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()Scroll the code for more ↓
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
totalat0, 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)Scroll the code for more ↓
Output
45Try it 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)Scroll the code for more ↓
(It reads live input, so it does not run here.)
totalis an accumulator. It begins at0, andtotal = total + sizeadds one stamp size on every loop turn. The function returns the final total after the loop.-
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.
-