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.
def countdown(n):
if n == 0: # base case
print("Lift-off")
return
print(n)
countdown(n - 1) # recursive step on a smaller problemThis 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.
def ribbon_total(boxes):
if boxes == 0:
return 0
return boxes + ribbon_total(boxes - 1) # one fewer box each callHere 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.
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:
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 6On 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:
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_bestForgetting 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.
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))ribbon_total(3)
ribbon_total(2)
ribbon_total(1)
ribbon_total(0)
6The 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.
Recursion in a small program
def ribbon_total(boxes):
if boxes == 0:
return 0
return boxes + ribbon_total(boxes - 1)
print(ribbon_total(4))10Line-by-line explanation
- Lines 2 to 3The base case: zero boxes need no ribbon.
- Line 4The recursive step adds this stack's count to the total for one fewer box: 4 + 3 + 2 + 1 + 0 = 10.
- Line 6The first call starts the chain; the answer travels back up through every frame.
Recursion over nested data
Recursion fits data that contains smaller copies of itself, like nested boxes.
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([]))5
0Line-by-line explanation
- Lines 5 to 6A nested list is a smaller version of the same problem, so the function calls itself on it.
- Lines 7 to 8A plain item is counted directly; this, together with an empty list, is the base case.
- Line 13An empty box returns 0 without any recursive calls.
Recursion compared with a loop
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))5050 5050
1000
RecursionError: maximum recursion depth exceeded
12502500Line-by-line explanation
- Line 4The same sum as a one-line recursive function.
- Lines 14 to 155,000 nested calls exceed the default limit of 1,000.
- Line 17The loop has no depth limit and handles any size.
Common Recursion Mistakes
Forgetting to return the recursive result
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 updef 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
def halve_until_one(n):
if n == 1:
return 0
return 1 + halve_until_one(n / 2) # 6 -> 3 -> 1.5 -> 0.75 ... never 1def 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.
What terminates recursive descent?
Which review concern is valid for recursion?
Recursion Quiz
2 questions to check what you learned.
What terminates recursive descent?
- A print statement
- A reachable base case
- Python ignores the distinction
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