Recursion vs Iteration — Recursion Crashed JSON Parser
RecursionError at 1000 nesting levels crashed payment API.
20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Recursion calls itself to solve smaller subproblems — elegant but costs a stack frame per call
- Iteration uses loops — lower overhead, predictable memory usage
- Python's default recursion limit is 1000 frames — exceeding it raises RecursionError
- Every recursive solution has an iterative equivalent via explicit stack management
- Memoization (@lru_cache) transforms exponential recursive algorithms into linear time
- Iteration is typically 3-10x faster than recursion due to avoided function call overhead
Recursion and iteration are two fundamental approaches to repetition in code. Recursion solves a problem by having a function call itself with a smaller or simpler input, relying on the call stack to manage state. Iteration uses explicit loops (for, while) and mutable variables to repeat logic.
The core trade-off: recursion often yields cleaner, more declarative code for naturally recursive structures (trees, graphs, divide-and-conquer algorithms), but it consumes stack frames proportional to the depth of recursion. Iteration is memory-efficient and predictable, but can require manual stack management for complex traversals.
In production systems, recursion is the silent killer: a deeply nested JSON payload (e.g., 10,000 levels) will blow the call stack in Python, JavaScript, or Java, crashing your parser. Python explicitly lacks tail-call optimization (Guido van Rossum said it destroys stack traces), so even properly written tail-recursive functions will overflow.
The practical rule: use recursion for clarity when depth is bounded and small (e.g., balanced binary trees, depth < 1000); use iteration or explicit stacks for any input that could be adversarial or unbounded. Real-world examples: recursive DFS on a 50MB JSON file from an untrusted API will crash Node.js; iterative DFS with a manual stack handles it gracefully.
Memoization can salvage recursive Fibonacci from O(2^n) to O(n), but still risks stack overflow at n=1000 in Python. When recursion wins: tree traversals where depth is logarithmic (e.g., balanced BST), divide-and-conquer like mergesort, and algorithms where the recursive formulation is the clearest specification (e.g., quicksort, Tower of Hanoi).
But for any production service parsing user-supplied data, always default to iteration or an explicit stack — your server's uptime depends on it.
Imagine you need to find a book on a shelf. Recursion is like asking a friend, who asks another friend, who asks another friend — each person handles one book and passes the rest along. Eventually someone finds it, and the answer bubbles back up through the chain. Iteration is like checking each book yourself, one at a time, moving along the shelf. Both find the book. But recursion uses more phone calls (stack frames), and if the chain gets long enough, someone's phone dies — that's your stack overflow. Iteration uses more of your own time (loop cycles) but never runs out of phone battery.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Recursion and iteration both repeat logic, but they differ in how they manage state: recursion uses the call stack, iteration uses explicit loops. In production, the choice determines whether your JSON parser survives a 10,000-level nested payload or crashes with a stack overflow. Recursion is elegant for bounded-depth problems like balanced tree traversal; iteration is mandatory for any input that could be adversarial or unbounded. This article breaks down the mechanics, trade-offs, and conversion patterns so you can pick the right tool for your production code.
Why Recursion Crashed Your JSON Parser
Recursion and iteration are both mechanisms for repetition, but they differ fundamentally in how they manage state. Recursion relies on the call stack: each recursive call pushes a new frame, holding local variables and a return address. Iteration uses explicit loop constructs (for, while) and mutates a single set of variables in place. The core mechanic is that recursion expresses the problem in terms of smaller self-similar subproblems, while iteration sequences through a linear progression.
In practice, recursion buys you elegance at the cost of stack depth. Every call consumes a fixed amount of stack memory — typically 1–8 KB per frame in Java. A deeply nested JSON input (say, 10,000 levels) will blow the default stack size (~1 MB) and throw a StackOverflowError. Iteration, by contrast, uses heap-allocated data structures (e.g., an explicit stack or queue) that can grow to gigabytes before hitting memory limits. This makes iteration predictable and safe for unbounded input sizes.
Use recursion when the problem is naturally hierarchical (tree traversal, divide-and-conquer) and depth is bounded — e.g., balanced binary trees with log N depth. Use iteration when input depth is unbounded or when you need tight control over memory. In production parsers, the choice is not stylistic: it determines whether your service survives adversarial input or crashes silently under load.
Fibonacci — Recursion vs Iteration vs Memoization
Fibonacci is the canonical demonstration of recursion's cost because the pathology is so clear. The naive recursive version recomputes the same subproblems an exponential number of times — fib(5) calls fib(4) and fib(3), fib(4) calls fib(3) and fib(2), and fib(3) gets called again from fib(4)'s branch. The call tree doubles in size with each increment of n. By fib(40), you're making roughly a billion function calls to compute a number that a loop would find in 40 iterations.
Memoization fixes the exponential redundancy while keeping the recursive structure intact. @lru_cache stores the result of each unique (n,) argument tuple, so fib(3) is computed once and every subsequent call to fib(3) returns the cached result immediately. This collapses the O(2^n) call tree into a O(n) chain — you still recurse, but each unique subproblem executes exactly once.
Iteration beats both on space. The memoized version caches n results — O(n) space. The iterative version uses two variables that get updated in place — O(1) space. For computing fib(1_000_000), the iterative version uses negligible memory; the memoized recursive version would require thousands of cached entries and stack frames.
import sys import time from functools import lru_cache # --- Naive recursion: O(2^n) time, O(n) space --- # DO NOT use this in production for n > 35 def fib_recursive(n): if n <= 1: return n return fib_recursive(n - 1) + fib_recursive(n - 2) # --- Iterative: O(n) time, O(1) space --- # This is what production code should use 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 # --- Memoized recursion: O(n) time, O(n) space --- # Useful when you need recursive structure for clarity # and can afford the O(n) cache memory @lru_cache(maxsize=None) def fib_memo(n): if n <= 1: return n return fib_memo(n - 1) + fib_memo(n - 2) # Timing comparison — run on your machine to feel the difference def time_it(fn, n, label): start = time.perf_counter() result = fn(n) elapsed = time.perf_counter() - start print(f"{label:30s} fib({n}) = {result:>20} in {elapsed*1000:.3f}ms") time_it(fib_iterative, 50, "Iterative") time_it(fib_memo, 50, "Memoized recursive") # fib_recursive(50) would take several minutes — do not run it # Python's recursion limit — check yours print(f"\nRecursion limit: {sys.getrecursionlimit()} frames") # fib_memo(990) works; fib_memo(1001) raises RecursionError # because the memoized version still recurses to compute uncached values try: fib_memo(995) print("fib_memo(995): OK") except RecursionError: print("fib_memo(995): RecursionError — cache not warm") # Iterative has no such limit print(f"fib_iterative(10000): {fib_iterative(10000)} (truncated)"[:60] + '...')
- Each plate = one function call's local variables, arguments, and return address
- Base case = the point where you stop adding plates and start removing them from the top
- Stack overflow = the stack grew taller than the OS allows — typically 1-8MB per thread
- Memoization = writing the result on each plate before putting it down so you never have to redo that calculation
- Iteration = using a single plate that you erase and rewrite in place — constant stack height regardless of n
Converting Recursive DFS to Iterative
The pattern for converting any recursive algorithm to iterative is mechanical once you understand it: the call stack that the language manages implicitly becomes an explicit data structure you manage yourself. Where the recursive version calls itself with a smaller problem, the iterative version pushes that smaller problem onto a list or deque. Where the recursive version returns and picks up where it left off, the iterative version pops from the list and continues the loop.
DFS is the cleanest example because the conversion is direct. The recursive DFS works by calling itself for each unvisited neighbour — the call stack naturally implements LIFO ordering, visiting the deepest unvisited node first. The iterative version replaces the call stack with a Python list used as a stack (append for push, pop for pop), producing identical traversal order.
The one subtlety that trips people up: push order. The recursive version processes neighbours left-to-right because it calls itself for the first neighbour, recurses all the way down that branch, then comes back and calls itself for the second neighbour. To match this order iteratively, you push neighbours in reverse — the last neighbour goes on the stack first, so the first neighbour gets popped and processed first.
This matters in interview settings and in production when the traversal order is semantically meaningful — for example, when processing rules in priority order or when the graph represents a dependency chain where the order of resolution matters.
from collections import deque # --- Recursive DFS --- # Clean, readable, mirrors the algorithm definition # Dangerous on graphs with unknown depth or high node count def dfs_recursive(graph, node, visited=None): if visited is None: visited = set() # IMPORTANT: never use mutable default arg visited.add(node) print(node, end=' ') for neighbour in graph.get(node, []): if neighbour not in visited: dfs_recursive(graph, neighbour, visited) return visited # --- Iterative DFS --- # Same traversal order, no stack overflow risk # Uses heap memory (Python list) instead of the C call stack def dfs_iterative(graph, start): visited = set() stack = [start] # explicit stack — append is push, pop is pop while stack: node = stack.pop() if node in visited: continue visited.add(node) print(node, end=' ') # Push in reverse to match recursive left-to-right processing order for neighbour in reversed(graph.get(node, [])): if neighbour not in visited: stack.append(neighbour) return visited # For contrast: BFS uses a deque instead of a list def bfs_iterative(graph, start): visited = set([start]) queue = deque([start]) # FIFO — popleft instead of pop while queue: node = queue.popleft() print(node, end=' ') for neighbour in graph.get(node, []): if neighbour not in visited: visited.add(neighbour) queue.append(neighbour) return visited # Test graph — directed, acyclic graph = { 'A': ['B', 'C'], 'B': ['D', 'E'], 'C': ['F'], 'D': [], 'E': ['F'], 'F': [] } print('Recursive DFS: ', end=''); dfs_recursive(graph, 'A'); print() print('Iterative DFS: ', end=''); dfs_iterative(graph, 'A'); print() print('Iterative BFS: ', end=''); bfs_iterative(graph, 'A'); print() # Demonstrate the depth advantage # Simulate a chain graph 5000 nodes long — recursive would crash chain = {i: [i + 1] for i in range(5000)} chain[5000] = [] visited = dfs_iterative(chain, 0) print(f"\nIterative DFS visited {len(visited)} nodes in 5000-node chain without stack overflow")
Recursion vs Iteration
Stack safety, performance, and readability for parsing deeply nested JSON.
Recursive Parser
Elegant, but stack-bounded
Iterative Parser
Verbose, but overflow-safe
Tail Recursion — Why Python Refuses to Optimize It
Tail recursion is a specific structural property of a recursive function: the recursive call is the very last operation executed before the function returns, with no pending computation waiting for the result. In a tail-recursive function, the current stack frame is no longer needed once the recursive call is made — all the information needed for the rest of the computation has been passed as arguments to the next call.
Languages like Scheme, Haskell, and Scala exploit this property through tail call optimization (TCO): the compiler recognizes that the current frame is no longer needed and reuses it for the next call instead of pushing a new one. The recursive call is effectively compiled into a jump instruction rather than a function call, making tail-recursive code consume constant stack space regardless of recursion depth. Tail recursion in these languages is genuinely equivalent to a while loop at the hardware level.
Python deliberately does not implement TCO. Guido van Rossum's reasoning has been documented publicly: stack traces are the primary debugging tool for Python developers, and TCO would make stack traces useless — instead of seeing the full chain of recursive calls, you would see a single frame. He also argued that Python is not a functional language and that the idiomatic solution for iteration is a loop, not compiler magic. This decision has never been reversed.
Java is in a similar position, though for a different reason. The JVM specification does not require TCO, and the HotSpot JVM does not implement it for general recursive calls. There is ongoing work in Project Loom and through invokedynamic to support functional-style TCO in some contexts, but as of 2026 you cannot rely on TCO in standard Java code.
The practical implication is stark: in Python and Java, tail-recursive code is exactly as expensive as non-tail-recursive code. Writing a function in tail-recursive style in Python provides zero performance benefit and no stack depth advantage. If you want iteration performance, write a loop.
import sys # --- Tail-recursive factorial --- # The recursive call is the LAST operation — nothing pending after it # accumulator carries the result forward so no computation needed after return def factorial_tail(n, accumulator=1): if n <= 1: return accumulator return factorial_tail(n - 1, n * accumulator) # tail position — accumulator updated # --- Non-tail-recursive factorial --- # Multiplication happens AFTER the recursive call returns # The stack frame must be retained to perform n * (result of recursive call) def factorial_non_tail(n): if n <= 1: return 1 return n * factorial_non_tail(n - 1) # NOT tail position — n must be remembered # --- Iterative factorial — what you should actually write --- def factorial_iterative(n): result = 1 for i in range(2, n + 1): result *= i return result # Demonstrate that Python treats both recursive versions identically # Both will fail at the same recursion depth for fn_name, fn in [("tail", factorial_tail), ("non_tail", factorial_non_tail)]: sys.setrecursionlimit(200) # low limit to make failure obvious try: result = fn(150) print(f"factorial_{fn_name}(150) = {str(result)[:20]}...") except RecursionError: print(f"factorial_{fn_name}(150) -> RecursionError at depth 150") # Iterative has no such limit sys.setrecursionlimit(1000) # restore print(f"factorial_iterative(150) = {str(factorial_iterative(150))[:20]}...") print(f"factorial_iterative(10000): computed successfully, len={len(str(factorial_iterative(10000)))} digits") # Show the tail-recursive -> iterative mechanical conversion # Replace: return factorial_tail(n-1, n*acc) with loop variable reassignment def factorial_tail_to_iterative(n): accumulator = 1 while n > 1: # was: if n <= 1: return accumulator accumulator = n * accumulator # was: argument to recursive call n = n - 1 # was: first argument to recursive call return accumulator print(f"\nTail-to-iterative conversion: factorial(20) = {factorial_tail_to_iterative(20)}")
When Recursion Wins — Tree Traversal and Divide and Conquer
Despite its costs, recursion is the right choice for problems whose structure is inherently recursive. Tree traversal is the clearest example: a tree is defined recursively (a node with left and right subtrees that are themselves trees), so a recursive traversal directly mirrors the definition. You can read the three-line recursive inorder traversal and immediately verify it is correct because the code and the definition are the same thing. The iterative equivalent requires managing an explicit stack, tracking multiple state variables, and reasoning about edge cases that simply do not exist in the recursive version.
Divide and conquer algorithms (merge sort, quicksort, binary search) have the same property. The recursive version of merge sort — split the array in half, sort each half, merge — is directly expressible in code. The iterative version requires managing multiple pointers and merge widths through nested loops, and is substantially harder to understand and verify.
The important constraint that makes recursion safe for these problems: the recursion depth is bounded by the structure of the data, not by user-controlled input. A balanced binary tree with one billion nodes has a depth of approximately 30. Even a perfectly degenerate tree (a linked list) has a depth equal to its node count — but if you know the tree is balanced (because you built it that way), depth 30 is well within any platform's stack limit.
The practical rule: use recursion when the problem structure is recursive AND you can bound the depth at design time. The moment depth is controlled by external input — user-uploaded files, API responses, user-constructed data structures — you must switch to iteration.
from __future__ import annotations from dataclasses import dataclass from typing import Optional @dataclass class TreeNode: val: int left: Optional['TreeNode'] = None right: Optional['TreeNode'] = None # --- Recursive inorder: left -> root -> right --- # 3 lines, directly mirrors the definition # Depth = tree height — O(log n) for balanced, O(n) for degenerate def inorder_recursive(node: Optional[TreeNode]) -> list[int]: if node is None: return [] return inorder_recursive(node.left) + [node.val] + inorder_recursive(node.right) # --- Iterative inorder: same result, harder to read --- # Required when tree height is unbounded or user-controlled def inorder_iterative(root: Optional[TreeNode]) -> list[int]: result, stack, current = [], [], root while current or stack: # Go as far left as possible while current: stack.append(current) current = current.left # Process the leftmost unprocessed node current = stack.pop() result.append(current.val) # Move to right subtree current = current.right return result # --- Recursive merge sort: clean divide-and-conquer --- def merge_sort(arr: list[int]) -> list[int]: if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) return merge(left, right) def merge(left: list[int], right: list[int]) -> list[int]: result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]); i += 1 else: result.append(right[j]); j += 1 return result + left[i:] + right[j:] # Build a balanced BST for testing # 4 # / \ # 2 6 # / \ / \ # 1 3 5 7 root = TreeNode(4, TreeNode(2, TreeNode(1), TreeNode(3)), TreeNode(6, TreeNode(5), TreeNode(7)) ) recursive_result = inorder_recursive(root) iterative_result = inorder_iterative(root) print(f"Recursive inorder: {recursive_result}") print(f"Iterative inorder: {iterative_result}") print(f"Results match: {recursive_result == iterative_result}") # Merge sort demo unsorted = [38, 27, 43, 3, 9, 82, 10] print(f"\nMerge sort: {unsorted} -> {merge_sort(unsorted)}") # Depth analysis: balanced tree with 1M nodes import math nodes = 1_000_000 balanced_depth = math.ceil(math.log2(nodes + 1)) print(f"\nBalanced tree with {nodes:,} nodes: max depth = {balanced_depth}") print(f"Python's recursion limit: 1000 frames") print(f"Safe for recursion? {'Yes' if balanced_depth < 900 else 'No'}")
The Stack Blow-Up — Why Recursion Kills Your Production Server
Every recursive call consumes a stack frame. Each frame stores local variables, return addresses, and saved registers. Call a function 10,000 times recursively and you've allocated 10,000 frames. Iteration? One frame, one loop counter.
This isn't academic. Your JSON parser that recursively descends into nested objects? A deeply nested payload from a third-party API hits depth 1,001, and your JVM throws a StackOverflowError. No graceful degradation. No partial results. Just a 500 and a pager at 2 AM.
The standard JVM stack size is 1024 KB. Each Java frame costs about 40-80 bytes. Do the math: 1024 * 1024 / 60 ≈ 17,000 frames. That's your hard limit before the server eats itself. Iteration doesn't have a hard limit—only heap pressure from data structures.
So when your PM asks "why did the JSON parser crash?" the answer isn't "we need more memory." The answer is "our stack-based approach doesn't scale to production data."
// io.thecodeforge — dsa tutorial public class StackBlowOut { // Recursive: crashes at around depth 17,000 private static void recursiveCount(int depth) { if (depth == 0) return; recursiveCount(depth - 1); } // Iterative: never crashes, uses constant stack private static void iterativeCount(int limit) { int runner = 0; while (runner < limit) { // process... runner++; } } public static void main(String[] args) { try { recursiveCount(200000); } catch (StackOverflowError e) { System.out.println("Recursion died at 200k depth"); } iterativeCount(200000); System.out.println("Iteration survived 200k iterations"); } }
Recursive JSON Parser Crashed Production API
parse_nested(). The errors were non-deterministic — some requests from the same payment provider succeeded and some failed — which initially made the team suspect a race condition rather than a depth issue. The pattern became clear only after correlating failed requests against payload size.- Never use recursion on untrusted input — you cannot control the depth, and the depth is often controlled by someone with different incentives than you
- sys.setrecursionlimit() only adjusts Python's internal frame counter — it does not increase the actual C stack size allocated by the OS; raising it beyond a few thousand reliably causes segfaults
- Always validate and reject input that exceeds your recursion budget before beginning recursive processing — fail fast with a 400, not slow with a 500
- Iterative parsing with an explicit stack is mandatory for production systems processing external or user-controlled data; the performance difference is negligible and the safety difference is absolute
sys.setrecursionlimit() as the first fix — this delays the segfault rather than preventing it. Add a depth guard at the entry point: if depth > MAX_DEPTH: raise ValueError(f'Input exceeds maximum depth {MAX_DEPTH}'). Then convert the recursive path to iterative using an explicit stack.set() at the top. Also check for accidental global state modification inside the recursive function.python -c "import sys; print(sys.getrecursionlimit())"python -c "import sys; print(sys.getrecursionlimit()); sys.setrecursionlimit(2000)"jcmd <pid> VM.flags | grep StackSizejcmd <pid> Thread.print | grep -B5 -A30 'StackOverflowError'jmap -dump:live,format=b,file=heap.hprof <pid>python -c "import tracemalloc; tracemalloc.start()" followed by snapshot comparison between processing start and peak| Aspect | Recursion | Iteration |
|---|---|---|
| Code clarity | Mirrors recursive problem structure (trees, divide-and-conquer) — algorithm and code are the same shape | Clear for sequential processing (lists, ranges, streams) — explicit state is visible at every step |
| Memory usage | O(depth) stack frames — each frame holds local variables, arguments, and return address | O(1) loop variables or O(n) explicit data structures on the heap — you control what is retained |
| Performance | Function call overhead per step: ~100-200ns in CPython, ~10-30ns in JVM depending on JIT warmup | Loop overhead only: ~5-15ns per iteration in CPython, sub-nanosecond in JVM after JIT |
| Stack depth limit | Python: 1000 frames default (sys.getrecursionlimit()). Java: 512KB-1MB C stack per thread (~5000-20000 frames depending on frame size) | No stack depth limit — uses heap memory which is bounded only by available RAM |
| Debugging experience | Full call chain visible in stack traces — each frame shows the argument values at that depth | Single stack frame — must instrument loop variables explicitly to trace state through iterations |
| Tail call optimization | Supported in Scheme, Haskell, Scala. Explicitly NOT in Python or standard Java — both treat tail calls identically to regular calls | N/A — loops are inherently iterative and require no compiler transformation |
| Untrusted or external input | Dangerous — depth is controlled by input, stack overflow is guaranteed if input is malformed or adversarial | Safe — depth has no bearing on stack consumption; all depth state lives in heap-allocated data structures |
| Memoization | Natural fit — @lru_cache decorates the recursive function directly; cache lookup and miss both follow the same code path | Manual — must implement explicit caching layer; bottom-up DP is the idiomatic iterative alternative |
| Deletion and backtracking | Natural via call stack unwinding — return from recursive call naturally restores previous state | Must manually save and restore state — requires explicit undo operations or snapshot/restore pattern |
| Best fit | Tree traversal, graph DFS on bounded-depth structures, divide-and-conquer, backtracking with bounded search depth | Linear processing, batch operations, parsing external data, any algorithm where input depth is externally controlled |
Key takeaways
sys.setrecursionlimit() moves the crash threshold but causes a segfault when the C stack is exhausted.Common mistakes to avoid
5 patternsUsing recursion on untrusted or externally controlled input
Raising sys.setrecursionlimit() as the fix for RecursionError
Not memoizing recursive functions with overlapping subproblems
Writing tail-recursive code expecting Python or Java to optimize it
Using a mutable object as a default argument in a recursive function
set() as the first line of the function body. This creates a fresh set for each top-level call.Practice These on LeetCode
Interview Questions on This Topic
What is Python's default recursion limit, how do you change it, and what are the risks of changing it?
sys.getrecursionlimit(). You can change it with sys.setrecursionlimit(n). The risk: this limit only adjusts Python's internal frame counter — it does not increase the actual C stack size that the OS allocates for the thread. The C stack is typically 1-8MB depending on the OS and how the thread was created. Setting the limit to 100000 and recursing that deep will cause a segfault (unhandled C stack overflow) rather than a catchable RecursionError. The process exits without a useful stack trace. The correct fix for deep recursion is always to convert to iterative — not to raise the limit.How do you convert a recursive DFS to an iterative one? What are the gotchas?
What is the time and space complexity of naive recursive Fibonacci, and how does memoization change it?
Why doesn't Python optimize tail recursion, and what should you do instead?
When would you choose recursion over iteration in a production system? What constraints make the choice safe?
Frequently Asked Questions
Tail recursion is a structural property of a recursive function: the recursive call is the very last operation, with no pending computation after it returns. In a tail-recursive function, the current stack frame is theoretically unnecessary once the recursive call is made — all needed state has been passed as arguments.
Some languages (Scheme, Haskell, Scala) exploit this by reusing the current stack frame for the recursive call instead of pushing a new one, effectively compiling the recursion into a loop at the machine code level.
Python explicitly does not implement this optimization. Guido van Rossum's stated reason: stack traces would become useless if frames were reused, and explicit loops are the idiomatic Python approach to iteration. Java is in the same position — the JVM specification does not require TCO and HotSpot does not implement it for general recursive calls. If you need iteration in Python or Java, write a loop.
Recursion is the better choice when the problem is structurally recursive — meaning the solution naturally decomposes into the same problem on smaller inputs — AND when the maximum depth is bounded by your own design invariants rather than external input.
The strongest cases for recursion: binary tree traversal (depth is O(log n) for balanced trees), divide-and-conquer algorithms like merge sort and quicksort (depth is O(log n)), backtracking algorithms like constraint solving and maze generation (depth is bounded by the search space).
In all these cases, the recursive code is shorter, more readable, and directly verifiable against the algorithm definition. The iterative equivalent typically requires managing explicit stack state that is harder to reason about and more likely to contain edge-case bugs.
Estimate the maximum recursion depth for your specific input domain, not for synthetic test data. For balanced binary trees: depth ≈ log2(n) — a billion-node balanced tree has depth ~30, well within Python's 1000-frame limit. For linear recursion processing a list of length n: depth = n — a 10000-element list will hit Python's limit. For divide-and-conquer: depth ≈ log2(n) — safe. For parsing nested structures: depth = nesting level — depends entirely on the data source.
The key question: is the depth bounded by your code's invariants or by external input? If a user, API, or file controls the nesting level, assume the worst-case depth is unbounded and use iteration.
No — and this is one of the more subtle bugs that memoization can introduce. @lru_cache caches return values keyed by input arguments. When a cached result is returned, the function body does not execute — which means any side effects (print statements, file writes, database inserts, counter increments, global state modifications) are also skipped.
A function memoized with side effects will execute those side effects exactly once per unique argument combination — the first time each unique input is seen. Every subsequent call with the same arguments returns the cached value and skips the side effects entirely. This is usually not what you want and produces confusing behaviour that is hard to debug because the function appears to return the correct value while silently skipping operations.
Memoize only pure functions: functions where the return value is determined entirely by the arguments, with no observable side effects and no dependence on external state.
20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.
That's Recursion. Mark it forged?
5 min read · try the examples if you haven't