Introduction
Python sets a default recursion depth limit of 1000 to prevent infinite recursion from crashing the interpreter with a stack overflow. While you can raise this limit, the better approach is usually to understand why the limit exists and how to write recursion that doesn’t hit it โ or convert to an iterative solution when needed.
The Default Recursion Limit
import sys
print(sys.getrecursionlimit()) # => 1000
When you exceed this limit, Python raises RecursionError:
def count_down(n):
return count_down(n - 1) # infinite recursion
count_down(0)
# RecursionError: maximum recursion depth exceeded
Changing the Recursion Limit
You can increase the limit with sys.setrecursionlimit():
import sys
sys.setrecursionlimit(5000)
print(sys.getrecursionlimit()) # => 5000
When this is appropriate:
- You have a well-bounded recursion that you know won’t go infinite
- You’re processing deeply nested data structures (e.g., deeply nested JSON, ASTs)
- You’ve verified the maximum depth your input can produce
When to be cautious:
- Each stack frame uses memory โ very deep recursion can exhaust RAM
- The actual stack size limit is set by the OS, not just Python; hitting it causes a hard crash (segfault), not a clean
RecursionError - Setting it to millions is dangerous
import sys
import resource
# Check the OS stack size limit (bytes)
soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
print(f"Stack limit: {soft / 1024 / 1024:.1f} MB")
# A rough safe maximum for recursion depth
# Each frame is ~1KB, so 8MB stack โ ~8000 frames
sys.setrecursionlimit(5000) # conservative for most systems
Common Recursion Patterns
Factorial
# Recursive โ hits limit at ~1000
def factorial_recursive(n):
if n <= 1:
return 1
return n * factorial_recursive(n - 1)
# Iterative โ no limit
def factorial_iterative(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
# Python built-in (best option)
import math
math.factorial(10000) # works fine
Fibonacci
# Naive recursive โ O(2^n), hits limit quickly
def fib_recursive(n):
if n <= 1:
return n
return fib_recursive(n - 1) + fib_recursive(n - 2)
# With memoization โ O(n) time, but still limited by depth
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_memo(n):
if n <= 1:
return n
return fib_memo(n - 1) + fib_memo(n - 2)
# Iterative โ O(n) time, O(1) space, no depth limit
def fib_iterative(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
print(fib_iterative(10000)) # works fine
Converting Recursion to Iteration
The general technique is to use an explicit stack (a list) to simulate the call stack:
Tree Traversal
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
# Recursive DFS โ limited by tree depth
def dfs_recursive(node):
if node is None:
return []
return [node.val] + dfs_recursive(node.left) + dfs_recursive(node.right)
# Iterative DFS โ no depth limit
def dfs_iterative(root):
if root is None:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
Flatten Nested Lists
# Recursive โ fails on deeply nested input
def flatten_recursive(lst):
result = []
for item in lst:
if isinstance(item, list):
result.extend(flatten_recursive(item))
else:
result.append(item)
return result
# Iterative โ handles any depth
def flatten_iterative(lst):
result = []
stack = list(lst)
while stack:
item = stack.pop()
if isinstance(item, list):
stack.extend(item)
else:
result.append(item)
return result[::-1] # reverse to restore order
nested = [1, [2, [3, [4, [5]]]]]
print(flatten_iterative(nested)) # => [1, 2, 3, 4, 5]
Directory Traversal
import os
from pathlib import Path
# Iterative directory walk โ handles deeply nested directories
def find_files(root, extension='.py'):
found = []
stack = [Path(root)]
while stack:
current = stack.pop()
for item in current.iterdir():
if item.is_dir():
stack.append(item)
elif item.suffix == extension:
found.append(item)
return found
# Or just use os.walk (already iterative)
for dirpath, dirnames, filenames in os.walk('/some/path'):
for filename in filenames:
if filename.endswith('.py'):
print(os.path.join(dirpath, filename))
Tail Call Optimization (TCO)
Python does not implement tail call optimization. In languages like Scheme or Haskell, a tail-recursive function (where the recursive call is the last operation) is automatically converted to a loop. Python doesn’t do this โ every recursive call adds a frame to the stack regardless.
# This looks like tail recursion, but Python still uses stack frames
def sum_tail(n, accumulator=0):
if n == 0:
return accumulator
return sum_tail(n - 1, accumulator + n) # tail call โ but Python doesn't optimize it
sum_tail(999) # OK
sum_tail(1001) # RecursionError
You can simulate TCO with a trampoline:
def trampoline(f):
"""Decorator that enables tail-call optimization via trampolining."""
def wrapper(*args, **kwargs):
result = f(*args, **kwargs)
while callable(result):
result = result()
return result
return wrapper
@trampoline
def sum_trampoline(n, acc=0):
if n == 0:
return acc
return lambda: sum_trampoline(n - 1, acc + n) # return a thunk instead of calling
print(sum_trampoline(100000)) # works without hitting recursion limit
When to Use Recursion vs Iteration
| Scenario | Recommendation |
|---|---|
| Tree/graph traversal | Recursion is natural; convert to iterative for deep trees |
| Divide and conquer (merge sort, quicksort) | Recursion is clean; depth is O(log n) โ usually fine |
| Fibonacci, factorial | Use iterative or math.factorial |
| Deeply nested data (depth > 500) | Use iterative with explicit stack |
| Mutual recursion | Consider converting to state machine |
| Simple, bounded depth | Recursion is fine and more readable |
Practical Checklist
- Know your maximum recursion depth for the given input
- Use
@lru_cacheor@functools.cacheto avoid redundant calls - For depth > 500, consider iterative conversion
- Never set
setrecursionlimitto millions โ use iteration instead - Use
os.walkorpathlibfor file system traversal (already iterative) - For production code processing user input, always bound the recursion depth
Resources
- Python sys.setrecursionlimit docs
- Python functools.lru_cache
- Tail Call Optimization in Python โ Stack Overflow
Comments