Base Case
The stopping condition. Every recursive function needs one. Write it first. Without it, you get infinite recursion and a crash.
You have probably heard the word recursion before and maybe it felt confusing or magical. It is actually one of the most elegant ideas in programming once it clicks - and this guide is going to make it click.
Okay, simple version: recursion is when a function calls itself.
That is it. A function that, at some point, calls itself again.
Here is a funny real-life example. When you search “recursion” on Google, it used to say:
Did you mean: recursion
Clicking that link takes you back to the same page. That is the joke - it loops back to itself. That is recursion.
In code, recursion is useful when you have a problem that looks exactly like a smaller version of itself. Instead of writing a long loop, you just say “hey, solve the smaller version of this, and I will handle the rest.”
Every single recursive function in the world needs exactly two things:
Rule 1: A base case
This is the “stop” condition. Without it, the function calls itself forever and your program crashes. Think of it like the bottom rung of a ladder - you need something to land on.
Rule 2: A recursive case
This is where the function actually calls itself, but with a smaller or simpler version of the problem. You always need to be moving toward the base case, not away from it.
Let us start with something super simple - printing numbers from 1 to N.
def print_numbers(n): if n == 0: # Base case - stop here return print_numbers(n - 1) # Recursive call with smaller input print(n)What happens when you call print_numbers(3)?
print_numbers(3) └── print_numbers(2) └── print_numbers(1) └── print_numbers(0) -> hits base case, stops -> prints 1 -> prints 2 -> prints 3Output: 1 2 3
Notice something important: the printing happens on the way back up, not on the way down. The function goes all the way to n = 0, then comes back and prints. This “going in and coming back out” behavior is core to how recursion works.
Every time your program calls a function, it puts that function on something called the call stack. Think of it like a stack of plates at a buffet - you add plates to the top and take from the top.
When you call print_numbers(3):
print_numbers(3) gets added to the stackprint_numbers(2), which gets added on topprint_numbers(1), added on topprint_numbers(0), added on topprint_numbers(0) hits the base case and returns - it gets removed from the stackprint_numbers(1) can finish - it prints 1, gets removedprint_numbers(2) finishes - prints 2, gets removedprint_numbers(3) finishes - prints 3, gets removedThe stack grows while going in, and shrinks while coming back out. Stack overflow happens when the stack grows too large - usually because the base case is missing.
You have probably seen factorial in math class. The factorial of 5 (written 5!) means 5 × 4 × 3 × 2 × 1 = 120.
Notice how 5! is really just 5 × 4!. And 4! is 4 × 3!. It keeps shrinking until you get to 1! = 1 (or 0! = 1). That is a perfect fit for recursion.
def factorial(n): if n == 0: # Base case return 1 return n * factorial(n - 1) # Recursive caseLet us trace through factorial(4):
factorial(4)= 4 * factorial(3)= 4 * 3 * factorial(2)= 4 * 3 * 2 * factorial(1)= 4 * 3 * 2 * 1 * factorial(0)= 4 * 3 * 2 * 1 * 1= 24Each call waits for the one below it to finish. Once factorial(0) returns 1, the values multiply all the way back up.
Here is the honest truth: most people try to trace through every single recursive call in their head and get lost. You do not need to do that.
The trick is called the “just trust it” method, and it works like this:
Assume the recursive call already works correctly - pretend you have a magic helper function that solves the smaller version perfectly.
Write the base case - what is the simplest possible input where you can answer without any help?
Write the recursive case - given that your helper works on smaller inputs, how do you combine its answer with the current step?
Test on a tiny example - just 2 or 3 steps, not the whole thing.
For factorial: “Assume factorial(n-1) gives me the right answer for n-1. Then factorial(n) is just n * factorial(n-1).” Done. You do not need to trace all 10 levels.
def array_sum(arr, i=0): if i == len(arr): # Base case - no more elements return 0 return arr[i] + array_sum(arr, i + 1)Think of it as: “The sum of this array is the first element plus the sum of the rest.”
A palindrome reads the same forwards and backwards (“racecar”, “madam”).
def is_palindrome(s, left, right): if left >= right: # Base case - checked all pairs return True if s[left] != s[right]: # Characters do not match - not a palindrome return False return is_palindrome(s, left + 1, right - 1)You check the outermost characters first, then move inward, one pair at a time.
def print_reverse(arr, i): if i == len(arr): return print_reverse(arr, i + 1) # Go to the end first print(arr[i]) # Print on the way backBy going to the end before printing, you print in reverse order - same trick as the first example.
Fibonacci is the sequence 0, 1, 1, 2, 3, 5, 8, 13... where each number is the sum of the two before it.
def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)This is called binary recursion because there are two recursive calls. Each call spawns two more calls, which each spawn two more… it looks like a tree.
For problems with multiple recursive calls, drawing a tree helps. For fib(4):
fib(4) / \ fib(3) fib(2) / \ / \ fib(2) fib(1) fib(1) fib(0) / \ fib(1) fib(0)Each level doubles the number of calls - this is O(2ⁿ) time complexity, which is why it gets slow fast.
How do you measure how expensive a recursive function is?
Time complexity - count the number of function calls:
n-1 -> O(n) (linear)Space complexity - how deep does the call stack get?
n calls uses O(n) spacefactorial(1000) # Stack goes 1000 levels deep -> O(n) spaceYou do not need to memorize these, but knowing the names helps when reading about recursion:
| Type | What It Means | Example |
|---|---|---|
| Tail recursion | The recursive call is the very last thing the function does | tail_factorial(n, acc) |
| Head recursion | The recursive call happens before any other work | Work happens on the way back up |
| Linear recursion | Only one recursive call per function | Factorial, array sum |
| Binary recursion | Two recursive calls per function | Fibonacci, subsequences |
Backtracking is a more advanced use of recursion. The idea: try something, explore the options it opens up, then undo it and try something else.
It follows a pattern called choose -> explore -> unchoose.
Here is how you would generate all permutations of a list:
def permute(nums, path, used): if len(path) == len(nums): print(path) return
for i in range(len(nums)): if used[i]: continue
used[i] = True # Choose permute(nums, path + [nums[i]], used) # Explore used[i] = False # Unchoose (backtrack)The “unchoose” step is what makes backtracking special - you put the world back to how it was before, then try a different path. It is like exploring a maze: you try one corridor, hit a dead end, and go back to the fork to try the next one.
A subsequence is any subset of characters in order - for the string "ab", the subsequences are "", "a", "b", "ab".
def subsequences(s, i, current): if i == len(s): print(current) return
subsequences(s, i + 1, current) # exclude current character subsequences(s, i + 1, current + s[i]) # include current characterAt each position, you make a binary choice: include or exclude. This creates a tree with 2ⁿ leaves - one for each possible subsequence.
Both recursion and loops can repeat work, but they shine in different situations:
| Recursion | Loops | |
|---|---|---|
| Code clarity | Very readable for tree/graph problems | Clear for simple repetition |
| Memory | Uses call stack (can overflow) | Constant memory |
| Risk | Stack overflow if base case is wrong | Infinite loop if condition is wrong |
| Best for | Trees, graphs, backtracking, divide & conquer | Simple counting, array scanning |
Recursion is powerful but not always the right tool:
n > 100,000) - the call stack will overflowPython’s default recursion limit is 1000 calls. You can raise it with sys.setrecursionlimit(), but that is usually a sign you should rethink your approach.
Forgetting the base case - your function will run until Python throws a RecursionError. Always write the base case first.
Wrong base condition - for example, using if n == 1 when n could be 0 or negative. Make sure your base case actually covers the smallest valid input.
Not making the problem smaller - if you call recursion(n) and pass the same n to the next call, you go in circles forever. Every recursive call must move closer to the base case.
Accidentally modifying shared data - if multiple calls share the same list or dictionary and one modifies it, things get confusing fast. Use copies or pass indices instead.
Before you write a single line of code, answer these four questions:
Base Case
The stopping condition. Every recursive function needs one. Write it first. Without it, you get infinite recursion and a crash.
Recursive Case
Where the function calls itself with a smaller input. You must always be moving toward the base case - never away from it.
Call Stack
Each function call gets stacked in memory. The stack unwinds when the base case is hit. Too many calls = stack overflow.
Trust the Recursion
Assume the recursive call works perfectly on smaller inputs. Focus on what your current call needs to do with that result.
Binary Recursion
Two recursive calls per function. Creates a tree of calls. Used for subsequences, Fibonacci, and many backtracking problems.
Backtracking
Choose something, explore all paths from there, then undo and try the next option. Used for permutations, mazes, puzzles.