This article contains 20 Python Lambda & Functional Programming coding practice questions including map(), filter(), reduce(), functools from beginner to advance level.
Each coding challenge includes a Practice Problem, Hint, Solution code, and detailed Explanation, ensuring you don’t just copy code, but genuinely practice and understand how and why it works.
- All solutions have been fully tested on Python 3.
- Use our Online Code Editor to solve these exercises in real time.
+ Table of Contents (20 Exercises)
Table of contents
- Exercise 1: Basic Lambda
- Exercise 2: Lambda with Sorting
- Exercise 3: Map Basics
- Exercise 4: Filter Basics
- Exercise 5: Reduce Basics
- Exercise 6: Conditional Lambda
- Exercise 7: Lambda in a Dictionary
- Exercise 8: Chaining Map & Filter
- Exercise 9: Sorted with Multiple Keys
- Exercise 10: Partial Functions
- Exercise 11: Function Composition
- Exercise 12: Flatten with Map & Reduce
- Exercise 13: Custom Key with sorted()
- Exercise 14: Memoization with lru_cache
- Exercise 15: Currying
- Exercise 16: Pipeline Function
- Exercise 17: Transducer Pattern
- Exercise 18: Lazy Evaluation with Generators
- Exercise 19: Higher-Order Function Library
- Exercise 20: Monadic Chaining
Exercise 1: Basic Lambda
Problem Statement: Write a lambda function that takes a single number and returns its square. Assign the lambda to a variable named square and call it with a few different values to verify it works.
Purpose: This exercise introduces the lambda keyword, Python’s syntax for creating small anonymous functions inline. Understanding lambdas is the foundation for using map(), filter(), sorted(), and other higher-order functions effectively, since all of them accept a callable as an argument.
Given Input: Call square(4) and square(9).
Expected Output: 16 and 81
▼ Hint
The syntax is lambda parameter: expression. The expression is evaluated and returned automatically – no return keyword is needed or allowed.
▼ Solution & Explanation
Explanation:
lambda x: x ** 2: Creates an anonymous function that takes one parameterxand returnsx ** 2. The entire expression to the right of the colon is the implicit return value.- Assigning to a variable: Storing a lambda in a named variable like
squaremakes it callable exactly like a regulardeffunction. The result is functionally identical todef square(x): return x ** 2. - When to use lambda vs. def: The Python style guide (PEP 8) recommends against assigning lambdas to variables at module level – use
deffor named functions. Lambdas shine as inline, one-off callables passed directly to another function, as the following exercises demonstrate. - Limitations: A lambda body must be a single expression. It cannot contain statements, assignments, loops, or multiple lines. For anything more complex, a regular
deffunction is the right tool.
Exercise 2: Lambda with Sorting
Problem Statement: Given a list of (name, age) tuples, sort them in ascending order by age using sorted() with a lambda as the key argument. Then sort the same list in descending order by age.
Purpose: Sorting by a custom criterion is one of the most common everyday tasks in Python. This exercise shows how a lambda eliminates the need to define a separate helper function just to extract a sort key, making the intent clear and the code concise.
Given Input: people = [("Alice", 30), ("Bob", 25), ("Charlie", 35), ("Diana", 28)]
Expected Output:
Ascending: [('Bob', 25), ('Diana', 28), ('Alice', 30), ('Charlie', 35)]
Descending: [('Charlie', 35), ('Alice', 30), ('Diana', 28), ('Bob', 25)]
▼ Hint
- Pass
key=lambda person: person[1]tosorted(). Thekeyfunction receives each element and returns the value Python should compare for ordering. - Add
reverse=Trueto the samesorted()call for descending order.
▼ Solution & Explanation
Explanation:
key=lambda person: person[1]: Thekeyparameter accepts any callable. Python calls it once per element and uses the returned value for comparison. Here,person[1]extracts the age from each tuple, so the list is sorted by age rather than by the default tuple comparison (which would sort by name first).sorted()vs..sort():sorted()returns a new list and leaves the original unchanged.list.sort()sorts in place and returnsNone. Both accept the samekeyandreverseparameters.reverse=True: Reverses the sort direction without requiring a separate lambda. It is more efficient than sorting ascending and then reversing the list afterwards.- Alternative –
operator.itemgetter:from operator import itemgetterfollowed bykey=itemgetter(1)is slightly faster than a lambda for simple index extraction because it avoids Python function call overhead. For most use cases the difference is negligible, and the lambda is more readable.
Exercise 3: Map Basics
Problem Statement: Use map() with a lambda to convert a list of temperatures from Celsius to Fahrenheit. The conversion formula is F = (C × 9/5) + 32. Collect the results into a new list and print it.
Purpose: map() applies a transformation to every element of an iterable without writing an explicit loop. This is the core idea behind functional programming: describing what to do to data rather than how to iterate over it. The pattern scales cleanly from lists to generators, files, and database result sets.
Given Input: celsius = [0, 20, 37, 100]
Expected Output: [32.0, 68.0, 98.6, 212.0]
▼ Hint
- Call
map(lambda c: (c * 9/5) + 32, celsius).map()returns a lazy iterator, so wrap it inlist()to get a concrete list you can print. - The lambda takes a single Celsius value
cand applies the formula directly in the expression.
▼ Solution & Explanation
Explanation:
map(function, iterable): Appliesfunctionto each element ofiterablelazily. The function is not called until the iterator is consumed – either bylist(), aforloop, or another consuming operation.list(map(...)): Forces evaluation of the lazy iterator and collects all results into a list. Without wrapping inlist(), printing the result would show something like<map object at 0x...>rather than the values.- Lambda as the transform: The lambda
lambda c: (c * 9 / 5) + 32encodes the conversion formula in a single expression. Because it has no name and is used in exactly one place, a lambda is more appropriate here than a separatedef. - List comprehension equivalent:
[(c * 9 / 5) + 32 for c in celsius]produces exactly the same result. Many Python developers prefer list comprehensions for their readability, butmap()is more composable when chaining multiple transformations or working with very large datasets where lazy evaluation matters.
Exercise 4: Filter Basics
Problem Statement: Use filter() with a lambda to extract only the even numbers from a list of integers. Collect the results into a new list and print it.
Purpose: filter() is the functional equivalent of a loop with an if condition inside. It keeps only the elements for which a predicate function returns True. Separating the filtering logic from the iteration makes code easier to test, reuse, and reason about.
Given Input: numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Expected Output: [2, 4, 6, 8, 10]
▼ Hint
- Call
filter(lambda n: n % 2 == 0, numbers). Likemap(),filter()returns a lazy iterator, so wrap it inlist(). - The lambda is a predicate: it returns
Truefor even numbers andFalsefor odd ones. Only elements where the lambda returns a truthy value are kept.
▼ Solution & Explanation
Explanation:
filter(function, iterable): Callsfunctionon each element and yields only those for which it returns a truthy value. PassingNoneas the function is also valid – it then keeps all elements that are truthy themselves.n % 2 == 0: The modulo operator%returns the remainder after division. A remainder of0when dividing by2means the number is even. The expression evaluates directly toTrueorFalse, which is exactly what a predicate needs to return.- Lazy evaluation: Like
map(),filter()does not process any elements until iterated. This makes it memory-efficient for large datasets – wrapping inlist()forces full evaluation only when you actually need all results at once. - List comprehension equivalent:
[n for n in numbers if n % 2 == 0]is the idiomatic Python alternative. As withmap(), preferfilter()when composing multiple functional operations in a pipeline, and list comprehensions for simple one-off filtering where readability is the priority.
Exercise 5: Reduce Basics
Problem Statement: Use functools.reduce() with a lambda to find the product of all numbers in a list. The function should multiply each element cumulatively from left to right until a single value remains.
Purpose: reduce() is the functional tool for collapsing an iterable into a single accumulated value. While Python offers sum() and max() as built-ins for common reductions, reduce() handles any binary operation including product, string concatenation, and custom aggregation logic.
Given Input: numbers = [1, 2, 3, 4, 5]
Expected Output: 120
▼ Hint
- Import
reducefromfunctoolsfirst:from functools import reduce. - Call
reduce(lambda acc, n: acc * n, numbers). The lambda takes two arguments: the accumulated value so far (acc) and the current element (n). It returns the new accumulated value, which is fed back in asaccon the next call.
▼ Solution & Explanation
Explanation:
reduce(function, iterable): Appliesfunctioncumulatively. It starts by callingfunction(iterable[0], iterable[1]), then callsfunction(result, iterable[2]), and so on until the iterable is exhausted. The final return value is a single scalar.lambda acc, n: acc * n: The two-argument lambda represents the binary operation being folded across the list. On the first callacc = 1andn = 2, returning2. On the second,acc = 2andn = 3, returning6. This continues until the product of all five numbers –120– is produced.- Optional initial value:
reduce()accepts an optional third argument as the starting accumulator:reduce(lambda acc, n: acc * n, numbers, 1). This is useful when the list might be empty – without an initial value,reduce()raisesTypeErroron an empty iterable. - Why not
math.prod()?: Python 3.8 addedmath.prod(numbers)as a built-in for exactly this case. Use it in production. Thereduce()approach is taught here because it generalises to any binary operation and demonstrates the fold pattern that underpins many functional programming concepts.
Exercise 6: Conditional Lambda
Problem Statement: Write a lambda that takes a single integer and returns the string "even" if the number is divisible by 2, or "odd" otherwise. Assign it to a variable named parity and test it on several values.
Purpose: Lambda expressions support Python’s inline conditional (ternary) syntax, making it possible to express simple branching logic without a full def block. This pattern appears frequently as the key or transform argument in map(), sorted(), and similar functions where a one-line decision is needed.
Given Input: Call parity(4), parity(7), and parity(0).
Expected Output: even, odd, even
▼ Hint
Use Python’s inline conditional inside the lambda: lambda n: "even" if n % 2 == 0 else "odd". The expression evaluates to one of the two string values depending on the condition.
▼ Solution & Explanation
Explanation:
"even" if n % 2 == 0 else "odd": This is Python’s ternary (conditional) expression. It evaluates to the value beforeifwhen the condition is true, and the value afterelsewhen it is false. Because it is a single expression, it fits naturally inside a lambda body.parity(0)returns"even": Zero is divisible by 2 with no remainder (0 % 2 == 0), so it correctly classifies as even. This is consistent with the mathematical definition of even numbers.- Composing with
map(): Becauseparityis a callable, it can be passed directly tomap():list(map(parity, [1, 2, 3, 4]))produces['odd', 'even', 'odd', 'even']. This illustrates how named lambdas become reusable building blocks for functional pipelines. - Readability note: Nested ternary expressions inside a lambda (e.g., deciding between three outcomes) quickly become hard to read. If the logic requires more than one condition, use a
deffunction with explicitif/elif/elsebranches instead.
Exercise 7: Lambda in a Dictionary
Problem Statement: Store four lambda functions in a dictionary under the keys "add", "sub", "mul", and "div". Each lambda should take two numbers and perform the corresponding arithmetic operation. Use the dictionary to build a simple calculator that looks up the operation by key and applies it to two operands.
Purpose: Storing callables in a dictionary is a clean alternative to long if/elif chains for dispatching operations. This pattern – sometimes called a dispatch table – is used in command parsers, plugin systems, and menu-driven programs. Combining it with lambdas keeps the entire definition concise.
Given Input: Apply each of the four operations to 10 and 3.
Expected Output:
add: 13 sub: 7 mul: 30 div: 3.3333333333333335
▼ Hint
- Define the dictionary with string keys and lambda values:
{"add": lambda a, b: a + b, ...}. Each lambda takes two parameters. - To call an operation, look it up and invoke it:
ops["add"](10, 3). The square-bracket lookup returns the lambda, and the parentheses immediately call it with the arguments.
▼ Solution & Explanation
Explanation:
- Lambdas as dictionary values: In Python, functions are first-class objects – they can be stored in variables, lists, and dictionaries just like integers or strings. Each lambda here is a value associated with a string key, retrievable and callable at any time.
ops["add"](10, 3): The lookupops["add"]returns the lambda object itself. Appending(10, 3)immediately calls it with those arguments. This is identical to calling a function stored in any other variable.- Dispatch table pattern: Iterating over
ops.items()applies every operation in one loop without anyif/elifbranching. Adding a new operation requires only a new dictionary entry – no changes to the loop or calling code. This open-closed design makes dispatch tables easy to extend. - Extending the calculator: You can add operations like
"mod": lambda a, b: a % bor"pow": lambda a, b: a ** bwithout modifying any existing code. For a user-facing calculator, you would look up the key from user input:op = input("Operation: "); result = ops[op](a, b)– adding aKeyErrorcheck for unknown operations.
Exercise 8: Chaining Map & Filter
Problem Statement: Given a list of integers that includes negative numbers, use filter() to remove all negatives, then use map() to square the remaining values. Chain the two operations so no intermediate named variable is required.
Purpose: Real data pipelines rarely apply just one transformation. This exercise shows how filter() and map() compose naturally by passing the output of one directly as the input of the other — producing a readable, loop-free pipeline that processes data in a single pass.
Given Input: numbers = [-3, -1, 0, 2, 4, -2, 5, 7]
Expected Output: [0, 4, 16, 25, 49]
▼ Hint
- Apply
filter(lambda n: n >= 0, numbers)first to keep only non-negative values. Becausefilter()returns a lazy iterator, you can pass it directly tomap()without converting to a list first. - Wrap the
filter()call as the second argument tomap(lambda n: n ** 2, ...). Finish by wrapping the whole expression inlist()to evaluate it.
▼ Solution & Explanation
Explanation:
- Evaluation order: Python evaluates arguments from the inside out.
filter()runs first and produces a lazy iterator of non-negative values. That iterator is passed directly tomap(), which wraps it in another lazy iterator that squares each value.list()at the outermost level triggers full evaluation. - Zero is kept: The predicate
n >= 0includes zero, so0passes the filter and0 ** 2 = 0appears in the output. If you want to exclude zero as well, change the condition ton > 0. - Single-pass efficiency: Because both
filter()andmap()are lazy, each element is processed only once aslist()pulls values through the chain. There is no intermediate list allocated between the two steps, which matters for large datasets. - List comprehension equivalent:
[n ** 2 for n in numbers if n >= 0]is the idiomatic Python alternative and is often preferred for its readability. Themap()/filter()chain is worth knowing because it mirrors the functional style used in languages like Haskell and Scala, and it composes cleanly when you need more than two stages.
Exercise 9: Sorted with Multiple Keys
Problem Statement: Given a list of employee dictionaries, each with a "name" and a "salary" key, sort the list by salary in descending order. Where two employees share the same salary, sort those entries by name in ascending alphabetical order. Use a single lambda as the key argument.
Purpose: Sorting on more than one criterion is a common real-world requirement: leaderboards, reports, and data exports all need it. This exercise shows the tuple-key trick that lets a single lambda express a multi-level sort without any additional imports or helper functions.
Given Input: employees = [{"name": "Alice", "salary": 70000}, {"name": "Bob", "salary": 90000}, {"name": "Charlie", "salary": 70000}, {"name": "Diana", "salary": 90000}]
Expected Output:
Bob $90000 Diana $90000 Alice $70000 Charlie $70000
▼ Hint
- Return a tuple from the lambda:
lambda e: (-e["salary"], e["name"]). Python sorts tuples element by element, so the first element controls the primary sort and the second controls the tiebreaker. - Negate the salary (
-e["salary"]) to achieve descending order without usingreverse=True, which would also reverse the name sort direction and break the tiebreaker.
▼ Solution & Explanation
Explanation:
- Tuple key: When the
keyfunction returns a tuple, Python compares tuples lexicographically: it compares the first element, and only moves to the second if the first elements are equal. Returning(-salary, name)means salary is the primary sort criterion and name is the tiebreaker. - Negating for descending order: Negating a numeric key (
-e["salary"]) inverts the sort direction for that field only. A higher salary becomes a more negative number, so it sorts to the front. This technique works for any numeric field and avoids the complication ofreverse=True, which would flip all sort levels simultaneously. - Stability of Python’s sort: Python’s
sorted()is a stable sort (Timsort). Elements that compare equal under the key function retain their original relative order. The tuple key makes equal-salary ties explicit, but stability would preserve insertion order even without the name component. - Negation limitation: The negation trick only works for numeric fields. To sort a string field descending, you would need a different approach – such as using
functools.cmp_to_keywith a custom comparator, or sorting twice (Python’s stable sort guarantees correctness when you do a secondary sort first and a primary sort second).
Exercise 10: Partial Functions
Problem Statement: Write a general multiply(x, n) function that returns x * n. Use functools.partial() to create two specialised functions from it: double(x), which always multiplies by 2, and triple(x), which always multiplies by 3. Call both with several values.
Purpose: partial() lets you lock in one or more arguments of an existing function to produce a simpler, more focused callable. This avoids writing wrapper functions by hand and is particularly useful when an API expects a one-argument callable but your logic needs additional context baked in.
Given Input: Call double(5), double(9), triple(4), and triple(7).
Expected Output:
double(5) = 10 double(9) = 18 triple(4) = 12 triple(7) = 21
▼ Hint
- Import
partialfromfunctools:from functools import partial. - Create the specialised functions with
partial(multiply, n=2)andpartial(multiply, n=3). The second argument topartial()is the value you want pre-filled for thenparameter ofmultiply. - The resulting
doubleandtripleobjects are fully callable — pass them a single argumentxand they behave exactly like a one-parameter function.
▼ Solution & Explanation
Explanation:
partial(func, *args, **kwargs): Returns a new callable that behaves likefunccalled with the given arguments pre-filled. Any additional arguments passed when calling the partial are appended to (or merged with) the pre-filled ones.- Keyword vs. positional pre-filling: Using
partial(multiply, n=2)pre-fillsnby name, leavingxto be provided at call time. You could also use positional pre-filling:partial(multiply, 5)would lockx=5and leavenfree — the direction depends on argument order and which parameter you want to specialise. - Practical use cases:
partial()shines when passing callbacks to APIs that expect a fixed signature. For example,map(partial(multiply, n=10), numbers)scales every number in a list by 10 without needing a dedicated lambda ordef. - Inspecting a partial: The resulting object exposes
double.func(the original function),double.args(pre-filled positional arguments), anddouble.keywords(pre-filled keyword arguments), making it straightforward to introspect or log what a partial was built from.
Exercise 11: Function Composition
Problem Statement: Write a compose(f, g) utility function that returns a new function equivalent to applying g first and then f to the result — that is, compose(f, g)(x) should equal f(g(x)). Test it by composing a lambda that doubles a number with a lambda that adds 3.
Purpose: Function composition is a fundamental concept in functional programming. Rather than calling functions one by one in sequence, composition lets you build a new function by wiring existing ones together. The result is more reusable, more testable, and reads like a description of a pipeline rather than a sequence of instructions.
Given Input: Compose double = lambda x: x * 2 and add_three = lambda x: x + 3. Call the composed function with 5.
Expected Output: double(add_three(5)) = 16
▼ Hint
compose(f, g)should return a lambda (or inner function) that takesxand returnsf(g(x)). The returned callable closes overfandg.- Test the direction carefully: in mathematical notation,
(f ∘ g)(x) = f(g(x))meansgis applied first. Verify with a simple example before building on it.
▼ Solution & Explanation
Explanation:
return lambda x: f(g(x)): The inner lambda closes overfandgfrom the enclosingcomposecall. Each time the returned function is called with a valuex, it evaluatesg(x)first, then passes that result tof.- Order matters:
compose(double, add_three)(5)computesdouble(add_three(5)): add 3 to get 8, then double to get 16. Reversing tocompose(add_three, double)(5)givesadd_three(double(5)): double to get 10, then add 3 to get 13. Composition is not commutative. - Extending to multiple functions: A variadic composer can chain any number of functions using
reduce():from functools import reduce; def compose_many(*fns): return reduce(compose, fns). This applies the rightmost function first and works its way left, matching standard mathematical notation. - Real-world relevance: Function composition appears in data transformation pipelines, middleware stacks, and decorator chains. Python’s own decorator syntax (
@decorator) is essentially syntactic sugar for a specific kind of function composition where the outer function wraps the inner one.
Exercise 12: Flatten with Map & Reduce
Problem Statement: Given a list of lists, use functools.reduce() with a lambda to flatten it into a single list. Do not use any for loops, list comprehensions, or itertools.
Purpose: Flattening a nested structure is a classic exercise in functional reduction. Solving it with reduce() reinforces how any operation that accumulates a result across a sequence — not just arithmetic — can be expressed as a fold. It also demonstrates that the accumulator in reduce() can be any type, not just a number.
Given Input: nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
Expected Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
▼ Hint
- Use
reduce(lambda acc, sublist: acc + sublist, nested). The+operator on two lists concatenates them, so each iteration appends the next sublist to the growing accumulator. - Pass
[]as the optional third argument toreduce()to set the initial accumulator to an empty list. Without it, the first sublist becomes the starting value, which works here but is safer to make explicit.
▼ Solution & Explanation
Explanation:
lambda acc, sublist: acc + sublist: On each step,accholds the flattened list built so far andsublistis the next inner list. The+operator creates a new concatenated list, which becomes the nextacc. After all three sublists are processed,accholds the fully flattened result.- Initial value
[]: Providing an empty list as the third argument guarantees the accumulator starts as a list even ifnestedis empty. Without it,reduce()would raiseTypeErroron an empty input because there is no first element to use as the starting value. - Performance note: Each
acc + sublistcreates a brand-new list, copying all elements accumulated so far. For a large number of sublists this results in O(n²) copies. A more efficient approach isreduce(lambda acc, s: acc.__iadd__(s) or acc, nested, [])using in-place extension, or simplylist(itertools.chain.from_iterable(nested))in production code. - Accumulator type flexibility: This exercise illustrates that
reduce()is not limited to numbers. The accumulator can be a list, dictionary, string, or any other type — whatever your binary function returns. This makesreduce()a general-purpose fold over any data structure.
Exercise 13: Custom Key with sorted()
Problem Statement: Given a list of strings, sort them alphabetically by their last character using a lambda as the key argument to sorted(). Where two strings end with the same character, preserve their original relative order.
Purpose: The key parameter of sorted() accepts any callable that extracts a sort value from each element. This exercise reinforces that the key does not have to be the element itself — it can be any derived value, including a substring, a computed property, or the result of a function call.
Given Input: words = ["banana", "apple", "cherry", "date", "fig", "kiwi"]
Expected Output: ['banana', 'apple', 'date', 'fig', 'cherry', 'kiwi']
▼ Hint
- Pass
key=lambda s: s[-1]tosorted(). In Python, negative indices count from the end of a sequence, sos[-1]is always the last character of the string. - Python’s sort is stable, so words that share the same last character will appear in their original list order relative to each other — no extra work needed for the tiebreaker.
▼ Solution & Explanation
Explanation:
lambda s: s[-1]: Extracts the last character of each string. The last characters of the input words are:a(banana),e(apple),y(cherry),e(date),g(fig),i(kiwi). Sorting these alphabetically givesa, e, e, g, i, y, which maps back to the output order.- Stable sort handles ties: Both
"apple"and"date"end with"e". Because Python’s Timsort is stable, their relative order from the original list is preserved:"apple"came before"date", so it stays before it in the output. - Key function called once per element: Python computes the key for each element exactly once before comparing. This means even an expensive key function is not repeatedly called during comparisons — making it safe to use
keywith slow operations like database lookups or regex matches. - Beyond last character: The same pattern generalises to any positional extraction: sort by second character with
s[1], by last two characters withs[-2:], or by file extension withs.rsplit(".", 1)[-1]. The lambda is just a projection function — it can express any transformation that produces a comparable value.
Exercise 14: Memoization with lru_cache
Problem Statement: Write a recursive Fibonacci function decorated with @functools.lru_cache. Then write the same function without caching. Use Python’s time module to measure and compare how long each version takes to compute fib(35).
Purpose: Naive recursive Fibonacci has exponential time complexity because it recomputes the same subproblems repeatedly. lru_cache applies memoization automatically: each unique argument is computed once and the result is stored for reuse. This exercise makes the performance difference concrete and introduces one of Python’s most useful built-in optimisation tools.
Given Input: Compute fib(35) with both cached and uncached versions and print the elapsed time for each.
Expected Output:
Cached fib(35) = 9227465 in 0.000012s Uncached fib(35) = 9227465 in 2.817043s
▼ Hint
- Import
lru_cachefromfunctoolsand apply it as a decorator:@lru_cache(maxsize=None)above the function definition.maxsize=Nonemeans the cache is unlimited in size. - Write the uncached version as a plain recursive function with the same body but without the decorator.
- Use
time.perf_counter()before and after each call to measure elapsed time in seconds with high precision.
▼ Solution & Explanation
Explanation:
@lru_cache(maxsize=None): Wraps the function with a Least Recently Used cache. On each call, Python checks whether the arguments are already in the cache. If they are, the stored result is returned immediately without executing the function body.maxsize=Nonedisables the eviction policy so every computed value is kept indefinitely — appropriate for a pure function like Fibonacci where results never change.- Why the uncached version is slow: Computing
fib_uncached(35)callsfib_uncached(34)andfib_uncached(33). Each of those branches calls two more, and so on, producing a call tree with roughly 2³⁵ (about 34 billion) nodes — though many are duplicates. The cached version computes each of the 36 unique inputs (fib(0)throughfib(35)) exactly once. time.perf_counter(): Returns a high-resolution timer value in seconds. Taking the difference before and after a function call gives wall-clock elapsed time. It is more precise thantime.time()for short durations and is the recommended choice for benchmarking.- Python 3.9 shorthand: From Python 3.9 onwards you can use
@lru_cachewithout parentheses (equivalent tomaxsize=128) or@functools.cache(equivalent tomaxsize=None). The explicitlru_cache(maxsize=None)form used here works on all Python 3 versions and makes the unlimited-cache intent clear.
Exercise 15: Currying
Problem Statement: Implement a curry(func) utility that transforms a two-argument function into a chain of single-argument functions, so that curry(add)(2)(3) returns the same result as add(2, 3). Then extend the idea to a manually curried three-argument function.
Purpose: Currying converts a multi-argument function into a sequence of one-argument functions, each returning the next function in the chain until all arguments have been supplied. This technique enables partial application without functools.partial, makes functions trivially composable, and is a cornerstone concept in functional languages such as Haskell and ML.
Given Input: A two-argument add(a, b) function and a three-argument volume(l, w, h) function.
Expected Output:
curry(add)(2)(3) = 5 curry(add)(10)(7) = 17 volume(2)(3)(4) = 24 box_with_height_4(2)(3) = 24
▼ Hint
- For a two-argument curry,
curry(func)should returnlambda a: lambda b: func(a, b). The outer lambda capturesaand returns a new lambda waiting forb. - For a three-argument version, extend the chain one level deeper:
lambda a: lambda b: lambda c: func(a, b, c). - Each call in the chain
f(2)(3)(4)invokes the outermost lambda with2, which returns a new lambda; that is called with3, returning another lambda; finally called with4to produce the result.
▼ Solution & Explanation
Explanation:
lambda a: lambda b: func(a, b): The outer lambda capturesfuncvia closure and returns a new function waiting fora. When called witha, it returns another lambda waiting forb. Only whenbis supplied does the original function execute. Each step in the chain is a closure that carries the arguments collected so far.- Partial application via currying:
add5 = curried_add(5)produces a reusable function that adds 5 to any number. This is the same result asfunctools.partial(add, 5)but achieved through the currying structure itself, without any library support. - Argument order matters: In the
make_box(4)example, the height is fixed first so the returned function accepts length and width in sequence. Designing curried functions with the most-stable argument first and the most-varying argument last is a functional programming convention that maximises reuse. - General-arity currying: The
curry()here handles exactly two arguments. A fully general solution inspectsfunc.__code__.co_argcountor usesinspect.signature()to determine how many arguments to collect before calling the original function. Libraries such astoolzprovide this astoolz.curry.
Exercise 16: Pipeline Function
Problem Statement: Build a pipeline(*funcs) utility that takes any number of single-argument functions and returns a new function. When called with a value, the returned function passes it through each function in left-to-right order, feeding each result into the next — like a Unix pipe. Demonstrate it by building a text-processing pipeline.
Purpose: A pipeline makes data transformation code read in the same direction as the transformations happen. Instead of deeply nested calls like f(g(h(x))), you write pipeline(h, g, f)(x) where the order of functions matches the order of operations. This pattern is central to data engineering, ETL scripts, and functional-style Python.
Given Input: The string " hello, world! " passed through a pipeline that strips whitespace, converts to title case, replaces the comma, and appends an exclamation mark.
Expected Output: Hello World!
▼ Hint
- Use
functools.reduce()insidepipeline:reduce(lambda value, func: func(value), funcs, initial_value). The accumulator is the running value and each function infuncsis applied to it in turn. - Return a lambda from
pipeline()that capturesfuncsand accepts the initial value:return lambda x: reduce(lambda v, f: f(v), funcs, x).
▼ Solution & Explanation
Explanation:
reduce(lambda value, f: f(value), funcs, x): The accumulator starts asx. On each step, the current functionfis called with the running value and the result becomes the new value. After all functions have been applied, the final value is returned. This is a left fold where the data flows left to right through the function list.- Functions as first-class values:
str.stripandstr.titleare passed as unbound method references without calling them. Each one is a callable that takes a string and returns a string, satisfying the single-argument contract the pipeline requires. - Pipeline vs. compose:
pipeline(f, g, h)appliesffirst, theng, thenh— left to right. Thecompose(f, g)from Exercise 11 applies right to left (gfirst, thenf), matching mathematical function composition notation. Both are useful; the choice depends on which reading direction feels more natural for the use case. - Reusability: The pipeline object
processcan be called as many times as needed on different input strings. Steps can be added, removed, or reordered simply by editing the argument list topipeline(), making the transformation logic easy to adjust without touching calling code.
Exercise 17: Transducer Pattern
Problem Statement: Compose a filter (keep only even numbers) and a map (square each value) into a single reducing function — a transducer — that processes a list in one pass without creating any intermediate list. Apply it manually using functools.reduce().
Purpose: Chaining filter() and map() creates a hidden intermediate iterator for each step. A transducer collapses the entire transformation stack into one reducing step, eliminating all intermediate allocations. This is how high-performance data pipelines in Clojure and similar languages work, and understanding it deepens your intuition for how lazy pipelines and stream processing operate.
Given Input: numbers = list(range(1, 11))
Expected Output: [4, 16, 36, 64, 100]
▼ Hint
- A transducer is a function that takes a reducing function and returns a new reducing function. Start by writing a
mapping(transform)transducer that wraps a reducer:lambda reducer: lambda acc, x: reducer(acc, transform(x)). - Write a
filtering(predicate)transducer similarly:lambda reducer: lambda acc, x: reducer(acc, x) if predicate(x) else acc. - Compose the two transducers by calling one with the result of the other, then apply the composed reducer via
reduce(composed_reducer, numbers, []). The base reducer appends to the accumulator list.
▼ Solution & Explanation
Explanation:
- What a transducer is: A transducer is a higher-order function with the signature
reducer -> reducer. It transforms a reducing function into a new one that applies an extra step (a map or filter operation) before delegating to the inner reducer. Composing two transducers stacks those steps without ever materialising an intermediate collection. - Composition order:
filtering(...)(mapping(...)(append))reads inside-out. The innermost call wrapsappendwith the squaring step, producing a reducer that squares and then appends. The outer call wraps that with the even-check step: only even numbers reach the squaring reducer. The overall data flow is: filter first, square second, append third. - Single-pass guarantee:
reduce(xf, numbers, [])iterates overnumbersexactly once. For each element,xfdecides whether to include it and how to transform it in a single function call chain. No intermediate list is created between the filtering and mapping steps. - When this matters: For small lists the performance difference is negligible. For pipelines processing millions of records or infinite streams, eliminating intermediate allocations can reduce memory usage dramatically. The transducer pattern also makes the transformation stack inspectable and reusable across different collection types.
Exercise 18: Lazy Evaluation with Generators
Problem Statement: Define an infinite sequence of natural numbers using a generator function. Use generator expressions (not map() or filter()) to lazily filter out odd numbers and square the remaining evens. Take only the first 5 results using itertools.islice().
Purpose: An infinite sequence cannot be fully evaluated — it must be processed lazily. Generator expressions create lazy pipelines that compute each value only when it is requested, using constant memory regardless of how long the sequence is. This exercise shows how to build such pipelines and how itertools.islice() acts as the valve that controls how many values are pulled through.
Given Input: An infinite stream of natural numbers starting from 1.
Expected Output: [4, 16, 36, 64, 100]
▼ Hint
- Write a generator
naturals()usingwhile Trueto yield an unbounded sequence: yieldn, then incrementn. This generator never raisesStopIterationon its own. - Layer generator expressions on top:
evens = (n for n in naturals() if n % 2 == 0), thensquared = (n ** 2 for n in evens). Each expression is a lazy wrapper — no values are computed yet. - Use
list(itertools.islice(squared, 5))to pull exactly 5 values through the entire pipeline and stop.
▼ Solution & Explanation
Explanation:
- Infinite generator:
naturals()useswhile Truecombined withyieldto produce an unbounded stream. Control returns to the caller after eachyieldand resumes at the next line whennext()is called again. The generator itself never finishes — it relies on the consumer to stop pulling values. - Layered generator expressions: Each generator expression wraps the previous one without consuming it. Writing
(n ** 2 for n in evens)creates a new generator object that, when iterated, pulls fromevens, which pulls fromnaturals(). The entire chain is wired up but idle until a value is requested. itertools.islice(iterable, n): Takes at mostnitems from any iterable and then stops. It is the standard tool for safely consuming a finite prefix of an infinite stream. Without it, callinglist(squared)would loop forever.- Memory usage: At any point in time, only one value exists in memory as it flows through the pipeline. Contrast this with a list-based approach where
filter()andmap()over a large pre-built list would require storing all intermediate values. Generator pipelines scale to arbitrarily large or infinite data sources with O(1) memory overhead.
Exercise 19: Higher-Order Function Library
Problem Statement: Implement your own versions of my_map(func, lst), my_filter(pred, lst), and my_reduce(func, lst, initial) from scratch using only recursion and lambdas. No loops, no built-in map/filter/reduce, and no list comprehensions. Verify each against a known input.
Purpose: Rebuilding standard library functions from first principles is one of the most effective ways to understand how they work. This exercise makes explicit the recursive structure hidden inside every functional operation: process the head of the list, then recurse on the tail until the base case (an empty list) is reached.
Given Input: numbers = [1, 2, 3, 4, 5]
Expected Output:
my_map (square, numbers) = [1, 4, 9, 16, 25] my_filter (is_even, numbers) = [2, 4] my_reduce (multiply, numbers, 1) = 120
▼ Hint
- Each function has the same recursive skeleton: base case returns a neutral value when
lstis empty; recursive case processeslst[0](the head) and concatenates or combines it with the result of calling the function again onlst[1:](the tail). - For
my_map: return[]when the list is empty; otherwise return[func(lst[0])] + my_map(func, lst[1:]). - For
my_filter: includelst[0]in the result only whenpred(lst[0])is true; always recurse onlst[1:]. - For
my_reduce: the accumulator starts asinitial; on each recursive call passfunc(initial, lst[0])as the new accumulator.
▼ Solution & Explanation
Explanation:
- Recursive structure: Each function follows the same two-branch pattern. The base case (
if not lst: return []orreturn initial) terminates the recursion when the list is exhausted. The recursive case processes the head element and delegates the rest of the work to the same function called on the tail, one element shorter each time. my_map– transform and prepend:[func(lst[0])] + my_map(func, lst[1:])appliesfuncto the first element, wraps the result in a single-element list, and concatenates it with the recursively mapped tail. The final result is assembled as the call stack unwinds.my_filter– conditional inclusion: The ternary expression[head] + rest if pred(head) else restincludes the head only when the predicate is true. Either way, the recursion continues on the tail so all elements are visited.my_reduce– tail-recursive accumulation: The accumulator is updated at each call rather than on the way back up, making this tail-recursive in structure. Python does not optimise tail calls, so a list of more than a few thousand elements would hit the default recursion limit. In production, use the built-infunctools.reduce()which uses an iterative implementation internally.
Exercise 20: Monadic Chaining
Problem Statement: Implement a Maybe class that wraps a value (or None) and provides a .bind(func) method. bind applies func to the wrapped value if it is not None, wrapping the result in a new Maybe; if the value is None, it returns Maybe(None) immediately without calling func. Chain several lambda transformations and show that a None at any step short-circuits the rest of the chain.
Purpose: The Maybe monad is the functional programming solution to the problem of None-propagation. Instead of sprinkling if x is not None: guards throughout your code, you chain transformations and let the monad handle the short-circuit logic invisibly. This pattern appears in Haskell as Maybe, in Scala as Option, and in Python libraries such as returns and pymonad.
Given Input: Two chains starting from Maybe(10) and Maybe(None), each passing through the same three lambda transformations.
Expected Output:
Maybe(10).bind(double).bind(add_three).bind(safe_sqrt) = Maybe(4.69...) Maybe(None).bind(double).bind(add_three).bind(safe_sqrt) = Maybe(None)
▼ Hint
- Store the value in
self.valueinside__init__. Give the class a__repr__that returnsf"Maybe({self.value})"for clear output. - In
bind(self, func): ifself.value is None, returnMaybe(None)immediately. Otherwise returnMaybe(func(self.value)). - For the
safe_sqrttransformation, returnNonefor negative inputs so you can demonstrate a mid-chain short-circuit as well.
▼ Solution & Explanation
Explanation:
bind(self, func): This is the core monad operation (calledflatMapor>>=in other languages). It checks the wrapped value before applying the function. If the value isNone, the function is never called and theNonepropagates. If the value is present, the function runs and its result is wrapped in a newMaybe.- Short-circuit propagation: Once
Noneappears anywhere in the chain, every subsequentbindcall returnsMaybe(None)immediately without executing the lambda. The entire chain runs to completion without raising exceptions or requiring any conditional checks in the calling code. - Returning
Nonefrom a lambda: Whensafe_sqrtreturnsNonefor a negative input,bindwraps thatNonein aMaybe. The nextbindseesself.value is Noneand short-circuits. This demonstrates that the monad handles failures originating anywhere in the chain, not just at the start. - Comparison to exception handling: The
Maybemonad andtry/exceptboth deal with operations that can fail, but in different styles. Exceptions use control flow that jumps out of the normal execution path.Maybekeeps everything in a linear chain and encodes the failure as data inside the wrapper. The monadic approach is easier to compose and reason about when the entire pipeline is built from pure functions.

Leave a Reply