LEARN PYTHON | 04 FUNCTIONS AND ALGORITHMS

Build it. Test it. Explain it.

Build reusable components and test the algorithms inside them. Work independently before opening a hint, then compare your tested solution with one possible approach.

YOUR CHALLENGE ROUTE

Complete these tasks in your Python editor. The page is a challenge guide, not a general Python runner. Use fictional details where helpful. Work in order or choose a task matching an area you need to practise.

These challenges combine earlier collections and control flow with functions, modules and algorithms. Run Python in your editor. Multi-file tasks require the named companion files; the page does not execute arbitrary source.

TRACK YOUR TESTED SOLUTIONS

Tick a task only after you have run it, checked at least two relevant cases and explained one line. These are self-recorded checks, not an automatic assessment of your source code.

Progress lasts while this page is open. Save your actual Python files in your project folder.

01 | FOUNDATION

RETURN A USEFUL CALCULATION

Create a rectangle-area function and use its return value in a report. Keep the calculation independent of input and display.

YOUR REQUIREMENTS

  • Accept width and height as parameters.
  • Return their product.
  • Call the function twice with known suitable non-negative numeric inputs.
  • Use one result in another calculation.
  • Write a docstring stating assumptions.
Hint: where to begin

A returned value can be stored; printing the area inside the function does not return that number.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
4 by 3Area 12; double 24
0 by 3Area 0
2.5 by 4Area 10.0
Reveal one possible solution and explanation
def rectangle_area(width, height):
    """Return area for suitable non-negative numeric dimensions."""
    return width * height

area = rectangle_area(4, 3)
print("Area:", area)
print("Double area:", area * 2)
print("Zero-width area:", rectangle_area(0, 3))

The helper returns a reusable number. The caller decides what to calculate and display next. The stated dimension assumptions are not enforced by the multiplication itself.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Add a separate perimeter function. Test a rectangle whose area and perimeter differ.

Revisit the related lesson

02 | REPAIR

REPAIR A LOST RETURN VALUE

A doubled-number function displays the expected value but the caller receives None. Repair its contract and explain the mistake.

YOUR REQUIREMENTS

  • Return number * 2.
  • Keep display in the caller.
  • Test positive, zero and negative input.
  • Explain the original implicit None result.

SOURCE TO REPAIR

def double(number):
    print(number * 2)

result = double(6)
print(result)
Hint: where to begin

The value passed to print is not automatically returned by the enclosing function.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
6Result 12; with one more 13
0Result 0; with one more 1
-3Result -6; with one more -5
Reveal one possible solution and explanation
def double(number):
    return number * 2

result = double(6)
print("Result:", result)
print("With one more:", result + 1)

The repaired call evaluates to the doubled number. The original body printed a message and reached its end, so its implicit result was None. Adding one to that result would raise TypeError.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Create a separate display helper returning None and explain why that can be an appropriate contract.

Revisit the related lesson

03 | PARAMETERS

DESIGN CLEAR ARGUMENTS

Create a report function with a required name and score and an optional maximum. Use positional and keyword calls.

YOUR REQUIREMENTS

  • Default maximum to 10.
  • Return formatted text without printing inside the helper.
  • Assume numeric scores and a positive numeric maximum.
  • Use a keyword call with maximum 20.
  • Explain the difference between parameter names and caller names.
Hint: where to begin

Default values are used only when the matching argument is omitted. f-string formatting can display the percentage to one decimal place.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Maya, 8Maya: 8/10 (80.0%)
Kai, 15, maximum 20Kai: 15/20 (75.0%)
Maya, 00/10 (0.0%)
Reveal one possible solution and explanation
def score_report(name, score, maximum=10):
    """Format suitable scores with a positive maximum."""
    percentage = score / maximum * 100
    return f"{name}: {score}/{maximum} ({percentage:.1f}%)"

print(score_report("Maya", 8))
print(score_report(score=15, maximum=20, name="Kai"))

The call binds arguments to parameters and the helper returns text. A zero maximum breaks the stated precondition; the example does not claim division performs validation.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Add an explicit maximum check raising ValueError, then test the zero case separately.

Revisit the related lesson

04 | SCOPE AND SHARING

REPAIR A MUTABLE DEFAULT

Separate independent note lists while still allowing a caller to supply a list to extend deliberately.

YOUR REQUIREMENTS

  • Use None as the default.
  • Create a fresh list only when the argument is None.
  • Append and return the selected list.
  • Test two omitted-list calls.
  • Test an explicitly supplied empty list.

SOURCE TO REPAIR

def add_note(note, notes=[]):
    notes.append(note)
    return notes

print(add_note("first"))
print(add_note("second"))
Hint: where to begin

if not notes would replace the caller’s empty list. is None distinguishes omission from an existing falsey collection.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Two omitted-list calls[first] then [second], independently
Supplied empty listCaller list becomes [kept]
Identity checkTrue for the deliberately supplied list
Reveal one possible solution and explanation
def add_note(note, notes=None):
    if notes is None:
        notes = []
    notes.append(note)
    return notes

print(add_note("first"))
print(add_note("second"))
shared = []
returned = add_note("kept", shared)
print("Caller list:", shared)
print("Same object:", returned is shared)

The default None is immutable and signals omitted input. Each omitted call creates a fresh list; a supplied list is intentionally mutated. The contract should state that effect.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Design a second helper that always returns a new outer list and leaves supplied flat input unchanged.

Revisit the related lesson

05 | MODULES

CREATE A REUSABLE PRICING MODULE

Create pricing.py and app.py. Keep calculations available for import and run the report only from the application entry point.

YOUR REQUIREMENTS

  • Save the labelled file blocks separately.
  • Import pricing with a qualified function call.
  • Use a main guard in app.py.
  • Run app.py and verify its cost.
  • Import app and confirm its guarded main does not print.
Hint: where to begin

Module headings here are comments, not file separators Python recognises. Copy the first block into pricing.py and the second into app.py.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Run app.pyCost: 13.5
Import appNo report from the guarded main
pricing.ticket_total(0, 4.5)0.0
Reveal one possible solution and explanation
# SAVE THIS BLOCK AS pricing.py
def ticket_total(quantity, unit_price):
    return quantity * unit_price

# SAVE THE FOLLOWING BLOCK AS app.py
import pricing

def main():
    print("Cost:", pricing.ticket_total(3, 4.5))

if __name__ == "__main__":
    main()

The application depends on the helper module. The helper needs values through parameters rather than importing application state. The two labelled blocks are two files, not one runnable combined solution.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Create a separate check file with direct assertions for zero quantity and zero price.

Revisit the related lesson

06 | DECOMPOSITION

REFACTOR A REPORT AND PRESERVE BEHAVIOUR

Split an attendance calculation from report formatting. Record results before and after the structural change.

YOUR REQUIREMENTS

  • Calculate a percentage in one helper.
  • Create report text in a second helper.
  • Use explicit arguments and return values.
  • State a positive-session-count assumption.
  • Check that the report still matches known results.
Hint: where to begin

The formatter can call the calculation helper with explicit values; neither needs hidden globals or input calls.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Maya 8 of 1080.0% attendance
Kai 0 of 100.0% attendance
No possible sessionsOutside the positive-denominator contract
Reveal one possible solution and explanation
def attendance_percentage(attended, possible):
    return attended / possible * 100

def attendance_report(name, attended, possible):
    value = attendance_percentage(attended, possible)
    return f"{name}: {value:.1f}% attendance"

print(attendance_report("Maya", 8, 10))
print(attendance_report("Kai", 0, 10))

Responsibilities are separate: one calculates, one formats. Refactoring should preserve the report’s agreed behaviour. The zero-denominator case is a stated limitation rather than a silently accepted result.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Add explicit validation as a separate requirement and rerun earlier valid-input cases.

Revisit the related lesson

07 | LINEAR SEARCH

RETURN THE FIRST SEARCH MATCH

Implement linear search returning an index or None. Handle position zero correctly.

YOUR REQUIREMENTS

  • Return at the first match.
  • Place absent return after the loop.
  • Test empty, first, last, absent and duplicate cases.
  • Use is not None when reporting success.
Hint: where to begin

Returning None inside the loop after one mismatch rejects later matches prematurely.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Target 8 in [8, 5, 12, 5]0, reported as found
Target 51, the first match
Target 99None
Empty listNone
Reveal one possible solution and explanation
def linear_search(values, target):
    for index, value in enumerate(values):
        if value == target:
            return index
    return None

position = linear_search([8, 5, 12, 5], 8)
if position is not None:
    print("Found at:", position)
else:
    print("Absent")

Index zero is a real match but falsey, so a truthiness check is unsuitable. The absent result follows exhaustion; a matching return ends the call early.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Return all matching indexes in a separate function and explain its changed contract.

Revisit the related lesson

08 | ORDERED SEARCH

TEST BINARY SEARCH WITHOUT GUESSING

Implement binary search and document that inputs are ascending. Test repeated values without assuming the first duplicate index.

YOUR REQUIREMENTS

  • Maintain inclusive low and high bounds.
  • Exclude the midpoint after a mismatch.
  • Return None when the interval is empty.
  • Test absent and empty cases.
  • Check a returned duplicate index actually contains the target.
Hint: where to begin

With inclusive bounds, high starts at len(values)-1. Update bounds with mid-1 or mid+1 to guarantee progress.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Target 123
Empty listNone
Target 7 in ordered dataNone
[2, 5, 5, 5, 8], target 5Any returned index must reference a 5; here 2
Reveal one possible solution and explanation
def binary_search(values, target):
    low, high = 0, len(values) - 1
    while low <= high:
        mid = (low + high) // 2
        if values[mid] == target:
            return mid
        if target < values[mid]:
            high = mid - 1
        else:
            low = mid + 1
    return None

print(binary_search([2, 5, 8, 12, 16], 12))
print(binary_search([], 12))

The bound changes depend on ascending order. This implementation finds any duplicate match, so comparing its index blindly against a first-match linear search would test the wrong contract.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Count inspected midpoints and compare with a linear search on already sorted data.

Revisit the related lesson

09 | RECORD SORTING

SORT A SCOREBOARD WITH A TIE POLICY

Sort player records by descending score and ascending name. Preserve original arrival order in the source list.

YOUR REQUIREMENTS

  • Use sorted rather than mutating the source.
  • Provide a named key function.
  • Return (-score, name) as the key.
  • Include equal-score records.
  • Print source and display orders separately.
Hint: where to begin

Default ascending tuple sorting orders negative scores from highest original score first, then names ascending.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Primary score orderingKai first with 12
Equal scores eightMaya before Noor
Original orderNoor, Kai, Maya remains unchanged
Reveal one possible solution and explanation
def ranking_key(player):
    return -player["score"], player["name"]

players = [{"name": "Noor", "score": 8},
           {"name": "Kai", "score": 12},
           {"name": "Maya", "score": 8}]
ranked = sorted(players, key=ranking_key)
print("Arrival:", [player["name"] for player in players])
print("Display:", [player["name"] for player in ranked])

The tuple explicitly defines two ordering levels. This is a different tie policy from sorting solely by score stably or sorting whole (score, name) tuples in reverse. A new outer list still shares the record dictionaries.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Compare with key returning only score and reverse=True, then explain the equal-key arrival order.

Revisit the related lesson

10 | RECURSION

TRACE AND CHECK RECURSIVE FACTORIAL

Implement factorial for small non-negative integers and compare it against a loop. Explain descent and unwinding before running.

YOUR REQUIREMENTS

  • Reject unsuitable types and negative integers.
  • Return one for zero.
  • Reduce each recursive input by one.
  • Test zero, one and four.
  • Compare both implementations on inputs zero through six.
Hint: where to begin

Each recursive call retains its own waiting multiplication. Python has recursion-depth limits; use small test inputs.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
0; 1; 41; 1; 24
Inputs 0 through 6Both implementations agree
-1 or TrueValueError from the input guard
Reveal one possible solution and explanation
def factorial(number):
    if type(number) is not int or number < 0:
        raise ValueError("Use a non-negative integer")
    if number == 0:
        return 1
    return number * factorial(number - 1)

def factorial_iterative(number):
    if type(number) is not int or number < 0:
        raise ValueError("Use a non-negative integer")
    total = 1
    for value in range(1, number + 1):
        total *= value
    return total

for number in range(7):
    assert factorial(number) == factorial_iterative(number)
print("Factorial checks passed")

The recursive version uses a base case and progress step. Agreement provides evidence but does not replace understanding. The loop avoids a growing call stack; integer sizes still grow with the result.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Count calls and multiplications for four. Distinguish those counts from an exact execution-time claim.

Revisit the related lesson

11 | CAPSTONE

BUILD AND TEST THE MODULAR ADVENTURE

Create the three project files from the lesson, then save and run this independent action-check file beside them.

YOUR REQUIREMENTS

  • Keep world.py, engine.py and adventure.py together.
  • Run adventure.py for interactive play.
  • Check invalid moves preserve state.
  • Check key transfer and repeated-take rejection.
  • Check the winning route.
  • Create fresh worlds for independent sessions.
Hint: where to begin

Use the downloadable project as a reference after attempting your own design. The test file imports helpers without launching the application.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
North before keyHall retained; lock feedback
Repeated takeOnly one inventory key
Winning routeObservatory reached with key
New worldLab key restored independently
Reveal one possible solution and explanation
from world import new_world
from engine import move, take_item, has_won

world = new_world()
state = {"room": "hall", "inventory": []}
move(world, state, "north")
assert state["room"] == "hall"
move(world, state, "east")
assert take_item(world, state, "key") == "Taken: key"
assert take_item(world, state, "key") == "That item is not here."
assert state["inventory"] == ["key"]
move(world, state, "west")
move(world, state, "north")
assert has_won(state)
assert new_world()["lab"]["items"] == ["key"]
print("Adventure checks passed")

The checks inspect state and effects separately from interface text. The reusable modules permit direct action testing without scripted input. They assume trusted, correctly authored room records.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Add a workshop room with return travel and rerun every original check.

Revisit the related lesson

12 | ANALYSE AND EXTEND

MEASURE WORK AND HAND OVER YOUR DESIGN

Count meaningful work in a search and explain why a fixed-length inner loop is linear. Then hand over your adventure with a brief module map.

YOUR REQUIREMENTS

  • Count equality checks for first-match and absent searches.
  • Define n as the list length.
  • Count body executions for n outer iterations with three inner visits.
  • Distinguish operation counts from timing.
  • Provide the adventure startup command and known tests.
Hint: where to begin

A fixed inner count of three gives 3n work, not n squared. The search’s best and worst arrangements differ.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
First targetTrue with 1 check
Absent targetFalse with 10 checks
Ten outer, three inner30 visits
Twenty outer, three inner60 visits, linear growth
Reveal one possible solution and explanation
def search_checks(values, target):
    checks = 0
    for value in values:
        checks += 1
        if value == target:
            return True, checks
    return False, checks

values = list(range(10))
print("First:", search_checks(values, 0))
print("Absent:", search_checks(values, 99))
visits = 0
for item in values:
    for repeat in range(3):
        visits += 1
print("Nested visits:", visits)

The selected operations expose growth without claiming every machine operation was counted. The handover demonstrates whether decomposition communicates responsibilities to another programmer.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Compare a second inner loop ranging over the whole values list and explain why its count is n squared.

Revisit the related lesson

KEEP A SMALL TEST RECORD

Record the function call, expected return value, expected changes to input objects, actual results and conclusion. Output text alone does not prove that a return value or mutation is correct.

For a multi-file project, record the entry point and test imports as well as direct execution. For searches, test zero indexes, empty input, missing values and duplicates. For recursion, test the base case, a small larger input and controlled rejection of unsuitable input.

READY FOR THE NEXT SECTION?

You are ready to move on when you can define functions with clear contracts, explain scope and sharing, import reusable modules, test search and sorting assumptions and trace a recursive call.

If one skill is uncertain, revisit its lesson and change a worked example before repeating the challenge. The Section 4 workbook is coming soon.

Next: text files and context managers Python course navigation