Coding · Algorithms
Recursion explained: how it works, with Python examples
Short answer
Recursion is a technique where a function solves a problem by calling itself on a smaller version of the same problem. Every recursive function needs a base case that returns without recursing and a recursive case that moves toward the base case. Each call waits on the call stack until the smaller call returns. Recursion fits naturally to trees, nested data and divide-and-conquer algorithms such as merge sort.
What problem recursion solves
Some problems are defined in terms of themselves. A folder contains files and other folders. A tree node has child trees. The factorial of n is n times the factorial of n − 1. Writing these with plain loops often needs an explicit stack of pending work; recursion lets the language keep that stack for you.
A recursive function has two parts:
- Base case: the smallest input, answered directly. For factorial,
0! = 1. - Recursive case: reduce the problem, call the function on the smaller part, and combine the result. For factorial,
n! = n × (n − 1)!.
def factorial(n):
if n == 0: # base case
return 1
return n * factorial(n - 1) # recursive case
print(factorial(5)) # 120The call stack, traced step by step
Each call gets its own frame on the call stack, holding its own copy of n. Calls go down until the base case, then the results come back up in reverse order.
| Step | Call | State |
|---|---|---|
| 1 | factorial(3) | waits for factorial(2) |
| 2 | factorial(2) | waits for factorial(1) |
| 3 | factorial(1) | waits for factorial(0) |
| 4 | factorial(0) | base case, returns 1 |
| 5 | factorial(1) | returns 1 × 1 = 1 |
| 6 | factorial(2) | returns 2 × 1 = 2 |
| 7 | factorial(3) | returns 3 × 2 = 6 |
The stack depth here is n + 1 frames, so the extra space is O(n) even though no list is created.
Recursion on nested data
Recursion is most useful when the shape of the data is itself recursive. This function sums all numbers in a list that may contain other lists, at any depth.
def deep_sum(items):
total = 0
for x in items:
if isinstance(x, list):
total += deep_sum(x) # same problem, smaller piece
else:
total += x
return total
print(deep_sum([1, [2, 3], [4, [5, [6]]]])) # 21The same pattern walks a directory tree, a JSON document, an HTML DOM or a binary tree: handle the current node, then recurse into each child.
def power_set(items):
if not items:
return [[]]
rest = power_set(items[1:])
return rest + [[items[0]] + s for s in rest]
print(power_set([1, 2, 3]))
# [[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]Complexity of recursive functions
Estimate the cost as (number of calls) × (work per call). Space is at least the maximum stack depth.
| Function | Calls | Time | Stack space |
|---|---|---|---|
| factorial(n) | n + 1 | O(n) | O(n) |
| binary search, recursive | about log₂ n | O(log n) | O(log n) |
| merge sort | about 2n | O(n log n) | O(log n) stack, O(n) buffer |
| naive Fibonacci | about 1.6ⁿ | O(2ⁿ) bound | O(n) |
| power set | n + 1 | O(n · 2ⁿ) | O(n) stack, output is 2ⁿ lists |
Divide-and-conquer recurrences such as T(n) = 2T(n/2) + O(n) for merge sort solve to O(n log n). A function that makes two calls on n − 1 grows exponentially.
Memoization: stop repeating work
The naive Fibonacci function recomputes the same values many times: fib(5) calls fib(3) twice and fib(2) three times. Caching each result the first time it is computed turns exponential time into linear time. Python's functools.lru_cache (or functools.cache, Python 3.9+) does this with one decorator.
from functools import lru_cache
def fib_slow(n):
return n if n < 2 else fib_slow(n - 1) + fib_slow(n - 2)
@lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib_slow(20)) # 6765, about 22,000 calls
print(fib(90)) # 2880067194370816120, 91 distinct callsMemoized recursion is the top-down form of dynamic programming. The bottom-up form fills a table with a loop instead.
Recursion limits and converting to a loop
You can raise the limit with sys.setrecursionlimit(), but a very deep recursion can still crash the interpreter. For input that may be deep, such as a linked list of a million nodes or a degenerate tree, rewrite the function with a loop and an explicit stack.
def deep_sum_iter(items):
total, stack = 0, [items]
while stack:
current = stack.pop()
for x in current:
if isinstance(x, list):
stack.append(x)
else:
total += x
return total
print(deep_sum_iter([1, [2, 3], [4, [5, [6]]]])) # 21Common mistakes
- Missing or unreachable base case, for example
factorial(-1)with the code above never reaches 0 and ends inRecursionError. - A recursive case that does not shrink the input, such as calling
f(n)fromf(n). - Forgetting to
returnthe recursive result:factorial(n - 1)withoutreturngivesNone. - Copying data on every call.
items[1:]copies the list, so recursing on slices of a long list costs O(n²) time; pass an index instead. - Using a mutable default argument such as
def f(x, seen=[])to carry state between calls; the same list is shared by every call to the function.
When to use recursion
Use recursion when the data or the problem is naturally recursive: trees, graphs (depth-first search), nested structures, parsing expressions, backtracking (permutations, N-Queens, sudoku) and divide and conquer (merge sort, quicksort, binary search). Use a loop when the problem is a simple sequence, or when the depth can grow with the input size in Python.
In interviews, recursion questions often check that you can state the base case, the recursive case, the time and space cost including the stack, and when memoization helps.
FAQ
What is a base case in recursion?
The base case is the input a recursive function answers directly, without calling itself. It stops the recursion; without it, the function calls itself until the stack overflows.
Is recursion slower than a loop?
In Python, usually a little, because each function call has overhead and uses a stack frame. The complexity is often the same; the main practical risk is the recursion depth limit.
What is the maximum recursion depth in Python?
CPython's default limit is 1000 frames, which you can read with sys.getrecursionlimit() and change with sys.setrecursionlimit(). Exceeding it raises RecursionError.
What is tail recursion?
A call is a tail call when it is the last thing the function does before returning. Some languages reuse the stack frame for tail calls, but CPython does not, so tail recursion in Python still grows the stack.
Can every recursive function be written as a loop?
Yes. Any recursion can be converted to a loop that manages an explicit stack. For simple linear recursion such as factorial, a plain loop with an accumulator is enough.