Lesson 4 · Solution · Recursion: base-case design

Solution: Find the Last Element

def last(xs):
    if len(xs) == 1:
        return xs[0]          # base case: the only element IS the last
    else:
        return last(xs[1:])   # recursive case: last of the rest

Base case: a one-element list [x] — its last element is xs[0], the element itself.

Recursive case: the last element of [3, 1, 2] is exactly the last element of [1, 2], which is the last element of [2]. Each step throws away the first element and asks the same question of a shorter list. Notice the difference from summing a list: there we combined xs[0] with the recursive result (xs[0] + total(xs[1:])); here we discard xs[0] and just pass the answer straight up (last(xs[1:])). The first element does no work — it’s only along for the ride until we reach the last one.

The trace

last([3,1,2]) = last([1,2])
              = last([2])     ← len is 1, base case
              = 2

The empty list: total vs partial functions

What about last([])? There’s genuinely no answer — an empty list has no last element. With the code above, last([]) doesn’t hit the len == 1 base case, so it recurses into last([])… forever (or crashes when it tries xs[1:] on nothing, depending on the language). That’s a real bug worth naming.

A function that returns a sensible result for every input in its type is called total. One that blows up or loops on some inputs is partial. last as written is partial — it’s undefined on [].

You have two honest ways to make it total:

# Option 1: refuse the impossible input loudly.
def last(xs):
    if xs == []:
        raise ValueError("last() of empty list")
    if len(xs) == 1:
        return xs[0]
    return last(xs[1:])

# Option 2: return an "absent" value instead of a real element.
def last(xs):
    if xs == []:
        return None           # "no last element"
    if len(xs) == 1:
        return xs[0]
    return last(xs[1:])

Option 2 hints at something important coming later: instead of crashing, you can make “might be absent” an explicit, ordinary value (often called Option/Maybe/None) that the caller must deal with. We’ll build that idea properly when we reach algebraic data types.

The deeper lesson: your base cases aren’t an afterthought — they’re where you decide what your function even means. Choosing [] vs [x] vs “raise vs return None” is the design.

Next: counting a list recursively — and a first look at the accumulator pattern that makes recursion efficient (Lesson 5).