This article offers 20 coding exercises that build your fluency with Python’s functional programming toolkit, from the basics of lambda through to advanced patterns. Topics covered include writing and applying lambda functions; using map(), filter(), and reduce() for data transformation; sorting with custom keys; creating partial functions and curried functions with functools; memoising recursive functions with lru_cache; and composing reusable function pipelines
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.
- Also, Solve Python Exercises: 29 topic-wise exercises with over 800+ coding questions
+ 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
▼ 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)]
▼ 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]
▼ 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]
▼ 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
▼ 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
▼ 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
▼ 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]
▼ 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
▼ 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
▼ 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
▼ 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]
▼ 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']
▼ 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
▼ 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
▼ 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!
▼ 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]
▼ 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]
▼ 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
▼ 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)
▼ 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