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
| Test input or action | Expected result |
|---|---|
| 4 by 3 | Area 12; double 24 |
| 0 by 3 | Area 0 |
| 2.5 by 4 | Area 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.
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
| Test input or action | Expected result |
|---|---|
| 6 | Result 12; with one more 13 |
| 0 | Result 0; with one more 1 |
| -3 | Result -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.
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
| Test input or action | Expected result |
|---|---|
| Maya, 8 | Maya: 8/10 (80.0%) |
| Kai, 15, maximum 20 | Kai: 15/20 (75.0%) |
| Maya, 0 | 0/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.
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
| Test input or action | Expected result |
|---|---|
| Two omitted-list calls | [first] then [second], independently |
| Supplied empty list | Caller list becomes [kept] |
| Identity check | True 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.
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
| Test input or action | Expected result |
|---|---|
| Run app.py | Cost: 13.5 |
| Import app | No 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.
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
| Test input or action | Expected result |
|---|---|
| Maya 8 of 10 | 80.0% attendance |
| Kai 0 of 10 | 0.0% attendance |
| No possible sessions | Outside 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.
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
| Test input or action | Expected result |
|---|---|
| Target 8 in [8, 5, 12, 5] | 0, reported as found |
| Target 5 | 1, the first match |
| Target 99 | None |
| Empty list | None |
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.
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
| Test input or action | Expected result |
|---|---|
| Target 12 | 3 |
| Empty list | None |
| Target 7 in ordered data | None |
| [2, 5, 5, 5, 8], target 5 | Any 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.
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
| Test input or action | Expected result |
|---|---|
| Primary score ordering | Kai first with 12 |
| Equal scores eight | Maya before Noor |
| Original order | Noor, 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.
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
| Test input or action | Expected result |
|---|---|
| 0; 1; 4 | 1; 1; 24 |
| Inputs 0 through 6 | Both implementations agree |
| -1 or True | ValueError 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.
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
| Test input or action | Expected result |
|---|---|
| North before key | Hall retained; lock feedback |
| Repeated take | Only one inventory key |
| Winning route | Observatory reached with key |
| New world | Lab 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.
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
| Test input or action | Expected result |
|---|---|
| First target | True with 1 check |
| Absent target | False with 10 checks |
| Ten outer, three inner | 30 visits |
| Twenty outer, three inner | 60 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.
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