PYTHON • ALGORITHMS AND COMPLEXITY

Writing Recursive Functions — Write recursive functions with correct base cases

Learn how functions can call themselves to elegantly solve problems that contain smaller versions of themselves.

Historical Context & Motivation

The idea of defining something in terms of itself might sound circular, but recursion is one of the most powerful concepts in computer science. Long before electronic computers existed, mathematicians realized that many natural processes — from the branching of trees to the spirals of seashells — follow patterns where each piece mirrors the whole. Translating that observation into a programming technique took decades of theoretical work, but today recursion sits at the heart of algorithms you use every time you search the web or sort a playlist.

1889
Peano Axioms
Giuseppe Peano defined the natural numbers recursively: start with 0, and every number is the successor of the one before it. This gave recursion a rigorous mathematical foundation.
1936
Church & Turing
Alonzo Church's lambda calculus and Alan Turing's theoretical machine both showed that recursion is fundamental to computation itself — any computable function can be expressed recursively.
1958
LISP Is Born
John McCarthy created LISP, the first programming language built around recursion. LISP proved that recursive functions could be practical, not just theoretical.
1991
Python Released
Guido van Rossum released Python, which supports recursion with clean, readable syntax. Python became a popular language for teaching recursive thinking to new programmers.

So why do we need recursion when we already have loops? The key insight is that some problems are naturally self-similar — they contain smaller copies of themselves. Calculating a factorial, navigating a file system, or solving a maze are all tasks where each step looks like a miniature version of the original problem. Recursion lets you write code that mirrors this structure directly, making solutions shorter, clearer, and often easier to reason about.

Core Principles of Recursion

Every correct recursive function rests on two pillars. First, it needs a base case — a condition under which the function stops calling itself and returns a known value. Second, it needs a recursive case — a step that breaks the problem into a smaller version and calls the function again. If either piece is missing or wrong, the function either never finishes or produces incorrect results.

1

Base Case

The stopping condition. When the input is small or simple enough, the function returns a result directly without making another recursive call. Without this, the function runs forever.
2

Recursive Case

The self-referential step. The function calls itself with a smaller or simpler input, trusting that the recursive call will eventually reach the base case and return a correct answer.
3

Progress Toward Base Case

Each recursive call must bring the input closer to the base case. For example, if the base case is n == 0, each call should reduce n. This guarantees the recursion terminates.
4

The Call Stack

Python keeps track of each recursive call on a structure called the call stack. Each call waits for the one below it to finish. When the base case is hit, results cascade back up the stack.
✦ KEY TAKEAWAY
Think of recursion like looking into a set of Russian nesting dolls. You open each doll to find a smaller one inside (the recursive case). Eventually you reach the tiniest doll that doesn't open (the base case). Then you put them all back together from smallest to largest — that's how the answers flow back up the call stack.

Visualizing the Recursive Call Stack

The diagram below traces how Python computes factorial(4) step by step. Notice how each call waits for the next one to finish. Once the base case returns 1, the results multiply back up through each waiting frame, ultimately producing 24.

The left column shows each recursive call descending toward the base case factorial(0) (highlighted in cyan). The right column shows the return values cascading back up, multiplying at each level until the final answer of 24 is produced.

This pattern applies to every recursive function. The calls descend with each recursive step, making the problem smaller. When they hit the base case, results begin ascending back through the stack. If you forget the base case, the purple arrows keep going down forever — Python will eventually crash with a RecursionError.

How Recursive Functions Work in Python

Let's break down the anatomy of a recursive function in Python. The factorial function is the classic starting point: n! means multiplying all positive integers from 1 to n. We can express this recursively because n! = n × (n−1)!, and we know that 0! = 1.

RECURSIVE DEFINITION OF FACTORIAL
n! = n × (n − 1)! where 0! = 1
n is a non-negative integer. The base case is n == 0, which returns 1. The recursive case multiplies n by the factorial of (n − 1).

Python Implementation

Here is how you translate the mathematical definition into Python code:

💻 Factorial in Python
def factorial(n): if n == 0: # Base case return 1 else: # Recursive case return n * factorial(n - 1) Line 2 checks if we've reached the base case. If n is 0, return 1 immediately. Otherwise, line 5 calls the function again with a smaller input.

The Three Rules of Recursion

RULE 1 — BASE CASE EXISTS
f(0) = known value
There must be at least one input for which the function returns a result without calling itself.
RULE 2 — RECURSIVE CASE MAKES PROGRESS
f(n) calls f(n − 1) or f(n / 2), etc.
Each recursive call must use an input that is closer to the base case than the current input.
RULE 3 — TRUST THE RECURSION
Assume f(n − 1) returns the correct answer
When writing the recursive case, assume the recursive call works correctly. Focus only on how to combine that result with the current input.

Common Recursive Patterns & Base Case Strategies

Choosing the right base case is the most critical decision when writing a recursive function. A missing base case causes infinite recursion, while an incorrect base case produces wrong answers. The diagram below shows four common patterns and their base cases.

Four common recursive patterns. The Countdown pattern decreases a number. Divide & Conquer halves a range. Shrinking Collection removes one element. Multi-Branch makes two recursive calls and may need multiple base cases.
⚠️ Common Mistake: Missing or Wrong Base Case
If you write def factorial(n): return n * factorial(n - 1) without a base case, Python will call factorial(4), then factorial(3), ... factorial(0), factorial(−1), factorial(−2) ... and never stop. Python's default recursion limit is 1000 calls, after which it raises a RecursionError. Always write the base case first, then the recursive case.

Worked Example: Recursive Sum of Digits

Let's write a function that takes a non-negative integer and returns the sum of its digits. For example, sum_digits(1234) should return 10 (because 1 + 2 + 3 + 4 = 10). We'll build it step by step, applying the three rules of recursion.

Writing sum_digits(n) Recursively
1
Step 1 — Identify the Base CaseWhat is the simplest input? A single-digit number (0 through 9). When n < 10, the sum of its digits is just n itself. So our base case is: if n < 10: return n.
Base case: n < 10 → return n
2
Step 2 — Define the Recursive CaseFor a number with multiple digits, we can extract the last digit using n % 10 (the remainder when dividing by 10). The remaining digits are n // 10 (integer division by 10). So sum_digits(1234) equals 4 + sum_digits(123).
Recursive case: return (n % 10) + sum_digits(n // 10)
3
Step 3 — Verify Progress Toward Base CaseEach call uses n // 10, which always produces a smaller number. For example, 1234 → 123 → 12 → 1. Since 1 is less than 10, we hit the base case. Progress is guaranteed.
✓ n // 10 always moves toward the base case
4
Step 4 — Write the Complete Functiondef sum_digits(n): if n < 10: return n return (n % 10) + sum_digits(n // 10) That's it — four lines of clean, readable code.
Complete function written and verified
5
Step 5 — Trace the ExecutionLet's trace sum_digits(1234): sum_digits(1234) = 4 + sum_digits(123) sum_digits(123) = 3 + sum_digits(12) sum_digits(12) = 2 + sum_digits(1) sum_digits(1) = 1 ← base case! Now the returns cascade: 1 → 2 + 1 = 3 → 3 + 3 = 6 → 4 + 6 = 10.
sum_digits(1234) = 10 ✓

Recursion vs. Iteration: Strengths & Limitations

Any recursive function can be rewritten as an iterative one using loops, and vice versa. So when should you choose recursion over iteration? The table below compares the two approaches across several important dimensions.

Comparison of recursive and iterative approaches
DimensionRecursionIteration (Loops)
Code ReadabilityOften mirrors the mathematical definition; very readable for naturally recursive problems like trees and graphsCan be more verbose but familiar to most beginners; straightforward for simple loops
Memory UsageEach call adds a frame to the call stack; deep recursion can cause stack overflowUses constant extra memory for a loop variable; no stack buildup
SpeedFunction call overhead makes recursion slightly slower; naive Fibonacci is exponentially slowGenerally faster due to no function call overhead
Problem FitIdeal for tree traversal, divide-and-conquer algorithms, backtracking, and problems with self-similar structureIdeal for simple counting, array scanning, and tasks with a fixed number of repetitions
Python LimitDefault recursion limit is 1000 calls deep; can be adjusted but risks crashing the programNo inherent limit on loop iterations
✦ WHEN TO USE RECURSION
Choose recursion when the problem itself is recursive — when each piece looks like a smaller copy of the whole. Think of it like giving directions: 'Walk to the corner, then follow the same directions from there.' If the problem is more like 'Walk 100 steps forward,' a simple loop is the better tool.

Connection to Advanced Recursion Techniques

Once you're comfortable writing basic recursive functions with correct base cases, there's a whole world of advanced techniques that build on the same principles. Understanding base cases is the foundation for everything that follows.

How this lesson connects to advanced recursion topics
This LessonAdvanced TechniqueWhat's New
Single base caseMultiple base casesSome problems need 2+ stopping conditions (e.g., Fibonacci needs n == 0 and n == 1)
One recursive callTree recursionFunctions that call themselves twice or more, creating a branching tree of calls
Simple return valueMemoizationStoring previously computed results to avoid redundant recursive calls; essential for Fibonacci
Stack builds upTail recursionRestructuring the recursive call so it's the very last operation, enabling optimization in some languages (not Python)
Direct self-callMutual recursionTwo functions calling each other — function A calls B, which calls A, and so on

All of these advanced techniques still rely on the core ideas from this lesson. You'll always need a well-defined base case, a recursive step that makes progress, and the trust that the recursion handles the smaller problem. Master these fundamentals, and the advanced material will feel like a natural next step.

Practice Problems

PROBLEM 1 — CONCEPTUAL
A student writes the following function: def countdown(n): print(n) countdown(n - 1) What will happen when countdown(5) is called? What is missing, and how would you fix it?
PROBLEM 2 — BASIC CALCULATION
Write a recursive function power(base, exp) that computes baseexp (where exp is a non-negative integer). Identify the base case and trace power(3, 4).
PROBLEM 3 — INTERMEDIATE
Write a recursive function reverse_string(s) that returns the reverse of a string. For example, reverse_string('hello') should return 'olleh'. Identify the base case and explain why your recursive step makes progress.
PROBLEM 4 — APPLIED
You're writing a program to count files in a folder structure. Each folder can contain files and subfolders. The data is represented as a nested dictionary: folder = { 'files': ['a.txt', 'b.txt'], 'subfolders': [ {'files': ['c.txt'], 'subfolders': []}, {'files': [], 'subfolders': [ {'files': ['d.txt', 'e.txt'], 'subfolders': []} ]} ] } Write a recursive function count_files(folder) that returns the total number of files across all folders and subfolders.
PROBLEM 5 — CRITICAL THINKING
Consider this recursive function: def mystery(n): if n == 1: return 0 if n % 2 == 0: return 1 + mystery(n // 2) else: return 1 + mystery(3 * n + 1) (a) Trace mystery(6) and find the return value. (b) Does this function always terminate? Can you prove it, or is there a reason you can't? (c) What happens if you change the base case to n == 0 instead of n == 1?

Lesson Summary

A recursive function is one that calls itself to solve smaller versions of the same problem. Every correct recursive function needs two essential components: a base case that stops the recursion and returns a known value, and a recursive case that breaks the problem down and calls the function again with a smaller input. The recursive case must always make progress toward the base case to guarantee the function eventually terminates.

We explored four common patterns: the countdown pattern (factorial, power), divide and conquer (binary search), shrinking collection (list sum, string reversal), and multi-branch recursion (Fibonacci). Python tracks each call on the call stack, with a default limit of 1000 frames. Recursion shines for problems with self-similar structure, while iteration is better for simple, flat repetition. Mastering base cases is the foundation for advanced techniques like memoization and tree recursion.

Varsity Tutors • Python • Writing Recursive Functions — Write recursive functions with correct base cases