Python Recursion

Write recursive functions with a reachable base case, trace the call stack and decide when recursion or a loop fits better.

Lesson 28 of 107Intermediate14 min read

Recursion is when a function calls itself to solve a smaller version of the same problem. Every recursive function has two parts: a base case that returns an answer directly, and a recursive step that calls the function on a smaller input and combines the result.

Python
def countdown(n):
    if n == 0:              # base case
        print("Lift-off")
        return
    print(n)
    countdown(n - 1)        # recursive step on a smaller problem

This lesson explains how to design both parts, what Python's call stack does while recursion runs, why missing a base case or a return breaks things, and when a plain loop is the better choice. The examples calculate ribbon for stacks of gift boxes.

Recursive step

The recursive step reduces the problem and calls the function again. "Reduce" is the key word: each call must move closer to the base case, usually by passing a smaller number, a shorter list or a sub-part of a structure.

Python
def ribbon_total(boxes):
    if boxes == 0:
        return 0
    return boxes + ribbon_total(boxes - 1)   # one fewer box each call

Here a stack of 4 boxes needs 4 + ribbon_total(3), which needs 3 + ribbon_total(2), and so on. If the step passed the same value again, ribbon_total(boxes), no call would ever get closer to the base case.

A useful way to design the step is to trust the recursion: assume the function already works correctly for the smaller input, and only ask "how do I build the answer for this input from that?" That mental shortcut is called the recursive leap of faith, and it keeps you from trying to trace every level in your head.

Base case

The base case is the input simple enough to answer without another call: zero boxes, an empty list, a single character, a leaf node in a tree. It must be checked before the recursive call, and every possible input must eventually reach it.

Python
def ribbon_total(boxes):
    if boxes < 0:
        raise ValueError("boxes cannot be negative")
    if boxes == 0:
        return 0
    return boxes + ribbon_total(boxes - 1)

Without the first check, ribbon_total(-1) would call itself with -2, -3 and so on, never reaching 0. Python stops runaway recursion with RecursionError: maximum recursion depth exceeded, by default after about 1,000 nested calls. Validating input before recursion starts, as above, turns a confusing crash into a clear message. Some problems have more than one base case, such as Fibonacci, where both 0 and 1 return directly.

Call stack

Each function call gets its own frame on the call stack, with its own local variables. A recursive call pauses the current frame until the deeper one returns:

Text
ribbon_total(3)
  3 + ribbon_total(2)            waiting
        2 + ribbon_total(1)      waiting
              1 + ribbon_total(0)  waiting
                    returns 0
              returns 1
        returns 3
  returns 6

On the way down, each frame remembers its own boxes value. On the way back up, each one adds its value to the result it receives.

Every waiting frame uses memory, which is why Python limits the depth. sys.getrecursionlimit() shows the limit, usually 1000. You can raise it with sys.setrecursionlimit(), but very deep recursion can still crash the interpreter, so treat that as a last resort. Python does not perform tail-call optimisation, so even a recursive call written as the last operation still adds a frame.

Combining results

After the recursive call returns, the current step combines its own contribution with that result, and must return the combined value:

Python
def longest_word(words):
    if len(words) == 1:
        return words[0]
    rest_best = longest_word(words[1:])
    return words[0] if len(words[0]) >= len(rest_best) else rest_best

Forgetting the return in front of a recursive call is the most common recursion bug. The deeper calls compute the right answer, but the outer call discards it and returns None.

The combining step is where recursion shines for branching structures. A folder's size is its files' sizes plus the size of each sub-folder; a tree's height is one plus the taller subtree. Writing those with loops requires managing your own stack, while recursion mirrors the shape of the data directly.

Independent practice and a worked check

Task: trace the ribbon total and reject negative counts before starting recursion. Add a depth parameter so each call prints its level, and raise ValueError for negative input.

Python
def ribbon_total(boxes, depth=0):
    if boxes < 0:
        raise ValueError("boxes cannot be negative")
    print("  " * depth + f"ribbon_total({boxes})")
    if boxes == 0:
        return 0
    return boxes + ribbon_total(boxes - 1, depth + 1)

print(ribbon_total(3))
Text
ribbon_total(3)
  ribbon_total(2)
    ribbon_total(1)
      ribbon_total(0)
6

The indentation shows each frame waiting for the one below it. For the worked check, recursive descent is terminated by a reachable base case: a condition that every input eventually meets and that returns without another call.

Reference and next practice

The sys.setrecursionlimit documentation explains the depth limit, and functools.lru_cache covers memoisation. Continue with recursive problem solving and trees and binary search trees. Practise with the Python exercises and Python quiz.

Recursion Summary

  • Recursive step: A recursive function calls itself on a smaller problem.
  • Base case: A base case returns without another recursive call.
  • Call stack: Active calls keep separate local state until deeper calls return.
  • Combining results: The current step combines its contribution with the smaller result.

Recursion Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Recursion in a small program

Python
def ribbon_total(boxes):
    if boxes == 0:
        return 0
    return boxes + ribbon_total(boxes - 1)

print(ribbon_total(4))
Output
10
Line-by-line explanation
  1. Lines 2 to 3The base case: zero boxes need no ribbon.
  2. Line 4The recursive step adds this stack's count to the total for one fewer box: 4 + 3 + 2 + 1 + 0 = 10.
  3. Line 6The first call starts the chain; the answer travels back up through every frame.
Example 2

Recursion over nested data

Recursion fits data that contains smaller copies of itself, like nested boxes.

Python
def count_items(box):
    """Count every item, including items inside nested boxes."""
    total = 0
    for thing in box:
        if isinstance(thing, list):
            total += count_items(thing)   # a smaller box
        else:
            total += 1
    return total

parcel = ["card", ["scarf", "gloves"], [["ring"], "candle"]]
print(count_items(parcel))
print(count_items([]))
Output
5
0
Line-by-line explanation
  1. Lines 5 to 6A nested list is a smaller version of the same problem, so the function calls itself on it.
  2. Lines 7 to 8A plain item is counted directly; this, together with an empty list, is the base case.
  3. Line 13An empty box returns 0 without any recursive calls.
Example 3

Recursion compared with a loop

Python
import sys

def total_recursive(n):
    return 0 if n == 0 else n + total_recursive(n - 1)

def total_loop(n):
    total = 0
    for i in range(1, n + 1):
        total += i
    return total

print(total_recursive(100), total_loop(100))
print(sys.getrecursionlimit())
try:
    total_recursive(5000)
except RecursionError as error:
    print("RecursionError:", error)
print(total_loop(5000))
Output
5050 5050
1000
RecursionError: maximum recursion depth exceeded
12502500
Line-by-line explanation
  1. Line 4The same sum as a one-line recursive function.
  2. Lines 14 to 155,000 nested calls exceed the default limit of 1,000.
  3. Line 17The loop has no depth limit and handles any size.

Common Recursion Mistakes

Forgetting to return the recursive result

Don't
def ribbon_total(boxes):
    if boxes == 0:
        return 0
    boxes + ribbon_total(boxes - 1)   # result thrown away

print(ribbon_total(3))   # TypeError on the way back up
Do
def ribbon_total(boxes):
    if boxes == 0:
        return 0
    return boxes + ribbon_total(boxes - 1)

Without return, each call returns None, and the next level up then tries to add a number to None.

A base case that some inputs never reach

Don't
def halve_until_one(n):
    if n == 1:
        return 0
    return 1 + halve_until_one(n / 2)   # 6 -> 3 -> 1.5 -> 0.75 ... never 1
Do
def halve_until_one(n):
    if n <= 1:
        return 0
    return 1 + halve_until_one(n // 2)

Use <= style base cases and integer steps so every input is guaranteed to stop.

Recursion in Real Projects

Where recursion is used

Recursion is the natural tool for hierarchical data: walking folders and sub-folders, processing nested JSON, rendering comment threads, evaluating expressions in a calculator, and searching trees and graphs. Sorting algorithms such as merge sort and quicksort are defined recursively, and many puzzle solvers use recursive backtracking.

For straight-line repetition, such as summing numbers or processing every line of a file, professional Python code uses loops, because they have no depth limit and are faster. When a recursive solution recalculates the same inputs many times, as naive Fibonacci does, functools.lru_cache remembers results and turns an exponential algorithm into a fast one. The recursive problem solving lesson applies these techniques to larger problems.

Recursion FAQ

What is a base case in recursion?

The base case is the condition under which a recursive function returns an answer directly, without calling itself again. It stops the chain of calls. Every recursive function needs at least one, and every input must eventually reach it.

What causes RecursionError in Python?

Too many nested calls, usually because the base case is missing, wrong or unreachable for some inputs. Python's default limit is about 1,000 frames. If the recursion is correct but genuinely deep, rewrite it as a loop rather than raising the limit.

Is recursion slower than a loop in Python?

Usually yes. Each call creates a stack frame, which costs time and memory, and Python doesn't optimise tail calls. For simple repetition, loops are faster and have no depth limit. Recursion is worth it when it makes code for nested or branching problems much clearer.

Can every recursive function be written as a loop?

Yes. Any recursion can be converted to iteration, sometimes by managing an explicit stack (a list) yourself. For linear problems the loop is easy; for trees and backtracking the recursive version is usually far shorter and easier to get right.

How do I make a slow recursive function faster?

If it calls itself repeatedly with the same arguments, add @functools.lru_cache (or @functools.cache from Python 3.9) to store results. Naive Fibonacci of 35 takes seconds; with caching it is instant.

Recursion Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What terminates recursive descent?

Multiple choicemedium

Which review concern is valid for recursion?

Recursion Quiz

2 questions to check what you learned.

  1. What terminates recursive descent?

    • A print statement
    • A reachable base case
    • Python ignores the distinction
  2. Which statement correctly describes call stack?

    • Active calls keep separate local state until deeper calls return.
    • Only the example output matters, not the rule
    • Every operation has the same behavior