Python by Projects

unit

Function Factory

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 def and run it by calling its name.
  • Pass values into a function through parameters.
  • Hand a result back with return instead 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 Ari

Try 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

45

Try 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.
  • return hands 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.

Go to the exercises (22)

For parents and teachers: report a problem