Fundamentals · Lesson 19 of 94

Recursion

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

Concept

A recursive function is one that calls itself. That sounds circular, and it would be, except that each call works on a smaller piece of the problem, and there is always a case small enough to answer directly. Counting down is the simplest example: to count down from 5, say 5, then count down from 4. To count down from 0, stop. The "stop" is the base case; the "count down from a smaller number" is the recursive case.

Recursion: countdown(3) → print 3, call countdown(2) → print 2, call countdown(1) → print 1, call countdown(0) → base case: n == 0, return
Recursion · Figure 1 · Open for full size

Every recursion has exactly those two parts, and forgetting the base case is the recursive version of the infinite loop. Python protects you: after about a thousand nested calls it raises a RecursionError instead of running forever. When you see that error, the base case is missing or never reached.

Recursion works because each call gets its own local variables, as you learned two lessons ago. Python keeps the pending calls on a stack: countdown(3) is paused while countdown(2) runs, which is paused while countdown(1) runs, and so on. When the base case returns, each paused call resumes where it left off, in reverse order. Tracing this once by hand, on paper, is the moment recursion stops being mysterious.

Recursion: factorial(4) → factorial(3) → factorial(2) → factorial(1) → 24
Recursion · Figure 2 · Open for full size

Recursion is the natural fit for problems that contain smaller copies of themselves: folders that contain folders, nested lists inside lists, tree-shaped data like JSON, and "divide the problem in half" algorithms. For a plain sequence, a loop is usually clearer and always faster, because every recursive call costs memory on that stack. The rule of thumb: if you would explain the solution as "and then do the same thing to the smaller part", recursion fits; if you would say "for each item", use a loop.

Problem Natural tool Why
Sum a flat list loop one pass, no nesting
Sum a list that contains lists recursion the parts look like the whole
Walk a folder tree recursion folders contain folders
Count down from n either recursion is the teaching example, a loop is the real one

Code

The countdown, with the two cases labeled. Note that the base case comes first; checking it before recursing is the habit that prevents runaway calls.

# countdown.py
def countdown(n):
    if n == 0:            # base case
        print("liftoff")
        return
    print(n)
    countdown(n - 1)      # recursive case: a smaller problem

countdown(3)
Output
3
2
1
liftoff

Factorial, where the result is built on the way back up. Each call waits for the smaller call's answer and multiplies it.

# factorial.py
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

for n in range(0, 6):
    print(n, factorial(n))
print(factorial(20))
Output
0 1
1 1
2 2
3 6
4 24
5 120
2432902008176640000

A problem that genuinely needs recursion: summing a list whose items may themselves be lists, to any depth. A loop cannot know how deep to go; recursion does not need to.

# nested_sum.py
def nested_sum(items):
    total = 0
    for item in items:
        if isinstance(item, list):
            total += nested_sum(item)
        else:
            total += item
    return total

print(nested_sum([1, 2, 3]))
print(nested_sum([1, [2, 3], [4, [5, 6]]]))
print(nested_sum([[[[7]]]]))
Output
6
21
7

What a missing base case looks like. The traceback is long and repetitive, which is itself the clue: the same line keeps calling itself.

# no_base_case.py
import sys
sys.setrecursionlimit(50)   # fail fast for the demo; the default is 1000

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

forever(1)
Output
Traceback (most recent call last):
  File "no_base_case.py", line 7, in <module>
    forever(1)
    ~~~~~~~^^^
  File "no_base_case.py", line 5, in forever
    return forever(n + 1)
  File "no_base_case.py", line 5, in forever
    return forever(n + 1)
  File "no_base_case.py", line 5, in forever
    return forever(n + 1)
  [Previous line repeated 46 more times]
RecursionError: maximum recursion depth exceeded

Try it yourself

Create recurse.py with two functions. count_down_up(n) prints n, recurses, then prints n again after the call returns, so count_down_up(3) prints 3 2 1 1 2 3 on separate lines; trace on paper why. depth(items) returns how deeply nested a list is: depth([1, 2]) is 1, depth([1, [2, [3]]]) is 3.

Common mistakes

What's next

You have every tool for control flow now. Next you will learn to read the error messages that all of this code will inevitably produce, calmly and bottom-up.

Course overview