Chapters
Python114 chapters

FunctionsChapter 54 of 114

Recursion

A function that calls itself, and the base case that stops it.

The shape

A recursive function calls itself on a smaller version of the problem, and has a base case that does not recurse:

Python
def countdown(n):
    if n == 0:
        print("liftoff")
        return
    print(n)
    countdown(n - 1)

countdown(3)

Output

3
2
1
liftoff

Every recursive function needs two things:

  • a base case that returns without calling itself
  • a recursive step that moves towards that base case

Miss either and it never stops.

Factorial

The textbook example, because the maths is already recursive:

Python
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))

Output

120

factorial(5) waits for factorial(4), which waits for factorial(3), and so on down to the base case. Then the answers multiply back up the chain.

Python has a recursion limit

Each call takes a frame on the stack, and Python caps how deep that can go:

Python
import sys
print(sys.getrecursionlimit())

def deep(n):
    return deep(n + 1)

try:
    deep(0)
except RecursionError:
    print("RecursionError, as expected")

Output

1000
RecursionError, as expected

Python does not optimise tail calls, so a recursion that would be a loop in another language will hit this limit here. When the depth is proportional to your input size, write the loop.

Python
def factorial(n):
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

print(factorial(5))

Output

120

Where recursion earns its place

Not counting down — that is a loop. Recursion shines on data that is itself nested, where the depth is small and unknown:

Python
def total(items):
    running = 0
    for item in items:
        if isinstance(item, list):
            running += total(item)
        else:
            running += item
    return running

print(total([1, [2, 3], [4, [5, 6]]]))

Output

21

Writing that with a loop means managing your own stack of pending lists. The recursive version is the shape of the data.

Walking a nested dictionary is the same idea, and is how you would flatten JSON:

Python
def leaves(data):
    found = []
    for value in data.values():
        if isinstance(value, dict):
            found.extend(leaves(value))
        else:
            found.append(value)
    return found

tree = {"a": 1, "b": {"c": 2, "d": {"e": 3}}}
print(leaves(tree))

Output

[1, 2, 3]

Repeated work

Naive recursion can redo the same call enormously often. Fibonacci is the standard demonstration:

Python
calls = 0

def fib(n):
    global calls
    calls += 1
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(20), "computed with", calls, "calls")

Output

6765 computed with 21891 calls

Caching the results fixes it completely:

Python
from functools import lru_cache

@lru_cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(20))
print(fib.cache_info().currsize)

Output

6765
21

Twenty-one cached results instead of twenty-two thousand calls.

Test yourself

2 questions

What does every recursive function need?

Show the answer

A base case that returns without recursing — And a step that actually moves towards it. Missing either gives a RecursionError.

Why does deep recursion fail in Python where it would not in some other languages?

Show the answer

Python does not optimise tail calls and caps the stack depth — The default limit is 1000 frames. When depth grows with your input, write the loop.

Next chapter

Lambda

A small unnamed function, and when a def is the better choice.