What you'll learn
Quick Answer
A recursive function calls itself on a smaller version of the problem and stops at a base case. Write the base case first. Do not try to trace every call in your head — assume the smaller call is already correct and check that your combining step is right.
Every recursive function has the same two parts
A base case that stops, and a recursive case that moves towards it. That is the whole structure.
def fact(n):
if n <= 1: # base case: stop here
return 1
return n * fact(n - 1) # recursive case: smaller problem
print(fact(5)) # 120
Beginners usually write the recursive case first and then bolt on a base case when it crashes. Reverse that. Ask "what is the smallest input, and what is the answer for it?" first. For factorial that is 1. Once the stopping point exists, the recursive line usually writes itself.
Do not trace it in your head
This is the single biggest reason recursion feels hard. People try to follow every call down and back up, run out of working memory around the third level, and conclude they are bad at it.
Professionals do not do that. They use the recursive leap of faith: assume the smaller call already returns the correct answer, and check only that you combine it correctly.
For fact(5), assume fact(4) correctly returns 24. Is 5 * 24 the right answer for fact(5)? Yes. Done — the function is correct, and you never traced anything.
Verify three things and you can stop worrying: the base case is right, each call genuinely moves towards it, and the combining step is right.
What actually happens, and the limit
Each call is added to the call stack and stays there until it returns. Python caps that depth deliberately:
import sys
print(sys.getrecursionlimit()) # 1000
def bad(n):
return bad(n + 1) # no base case
bad(0) # RecursionError
The limit of 1000 exists to turn runaway recursion into a clean error rather than a crash. If you hit RecursionError, the problem is almost always a missing or unreachable base case — not a limit that needs raising. Raising the limit to hide a bug turns a clear error into a hard crash.
It also means recursion depth is bounded in Python. Recursing once per element over a list of 100,000 items will not work, even if the logic is correct.
Fibonacci, and why the naive version is unusable
Fibonacci is the standard second example, and it hides something important. The obvious version calls itself twice:
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
This recomputes the same values enormously many times — fib(30) makes over a million calls, and the work roughly doubles with each increment of n. It is correct and effectively unusable.
Remembering answers you have already computed fixes it completely:
def fib(n, memo={}):
if n in memo: return memo[n]
if n < 2: return n
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
print(fib(30)) # 832040, instantly
That is memoisation, and it is the entry point to dynamic programming. The lesson generalises: when a recursive solution is slow, the cause is usually recomputing the same subproblem, not recursion itself.
One caution — the mutable default memo={} is shared across calls. Here that is intentional and helpful, but it is the same mechanism that causes bugs elsewhere; see OOP in Python for where it bites.
When recursion is the right choice
Recursion wins when the data is itself recursive — trees, nested folders, nested JSON, graphs. Walking a directory tree recursively is natural; doing it with a loop means managing your own stack, which is more code and more bugs.
For simple counting or iterating over a flat list, a loop is clearer and has no depth limit. "Can it be recursive" and "should it be" are different questions, and interviewers do ask the second one.
Recursive problems worth practising: tree traversal, permutations, binary search, and directory walking. Those four cover most of what appears in interviews — see the time complexity cheat sheet for how to reason about their cost.
