8. Trees & Recursion Mechanics

How deep you can recurse before each runtime crashes, and the null-check, default-argument, and multi-return idioms tree recursion depends on.

You already know DFS, BFS, and tree DP — this section is about the mechanical details that trip people up under interview pressure: how each runtime represents a node, how deep you can actually recurse before it blows up, and what your escape hatch looks like when it does.

See roadmap: Trees

Language Verdict: Pros, Cons & Recommendation

Python

3/5
  • Native tuple returns (return height, diameter) make multi-value recursive helpers zero-ceremony, no wrapper type needed
  • is None gives an explicit, unambiguous base-case check with no truthiness ambiguity for node references
  • sys.getrecursionlimit()/setrecursionlimit() at least make the ceiling introspectable and adjustable, even if imperfect
  • Shallowest default recursion ceiling of the four (~1000 frames) — a skewed/degenerate tree can raise RecursionError well before Java, Go, or JS would fail
  • Raising the limit doesn't grow the real C stack, so setting it too high risks a hard interpreter segfault instead of a catchable error
  • Mutable default arguments (def helper(node, path=[])) are evaluated once at def-time and silently shared across calls — a classic trap in recursive accumulator helpers

Java

5/5
  • No implicit truthiness for references — node == null is the only option, making base cases impossible to get subtly wrong
  • Deep, memory-bounded recursion headroom, and tunable further via -Xss if you control the launch
  • No default parameter values at all removes the entire class of Python-style shared-mutable-default bugs
  • Modern record types give clean, compiler-checked multi-value returns (record Result(int height, int diameter) {})
  • No native tuple/multi-return — every multi-value helper needs an explicit record or wrapper class, even for a throwaway pair of ints
  • No default parameters means overloaded constructors or a public/private helper split just to give callers a convenient entry point

Go

4/5
  • A three-field struct is the whole node type — zero values make Left/Right nil without constructors
  • Native multiple return (return height, diameter) makes tree-DP helpers as clean as Python tuples
  • No default parameters, so the Python mutable-default-list trap cannot happen
  • Goroutine stacks grow — recursion on a typical interview tree is fine, unlike Python's ~1000-frame ceiling
  • No tuple unpack of a struct — multiple return covers two ints, but a growing result set means extra named return values or a small struct
  • if node does not compile; every base case is if node == nil
  • No TCO; a degenerate million-node tree can still panic, so know the explicit-stack rewrite

JavaScript

4/5
  • Array/object destructuring (const [h, d] = helper(...)) gives Python-tuple-like multi-return ergonomics for free
  • Default parameters are evaluated fresh per call, so there's no Python-style shared-mutable-default trap in recursive accumulators
  • Recursion headroom comparable to Java's — deep enough that call-stack limits rarely bite on realistic interview-sized trees
  • !node truthy shortcuts are fine for node references but a real footgun if reused on a value field (if (!node.val) wrongly skips a legitimate 0)
  • No compile-time null safety — === null/=== undefined correctness is entirely on the programmer, unlike Java's enforced == null
Recommendation: Python remains the default DSA language, but its ~1000-frame limit and mutable-default trap are the real risks on trees. Java's strict == null and Go's == nil plus multiple return make both comfortable for recursive tree DP; Go has a real edge on returning (height, diameter) without a record. JavaScript is close behind. Convert to an explicit stack if the tree can be a linked-list-shaped degenerate case.

Coding Mechanics, Side by Side

Tree node class idiom

Must-know

Mechanically near-identical across all four, but the exact constructor/default-argument/struct syntax differs enough that it's worth locking down before you're under interview pressure.

class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right # or, more terse: from dataclasses import dataclass from typing import Optional @dataclass class TreeNode: val: int = 0 left: Optional["TreeNode"] = None right: Optional["TreeNode"] = None

Plain class with default arguments is the standard LeetCode-style definition and what most interviewers expect verbatim. @dataclass auto-generates __init__, __repr__, and __eq__, which is convenient for debugging (printing a node shows its values) but rarely used in interview code since it adds an import and a forward-reference string type hint for the self-referential left/right fields.

Null / None / undefined base-case checks

Must-know
def dfs(node): if node is None: return 0 return 1 + dfs(node.left) + dfs(node.right)

Always use is None, never == None or a bare truthiness check on a node. is None is an identity check (fast, unambiguous) and is the idiomatic Python style enforced by linters (PEP 8). A bare if not node: also works for a well-formed tree node (there's no falsy TreeNode), but writing is None signals explicit intent and avoids surprises if the node class ever defines __bool__ or __len__.

Recursion depth limits per runtime

Must-know

The single most important mechanical fact in this section: all four languages recurse on a bounded call stack, but the bound and the failure mode are very different — and this is a legitimate interview probe ("would your recursive solution work on a very unbalanced tree with 10,000+ nodes?").

import sys print(sys.getrecursionlimit()) # 1000 by default sys.setrecursionlimit(10_000) # raise the *frame count* limit only def dfs(node): if node is None: return 0 return 1 + dfs(node.left) + dfs(node.right) # on a 10,000-node degenerate (linked-list-shaped) tree: # RecursionError: maximum recursion depth exceeded

CPython's default limit is ~1000 stack frames, tracked purely as a frame counter, not tied to actual memory. Exceeding it raises a catchable RecursionError. You can raise the limit with sys.setrecursionlimit(), but this doesn't grow the real OS/C thread stack — set it too high and instead of a clean RecursionError you can get a genuine C-stack overflow that segfaults the interpreter with no traceback at all. Of the three languages, Python is by far the most likely to fail first on a skewed/degenerate binary tree (effectively a linked list) with thousands of nodes.

No tail-call optimization, in any of the four

Must-know

It's tempting to think you can dodge recursion-depth limits by writing your recursive tree function in "tail-call" form (the recursive call is the very last thing the function does). Don't rely on this in any of these four languages:

  • CPython deliberately does not implement tail-call elimination. This isn't an oversight — Guido van Rossum has explicitly written that TCO would make stack traces harder to read and conflicts with Python's debugging philosophy, and there are no plans to add it.
  • The JVM does not perform TCO either. This is a long-standing, well-known limitation of the platform; the bytecode and stack-frame model don't support it, and javac/HotSpot never rewrite tail calls into loops. Workarounds like manual trampolining exist but are rarely worth the complexity in interview code.
  • Go does not perform TCO. A tail-recursive function still consumes a stack frame per call; goroutine stacks grow, but they do not turn recursion into a loop.
  • JavaScript is the one case where TCO was actually specified — ES2015 defined "proper tail calls" in the language spec. But in practice only Safari/JavaScriptCore ever shipped it. V8 (which powers Chrome and Node.js) never implemented it and there's no indication it will, so treat JS tail calls as unoptimized too.

Bottom line: if your recursive solution needs to handle 10,000+ stack frames, do not "fix" it by rewriting it in tail-recursive style and hoping the runtime collapses it into a loop — in all four languages it will still consume one real stack frame per call. The actual fix is converting to an explicit-stack iterative version (next block).

Converting recursion to explicit-stack iteration (DFS)

Must-know

This is the direct, practical escape hatch when recursion depth is a real concern — you trade the call stack for a heap-allocated stack, which is bounded only by available memory, not by a frame-count or a non-growing OS stack.

def dfs_iterative(root): if root is None: return [] stack = [root] order = [] while stack: node = stack.pop() order.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return order

A Python list used as a stack via append/pop (from the end) is O(1) amortized for both operations, so it's the natural, idiomatic choice — no need for collections.deque here since you only ever push/pop one end. Pushing right before left ensures left is processed first (LIFO), matching typical pre-order DFS.

Iterative in-order traversal via explicit stack

Recommended

The classic "push the left spine, pop, visit, move right" pattern. The algorithm is identical across languages; only the loop-condition syntax for "stack not empty OR current node not null" differs.

def inorder(root): stack = [] node = root order = [] while stack or node: while node: stack.append(node) node = node.left node = stack.pop() order.append(node.val) node = node.right return order

while stack or node: relies on Python's truthiness: a non-empty list and a non-None node object are both truthy. This reads cleanly but is exactly the kind of implicit-bool-conversion idiom that's fine here (a list and an object reference are never ambiguous) yet worth being deliberate about in general, per the null-check block above.

Default parameter mutable-state trap

Must-know

A concrete, language-specific footgun worth memorizing: it bites almost exclusively in Python, and it bites specifically in recursive tree helpers that accumulate a path/result list. Java and Go have no default parameters; JS evaluates defaults fresh per call.

# BUGGY: default list is created ONCE at function-definition time # and shared across every call that doesn't pass its own path def helper(node, path=[]): if node is None: return path.append(node.val) helper(node.left, path) helper(node.right, path) # FIXED: use None as the sentinel, create a fresh list per call def helper(node, path=None): if path is None: path = [] if node is None: return path.append(node.val) helper(node.left, path) helper(node.right, path)

Python evaluates default argument values exactly once, at def time — not on every call. A mutable default like [] or {} is therefore the same object reused across every call site that relies on the default, silently accumulating state across unrelated top-level invocations (e.g. across repeated test cases in the same process). The standard fix is the None-sentinel pattern shown above. This is one of Python's most infamous gotchas and is explicitly called out in Fluent Python and the official FAQ.

Returning multiple values from a recursive helper

Recommended

Common in tree DP where one pass needs to report two things at once (e.g. subtree height and running diameter). Python and Go have native multi-return; Java needs a record; JS uses array/object destructuring.

def helper(node): if node is None: return 0, 0 # (height, diameter) lh, ld = helper(node.left) rh, rd = helper(node.right) height = 1 + max(lh, rh) diameter = max(ld, rd, lh + rh) return height, diameter height, diameter = helper(root)

Tuples make multi-return a first-class, zero-ceremony feature — return a, b and unpacking with a, b = f(x) is idiomatic and cheap (small tuples are lightweight). No extra class needed.

Further Reading

  • The authoritative description of CPython's recursion limit and why raising it can still crash the interpreter.

  • Tail Recursion EliminationGuido van Rossum (neopythonic blog)

    Python's creator explaining, in his own words, why CPython deliberately never implements tail-call optimization.

  • StackOverflowErrorOracle Java Docs

    Official JVM documentation of the error thrown when a thread's stack is exhausted, e.g. via deep recursion.

  • Reference for the -Xss flag that controls per-thread stack size, which directly bounds Java recursion depth.

  • MDN's explanation of recursion in JS, including the call-stack limits and RangeError behavior in engines like V8.

  • The actual spec language for 'proper tail calls' that V8 never implemented — useful to see what was standardized versus shipped.

  • A Tour of Go — Structs and pointersGo official docs (go.dev/tour)

    Structs, pointers, and nil — the mechanics behind `type TreeNode struct { Val int; Left, Right *TreeNode }` and nil base cases.