Consolidating Advanced Programming Techniques
advanced30 minLearning objectives
- Integrate exception handling, stack-frame understanding and recursion within larger programs
Learn
AQA 4.1.1.9, .15, .16 — Consolidating advanced programming techniques
Retrieval: this sequence has covered exception handling, the call stack, and recursion as separate topics, each in its own lesson. Real programs never keep them this neatly separated — a single robust, recursive function has to get its base case right and handle whatever invalid input reaches it, at the same time. This final lesson brings all three together.
Understand — why integration is harder than any one technique alone
A function that's recursive AND must validate its own input has to get both things right simultaneously: an exception raised partway through a deep recursive call still has to unwind cleanly back through every stack frame above it, and a base case has to account for genuinely invalid input, not just the "normal" smallest case. Treating these as separate concerns, bolted on independently, is exactly where combined bugs hide.
Worked example — a robust recursive function
def digit_sum(n):
if not isinstance(n, int) or n < 0:
raise ValueError("digit_sum requires a non-negative integer.")
if n < 10: # base case: single digit
return n
return n % 10 + digit_sum(n // 10) # recursive case: last digit + the rest
def safe_digit_sum(value):
try:
return digit_sum(value)
except ValueError as e:
return f"Could not calculate: {e}"
print(safe_digit_sum(2024)) # 8 (2+0+2+4)
print(safe_digit_sum(-5)) # "Could not calculate: digit_sum requires a non-negative integer."
print(safe_digit_sum("abc")) # "Could not calculate: digit_sum requires a non-negative integer."
Notice the validation happens once, at the very top of every call — including every recursive call — rather than being checked only by the outer wrapper. This matters: without it, a single bad recursive call partway through (not just the very first call) could still slip through unvalidated.
Diagnose the category — three broken versions, three different faults
Each version below of a recursive function list_depth(value) (meant to return how deeply nested a list is — a plain value is depth 0, a list containing only plain values is depth 1, and so on) contains exactly one fault. For each, diagnose which category the fault belongs to (missing/incorrect base case, unhandled exception, or a scope/stack-frame misunderstanding) before fixing it:
Version A:
def list_depth(value):
if not isinstance(value, list):
return 0
return 1 + max(list_depth(item) for item in value)
Calling list_depth([]) crashes with ValueError: max() arg is an empty sequence.
Version B:
def list_depth(value):
depth = 0
if not isinstance(value, list):
return depth
return 1 + max(list_depth(item) for item in value)
print(list_depth([1, [2, 3]]))
print(depth)
The print(depth) line crashes with NameError: name 'depth' is not defined.
Version C:
def list_depth(value):
if not isinstance(value, list):
return 0
if len(value) == 0:
return 0
return 1 + max(list_depth(item) for item in value[1:])
list_depth([1, 2, 3]) returns 1 — correct. list_depth([[1, 2], [3, 4]]) returns 1 — should be 2.
(Version A: missing/incorrect base case — an empty list has no items for max() to compare, so the "empty list" case needs its own explicit base case (return 0 or 1 depending on convention), not just "not a list". Version B: scope/stack-frame misunderstanding — depth is a local variable belonging only to list_depth's own stack frame; the outer print(depth) has no access to it, exactly the pattern from the Stack Frames lesson's debug task. Version C: an incorrect recursive case — value[1:] silently skips the very first item of every list at every level, so nesting inside that first item is never actually explored; the recursive case should use the whole value, not value[1:].)
Apply it — build a combined program
Write a function safe_factorial_sum(numbers) that takes a list of values, and for each one that is a non-negative integer, calculates its factorial recursively and adds it to a running total — skipping (not crashing on) any value in the list that isn't a valid non-negative integer, and printing a specific message naming which values were skipped and why.
Challenge
Extend safe_factorial_sum so that if the entire list contains no valid non-negative integers at all, it raises its own exception with a clear message, rather than silently returning 0 as if everything had gone fine — then write code that calls it inside a try/except and handles that case gracefully too.
Looking ahead: Sequence 11 (Programming Paradigms) introduces Object-Oriented Programming — a fundamentally different way of organising the kind of robust, multi-part programs this lesson has just practised combining.