3.13 Developing Procedures

Source lesson: python/developing-procedures

Learning Objective: Write procedures that each do one named task, choose names that say what they do, and split long procedures into smaller ones that are easier to test.

Exercise 1: Rename the functions to be more descriptive

The lesson’s measure_flour, mix_ingredients and bake say what but not how much or what result, and make_dessert doesn’t say which dessert. The renamed versions also take parameters, so the details live in the call instead of being hidden inside.

Code Runner Challenge

3.13 Exercise 1 - Descriptive procedure names

View IPYNB Source
# CODE_RUNNER: 3.13 Exercise 1 - Descriptive procedure names
def bake_chocolate_cake():
    measure_dry_ingredients(cups_flour=2, cups_sugar=1)
    mix_wet_into_dry(eggs=3)
    bake_in_oven(temp_f=350, minutes=25)

def measure_dry_ingredients(cups_flour, cups_sugar):
    print(f"Measuring {cups_flour} cups of flour and {cups_sugar} cup of sugar.")

def mix_wet_into_dry(eggs):
    print(f"Mixing {eggs} eggs into the flour and sugar.")

def bake_in_oven(temp_f, minutes):
    print(f"Baking at {temp_f}°F for {minutes} minutes.")

bake_chocolate_cake()
Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Reading only the first procedure, bake_chocolate_cake, you already know the whole recipe without opening any of the helpers. With do_step() you’d have to open each one to find out.

Exercise 2: Convert the Python functions to Java methods

Code Runner Challenge

3.13 Exercise 3 - Before: one long procedure

View IPYNB Source
public class DessertRobot {

    // public: the one method other classes are meant to call
    public void bakeChocolateCake() {
        measureDryIngredients(2, 1);
        mixWetIntoDry(3);
        bakeInOven(350, 25);
    }

    // private: helper steps only DessertRobot itself should call
    private void measureDryIngredients(int cupsFlour, int cupsSugar) {
        System.out.println("Measuring " + cupsFlour + " cups of flour and " + cupsSugar + " cup of sugar.");
    }

    private void mixWetIntoDry(int eggs) {
        System.out.println("Mixing " + eggs + " eggs into the flour and sugar.");
    }

    private void bakeInOven(int tempF, int minutes) {
        System.out.println("Baking at " + tempF + "°F for " + minutes + " minutes.");
    }

    public static void main(String[] args) {
        new DessertRobot().bakeChocolateCake();
    }
}
Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Access (public vs. private): public means any other class can call the method. private means only code inside DessertRobot can. bakeChocolateCake is the robot’s one job, so it’s public. The step methods are private because calling bakeInOven on its own, without measuring or mixing first, would skip steps, so outside code shouldn’t be able to. Python has no enforced private. By convention a leading underscore (_bake_in_oven) signals “internal, don’t call this from outside.”

The other differences: Java methods live inside a class and declare a return type (void = returns nothing) and a type for each parameter (int). Python just uses def with untyped parameters.

Exercise 3: Split a long procedure into smaller ones and show how tests get easier

Before: one procedure does everything and only prints, so there’s nothing to test except reading the output.

Code Runner Challenge

3.13 Exercise 3 - After: small procedures + tests

View IPYNB Source
# CODE_RUNNER: 3.13 Exercise 3 - Before: one long procedure
def do_step(eggs, cups_flour, oven_temp_f):
    if eggs < 2 or cups_flour < 1:
        print("Not enough ingredients!")
    else:
        batter_cups = cups_flour + eggs * 0.25
        print("Batter:", batter_cups, "cups")
        if oven_temp_f < 325:
            minutes = 35
        elif oven_temp_f > 375:
            minutes = 20
        else:
            minutes = 25
        print("Bake for", minutes, "minutes")
        print("Dessert is ready!")

do_step(3, 2, 350)
Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

After: each sub-task is its own procedure that returns a value. The main procedure just calls them in order, and each piece can be checked on its own with assert.

# CODE_RUNNER: 3.13 Exercise 3 - After: small procedures + tests
def has_enough_ingredients(eggs, cups_flour):
    return eggs >= 2 and cups_flour >= 1

def batter_volume(eggs, cups_flour):
    return cups_flour + eggs * 0.25

def bake_minutes(oven_temp_f):
    if oven_temp_f < 325:
        return 35
    elif oven_temp_f > 375:
        return 20
    else:
        return 25

def make_dessert(eggs, cups_flour, oven_temp_f):
    if not has_enough_ingredients(eggs, cups_flour):
        print("Not enough ingredients!")
        return
    print("Batter:", batter_volume(eggs, cups_flour), "cups")
    print("Bake for", bake_minutes(oven_temp_f), "minutes")
    print("Dessert is ready!")

# Each small procedure gets its own test, including the boundaries
assert has_enough_ingredients(2, 1) == True
assert has_enough_ingredients(1, 3) == False
assert batter_volume(4, 2) == 3.0
assert bake_minutes(300) == 35
assert bake_minutes(325) == 25   # boundary: 325 is not < 325
assert bake_minutes(375) == 25   # boundary: 375 is not > 375
assert bake_minutes(400) == 20
print("All tests passed")

make_dessert(3, 2, 350)
make_dessert(1, 2, 350)

Why testing got easier: in the long version, checking the bake time at 325°F means running the whole recipe and reading printed text. That only works if there are enough eggs, and the result can’t be compared in code. After splitting, bake_minutes(325) is one line that returns a number, so the boundary cases (325 and 375) each get their own assert. If a test fails, its name points straight to the procedure with the bug.