Recursion Explained Without the Headache
Understand recursion with a step-by-step call stack walk-through of factorial, real Python examples, and clear rules for when a loop is the better tool.
Recursion has a reputation problem. Most explanations either hand you a one-liner ("a function that calls itself") and move on, or bury you in mathematical induction. Neither helps you actually trace what happens when the code runs.
So let's do it properly: what recursion is, the two parts every recursive function needs, a step-by-step walk through the call stack, a real-world example where recursion genuinely wins, and honest guidance on when you should just use a loop. All examples are Python, and all of them run.
What recursion actually is
Recursion is a way of solving a problem by solving a smaller version of the same problem, and trusting that the smaller version works.
Think of a stack of nested boxes, each containing a smaller box, with a coin in the smallest one. How do you find the coin? Open the box. If the coin is there, done. If there's another box inside, apply the exact same procedure to it. You don't need a different plan for each layer. One rule, applied repeatedly to smaller and smaller versions, solves the whole thing.
In code, that looks like a function that calls itself with a smaller input. The trick that makes it work, and the part beginners are rightly suspicious of, is that each call is a separate, independent execution with its own local variables. We'll see exactly how in a moment.
The two parts every recursive function needs
Every correct recursive function has exactly two pieces:
- A base case. The smallest version of the problem, answered directly, with no further recursion. This is the coin at the bottom of the boxes.
- A recursive case. Every other version, solved by doing a little work and delegating the rest to a call on a smaller input.
Miss the base case, or write a recursive case that never actually shrinks the problem, and the function calls itself forever. Well, not forever; Python will stop you, and we'll cover that crash below.
When you read or write recursive code, check those two things first. Where does it stop? And does every call move closer to that stopping point? If both answers are solid, the function works. That's the whole discipline.
Factorial, step by step through the call stack
Factorial is the classic first example because the math is trivial: 4! means 4 * 3 * 2 * 1, which is 24. Notice the structure hiding in it: 4! is just 4 * 3!. The problem contains a smaller copy of itself, which is the signal that recursion fits.
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1) # recursive case
print(factorial(4)) # 24
Now the important part: what actually happens when you call factorial(4)?
Python uses a call stack, a pile of "stack frames" where each frame holds one function call's local variables and its place in the code. When a function calls another function (including itself), the current frame pauses and a new frame goes on top of the pile. When a function returns, its frame pops off and the one underneath resumes exactly where it left off.
Here's factorial(4) unfolding, then collapsing:
call factorial(4) needs 4 * factorial(3), so it waits
call factorial(3) needs 3 * factorial(2), so it waits
call factorial(2) needs 2 * factorial(1), so it waits
call factorial(1) base case! returns 1
factorial(2) resumes: returns 2 * 1 = 2
factorial(3) resumes: returns 3 * 2 = 6
factorial(4) resumes: returns 4 * 6 = 24
Two things to internalize from this trace:
- The
nin each frame is a different variable.factorial(4)has its ownnequal to 4, sitting frozen whilefactorial(3)runs with its ownnequal to 3. They never interfere. - The work happens on the way back up. The calls descend until they hit the base case, and then the multiplications happen as each frame resumes and returns. Nothing is multiplied until
factorial(1)answers.
If recursion has ever felt like magic, it's usually because this pause-and-resume mechanic was never made explicit. There is no magic. It's a pile of paused function calls, resolved from the top down.
A second example: measuring a directory tree
Factorial is a teaching toy; you'd write it with a loop in real life. Here's a problem where recursion is the natural tool: computing the total size of a folder, including everything nested inside it.
The reason a plain loop struggles here is that you don't know the depth in advance. A folder can contain folders that contain folders, arbitrarily deep. But the recursive structure of the problem is clean: the size of a folder is the sum of the sizes of its children, and each child is either a file (easy) or another folder (same problem, smaller).
from pathlib import Path
def total_size(path):
path = Path(path)
if path.is_file():
return path.stat().st_size # base case: a file has a known size
total = 0
for child in path.iterdir():
total += total_size(child) # recursive case: measure each child
return total
print(total_size("my_project"))
Check our two requirements. Base case: a file, answered directly from its metadata. Recursive case: a directory, answered by delegating to each child. Does every call shrink the problem? Yes, each recursive call handles one entry inside the current folder, so we always move downward toward files.
This shape, "the data is a tree, so the code mirrors the tree," is where recursion earns its keep. File systems, nested JSON, HTML documents, organization charts, comment threads with replies: all trees, all naturally recursive.
When recursion beats loops, and when it doesn't
Reach for recursion when:
- The data is recursive. Trees and nested structures, as above. A recursive function is often a third the length of the loop version and far easier to verify.
- The algorithm divides the problem. Binary search, merge sort, and most "divide and conquer" algorithms state their logic most clearly as recursion.
- You're exploring branching possibilities. Backtracking problems like solving a maze or generating combinations get messy fast without it.
Prefer a loop when:
- The repetition is linear. Summing a flat list, counting items, processing rows one by one. A
forloop is clearer, faster, and safer. - The input can be large and flat. Recursing once per element of a big list will hit Python's depth limit; more on that next.
- Performance is tight. Each function call in Python has real overhead. Python also does not optimize tail calls, so even "loop-shaped" recursion pays the full cost.
A rule of thumb that serves beginners well: recursion for nested data, loops for flat data.
Stack overflow: when recursion runs out of room
Every paused call occupies memory on the call stack, and the stack is finite. Python defends itself with a recursion limit, roughly 1,000 frames by default. Blow past it and you get a RecursionError:
import sys
print(sys.getrecursionlimit()) # typically 1000
factorial(5000)
# RecursionError: maximum recursion depth exceeded
You'll hit this in two situations. The first is a bug: your base case is missing, or the recursive case doesn't actually shrink the input, so the calls never stop. The fix is to fix the logic. The second is legitimate depth: correct code, but input deeper than the limit, like factorial(5000) or an extremely nested structure. You can raise the limit with sys.setrecursionlimit(), but treat that as a signal. If plain depth is breaking you, an iterative rewrite is usually the sturdier fix.
Note the directory example is rarely at risk: its depth matches the folder nesting, and real file systems are dozens of levels deep, not thousands.
A teaser: memoization
One more idea, because you'll meet it soon after recursion clicks. The naive recursive Fibonacci recomputes the same subproblems over and over; fib(50) this way makes billions of calls. Caching results as you compute them, called memoization, collapses that to 51:
from functools import cache
@cache
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(50)) # 12586269025, instantly
One decorator from the standard library turns an unusable function into an instant one. Memoization is the doorway into dynamic programming, a core interview topic, but that's a story for another article.
Frequently asked questions
Is recursion faster than a loop?
In Python, usually the opposite. Function calls have overhead, and Python doesn't optimize tail calls. You choose recursion for clarity on nested problems, not for speed. When the two are equally clear, the loop typically wins on performance.
Can every recursive function be rewritten as a loop?
Yes. Any recursion can be converted to iteration, sometimes trivially (factorial), sometimes by managing your own explicit stack (tree traversals). That's what makes the choice a matter of clarity rather than capability.
Why do I keep getting RecursionError?
Check three things in order: does a base case exist, does every recursive call actually move toward it, and is your input simply deeper than Python's limit of about 1,000 frames? The first two are bugs; the third is a design signal to consider iteration.
Do coding interviews still ask recursion questions?
Constantly. Trees, graphs, backtracking, and dynamic programming all lean on recursive thinking, and they're staples of technical interviews. If you can trace a call stack on a whiteboard the way we traced factorial(4), you're ahead of most candidates.
Your next step
Don't just read this twice; trace it once. Take factorial(3), write the call stack on paper, and collapse it by hand. Then write total_size yourself and run it on a real folder. Recursion clicks through your fingers, not your eyes.
When you're ready to put it to work on trees, sorting, and interview-style problems, the Data Structures & Algorithms course builds recursion up carefully with interactive exercises. And if the Python itself still feels shaky, start with the free Python Essentials course first, then come back for the algorithms.
Kodion Team
Kodion Editorial
Written by the team that builds Kodion's courses: working engineers who test every example in the interactive editor before it ships. How we create and review content
Continue Learning
📚 Related Articles
Big O Notation for Beginners: Finally Understand Time Complexity
Big O notation explained in plain English: what it really measures, the five complexities that matter, and how to read the runtime of any code you write.
Build Your First REST API: A Beginner's Roadmap
Design and build a working REST API in Python with FastAPI: endpoints, Pydantic models, status codes, and curl tests, explained step by step.