Recursion, Debugging, and Final Code Reading
Key Takeaways
- A recursive function needs a reachable base case, or Python eventually raises RecursionError.
- Each recursive call has its own local variables even when the parameter names are identical.
- Debugging a snippet means finding the first failing expression on the executed path, not guessing from the last line.
- Operation type predicts the exception: indexing problems give IndexError/TypeError, conversions give ValueError, lookups give KeyError.
- A consistent final-review routine traces calls, scopes, return values, printed output, and exception paths in a fixed order.
Recursion at entry level
Recursion means a function calls itself. PCEP does not require advanced algorithms, but you must recognize a base case, a recursive case, and the order returned values combine. Without a reachable base case the calls continue until Python hits its recursion limit (1000 by default) and raises RecursionError.
def countdown(n):
if n == 0:
return 'go'
return str(n) + countdown(n - 1)
print(countdown(3))
The base case is n == 0. The chain is countdown(3), countdown(2), countdown(1), countdown(0). Returns combine on the way back up, producing '321go'. Read recursion in two directions: descend to the base case, then unwind the returns upward. Most wrong answers come from trying to combine values on the way down instead of on the way back.
Each call owns its locals
Recursive calls reuse the same parameter name, but each call has its own copy. They do not share one variable.
| Call | Local n | Returns |
|---|---|---|
countdown(3) | 3 | '3' + result of countdown(2) |
countdown(2) | 2 | '2' + result of countdown(1) |
countdown(1) | 1 | '1' + result of countdown(0) |
countdown(0) | 0 | 'go' |
Reading bottom-up: countdown(0) gives 'go'; countdown(1) gives '1go'; countdown(2) gives '21go'; countdown(3) gives '321go'. This stacked-table method works for any recursive snippet that asks for final output. It also visually reinforces that the deepest call returns first while the outermost call returns last.
Debugging means first failure on the executed path
A debugging item may show code with several plausible problems. Python stops at the first unhandled exception on the path actually executed; later potential errors are irrelevant if execution never reaches them.
def pick(data, key):
return data[key][0]
scores = {'Ava': []}
print(pick(scores, 'Ava'))
print(pick(scores, 'Noah'))
The first call finds key 'Ava', then tries index 0 of an empty list, raising IndexError. The program crashes there; the second call, which would raise KeyError, is never reached. If a question asks which exception the program raises, the answer is IndexError, not KeyError, precisely because order of execution decides which fires first.
This is why guessing from the last line is dangerous: the last line may never run. Always trace from the first top-level executable statement forward until something fails.
Sorting exception clues, and a final routine
Use the operation to predict the exception type:
| Clue in the snippet | Likely exception |
|---|---|
| Undefined / unbound variable | NameError / UnboundLocalError |
| Bad argument count or incompatible operand types | TypeError |
| Failed conversion from a valid type | ValueError |
| Sequence position does not exist | IndexError |
| Dictionary key does not exist | KeyError |
| Division or modulo by zero | ZeroDivisionError |
For example, [1, 2]['0'] raises TypeError (a list index must be an integer or slice, not a string), while [1, 2][9] raises IndexError (index type valid, position absent).
Final code-reading routine for the last minutes before the exam:
- Read all
defs as definitions, not executions. - Start at the first top-level executable line.
- For each call, bind parameters and trace its fresh local scope.
- Record printed output separately from returned values.
- Stop at the first unhandled exception, but still run any matching except and finally clauses.
- For recursion, descend to the base case, then unwind the returns.
Label every value as output, return value, local variable, or exception. One missed None, one wrong handler order, or one skipped finally block can flip the entire answer, and at 70% to pass, every traced item counts.
Recursion that accumulates, and a mixed worked example
Not all recursion concatenates strings; many PCEP items accumulate a number. The same descend-then-unwind logic applies.
def summ(n):
if n == 0:
return 0
return n + summ(n - 1)
print(summ(3))
Descend: summ(3) waits on summ(2), which waits on summ(1), which waits on summ(0) returning 0. Unwind: summ(1) is 1 + 0 = 1; summ(2) is 2 + 1 = 3; summ(3) is 3 + 3 = 6. The printed value is 6. If the base case were forgotten, summ(-1) would recurse forever and raise RecursionError, so always confirm the base case is actually reachable for the given input.
A mixed snippet combining the whole chapter:
total = 0
def apply(values, key):
return values[key] * 2
data = [10, 20]
try:
print(apply(data, 1))
print(apply(data, 5))
except IndexError:
print('out of range')
finally:
print('cleanup')
Trace it in order: apply(data, 1) returns 20 * 2 = 40, printed. apply(data, 5) indexes past the end and raises IndexError; the matching except prints out of range; then finally prints cleanup. The full output is 40, out of range, cleanup.
| Step | Action | Output so far |
|---|---|---|
| 1 | apply(data, 1) returns 40 | 40 |
| 2 | apply(data, 5) raises IndexError | (none added) |
| 3 | except IndexError runs | out of range |
| 4 | finally runs | cleanup |
That single example exercises call flow, argument binding, an IndexError, and finally, exactly the kind of integration the last exam questions demand.
What is the purpose of a base case in a recursive function?
Which exception is most likely raised by {'x': 1}['y']?
A call inside a try block raises IndexError, a matching except IndexError prints 'fixed', and a finally block prints 'done'. If no new exception is raised, what happens next?
You've completed this section
Continue exploring other exams