Functional Programming
Python is a multi-paradigm language that borrowed the functional tools without the functional semantics. It has first-class functions, closures, lambda, map, filter, functools, itertools — and, at the same time, mutable objects, side effects everywhere, eager evaluation by default, and no tail-call optimization at all. The correct interview phrasing is exactly that — "Python supports the functional style partially" — not "Python is a functional language" and not "Python has no functional programming".
Every trap in this topic grows out of that half-support, and almost all of them are Python 3 specific. map and filter stopped returning lists and hand back a lazy one-shot iterator that is empty after the first pass. reduce was deliberately moved out of the builtins into functools, because a fold almost always reads worse than an explicit loop. functools.lru_cache requires hashable arguments and grows without bound when maxsize is None. And tail recursion buys you nothing in CPython — the stack grows just the same, and around a thousand frames deep you get RecursionError. Each mechanism is worked through below.
Topic map
- The functional style in Python — purity, immutability, and where Python deliberately refused the functional model.
- Higher-order functions — functions as first-class objects, taking and returning a function,
key=insorted. - map, filter and reduce — lazy iterators instead of lists,
reduceinfunctools, and why a comprehension is usually better. - The operator module — named operator functions,
itemgetter,attrgetter,methodcallerinstead of tiny lambdas. - Currying and partial — a chain of one-argument functions versus the partial application Python actually uses.
- itertools building blocks — lazy and infinite iterators,
chain,islice,groupbyand combinatorics. - Memoization — lru_cache — caching by arguments, the hashability requirement, eviction and memory leaks.
- Recursion — the base and recursive cases, stack frames,
RecursionErrorand the exponential naive Fibonacci. - Tail recursion and the absence of TCO — the accumulator, why CPython never collapses frames, and what to do instead.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
| Calling Python a purely functional language | The answer is wrong on its face — Python has mutable state, eager evaluation and no purity guarantees |
Expecting a list back from map/filter | In Python 3 these are lazy one-shot iterators — a second list() returns empty, and len() does not work at all |
Calling reduce without importing it | NameError — reduce was removed from the builtins on purpose and lives in functools |
Reducing the functional style to lambda | Purity, immutability and comprehensions get lost — and those are what actually make the code functional |
Caching a function that takes a list or a dict with lru_cache | TypeError — unhashable type on the very first call, because the cache key is the tuple of arguments |
Putting lru_cache on a method or on an impure function | The cache pins self and leaks, while an impure function starts serving stale data |
Calling list() on count(), or expecting groupby to group globally | The infinite iterator eats all memory, and groups break at every key change — the input must be sorted |
| Believing tail recursion protects you from overflow | CPython performs no TCO — frames pile up and RecursionError fires around depth 1000 |
What interviews check
The topic is almost never asked as theory for its own sake — it is a fast test of whether you have read anything about Python 3 beyond a tutorial. The classic opener is "what does map return": the right answer is "a map object, a lazy iterator", followed by "so you can only walk it once". Then you are asked to rewrite the example as a comprehension, and the interviewer listens for you to say out loud that in Python a comprehension is usually preferable to a map+filter pair with lambdas. Knowing that reduce was moved out of the builtins deliberately, not by oversight, is a separate seniority marker.
The second half of the conversation goes to caching and recursion. On lru_cache you get three questions — what serves as the key, why the arguments must be hashable, and what maxsize does; answering "it caches by call order" fails the question instantly. On recursion you are asked to name the two cases and — at senior grade — whether CPython optimizes tail calls. The correct answer is "no, and it is a deliberate decision made to keep full tracebacks"; the typical mistake is saying "yes" or deciding that sys.setrecursionlimit turns the optimization on. The practical follow-up is always the same — rewrite the recursion as a loop.