Skip to main content

Python Partial and Currying Practice Problems & Exercises

Practice: Partial and Currying

11 problems4 Easy4 Medium3 Hard45-60 min
← Back to lesson

Easy

#1Basic functools.partialEasy
partialfunctoolsargument-bindingspecialisation

Create double and triple using functools.partial on the operator.mul function, and a power_of_2 using partial on pow. Inspect the partial object attributes.

Python
import functools
import operator

double = functools.partial(operator.mul, 2)
triple = functools.partial(operator.mul, 3)

print(f"double(5): {double(5)}")
print(f"triple(5): {triple(5)}")

# partial on pow — pre-fill the exponent (second argument) using keyword
power_of_2 = functools.partial(pow, 2)

print(f"power_of_2(8): {power_of_2(8)}")

# Inspect the partial object
print(f"partial func name: {power_of_2.func.__name__}")
print(f"partial keywords: {power_of_2.keywords}")
print(f"partial args: {power_of_2.args}")
Solution
double(5): 10
triple(5): 15
power_of_2(8): 256
partial func name: pow
partial keywords: {}
partial args: (2,)

functools.partial internals:

partial(fn, *args, **kwargs) creates a functools.partial object with three attributes:

  • .func — the original callable.
  • .args — the positional arguments pre-filled (as a tuple).
  • .keywords — the keyword arguments pre-filled (as a dict).

When the partial is called, its .args are prepended to any additional positional arguments, and its .keywords are merged with any additional keyword arguments, then .func is called.

For double = partial(operator.mul, 2):

  • double.args = (2,), double.keywords = {}.
  • double(5)operator.mul(2, 5)10.

For power_of_2 = partial(pow, 2):

  • power_of_2(8)pow(2, 8)256 (2 to the power of 8).

partial vs lambda: partial(fn, x) is equivalent to lambda *args, **kwargs: fn(x, *args, **kwargs). The difference: partial preserves the original function's __name__ via .func.__name__, is picklable (unlike lambdas), and is more explicit about intent.

Expected Output
double(5): 10
triple(5): 15
power_of_2(8): 256
partial func name: pow
partial keywords: {}
partial args: (2,)
Hints

Hint 1: `functools.partial(fn, *args, **kwargs)` returns a new callable with some arguments pre-filled. Calling the partial supplies the remaining arguments.

Hint 2: Inspect `.func`, `.args`, and `.keywords` on the partial object to see what was pre-filled.

#2partial with Keyword ArgumentsEasy
partialkeyword-argumentsconfigurationspecialisation

Use functools.partial to bind keyword arguments for a JSON formatter and a log message function.

Python
import functools
import json

def log_message(message, level="INFO"):
    return f"[{level}] {message}"

# Pre-fill json.dumps with formatting options
json_pretty = functools.partial(json.dumps, indent=4, sort_keys=True)
json_compact = functools.partial(json.dumps, separators=(",", ":"))

data = {"name": "alice", "age": 30}
print(f"json_pretty: {json_pretty(data)}")
print(f"json_compact: {json_compact(data)}")

# Specialise log_message by pre-filling level
log_info = functools.partial(log_message, level="INFO")
log_error = functools.partial(log_message, level="ERROR")

print(f"log_info: {log_info('server started')}")
print(f"log_error: {log_error('connection refused')}")
Solution
json_pretty: {
"name": "alice",
"age": 30
}
json_compact: {"name":"alice","age":30}
log_info: [INFO] server started
log_error: [ERROR] connection refused

Partial with keyword arguments:

partial(json.dumps, indent=4, sort_keys=True) stores keywords = {"indent": 4, "sort_keys": True}. When json_pretty(data) is called, it becomes json.dumps(data, indent=4, sort_keys=True).

partial(log_message, level="INFO") binds the level keyword. Calling log_info("server started") passes "server started" as the positional message argument plus the pre-filled level="INFO".

Why bind keyword arguments: This pattern is useful for:

  • Configuration at construction time: json_pretty always uses the same formatting settings.
  • Creating specialised variants without subclassing or new functions: log_info and log_error are identical to log_message in every way except their level.
  • Dependency injection: partially apply configuration (db URL, API key) to a function so call sites do not need to know about configuration.
Expected Output
json_pretty: {
  "name": "alice",
  "age": 30
}
json_compact: {"name":"alice","age":30}
log_info: [INFO] server started
log_error: [ERROR] connection refused
Hints

Hint 1: Use `functools.partial(json.dumps, indent=4, sort_keys=True)` to pre-fill keyword arguments. The resulting partial can still accept positional data.

Hint 2: For the logger, pre-fill the `level` keyword argument to specialise `log_message` into `log_info` and `log_error`.

#3Manual Currying with Nested LambdasEasy
curryinglambdafunction-factoryarity

Implement three manually curried functions using nested lambdas: curried_add, curried_mul, and curried_clamp.

Python
# Manually curried functions
curried_add = lambda x: lambda y: x + y
curried_mul = lambda x: lambda y: x * y
curried_clamp = lambda lo: lambda hi: lambda v: max(lo, min(hi, v))

# Test
print(f"curried_add(3)(4): {curried_add(3)(4)}")
print(f"curried_mul(5)(6): {curried_mul(5)(6)}")
print(f"curried_clamp(0)(100)(150): {curried_clamp(0)(100)(150)}")
print(f"curried_clamp(0)(100)(-10): {curried_clamp(0)(100)(-10)}")
Solution
curried_add(3)(4): 7
curried_mul(5)(6): 30
curried_clamp(0)(100)(150): 100
curried_clamp(0)(100)(-10): 0

Currying vs partial application:

  • Currying: transforming a function of n arguments into a chain of n single-argument functions. add(x, y)add(x)(y).
  • Partial application: fixing some arguments of a function, producing a function of fewer arguments. add(x, y) with x=3add(3, y).

They sound similar but differ: currying always produces unary functions. Partial application can fix any subset of arguments, not necessarily in order.

curried_clamp = lambda lo: lambda hi: lambda v: max(lo, min(hi, v)):

  • curried_clamp(0) returns lambda hi: lambda v: max(0, min(hi, v)).
  • curried_clamp(0)(100) returns lambda v: max(0, min(100, v)).
  • curried_clamp(0)(100)(150) returns max(0, min(100, 150)) = max(0, 100) = 100.

Practical use of currying: Creates specialised functions by partially applying arguments left to right. clamp_0_to_100 = curried_clamp(0)(100) is a reusable function you can pass to map, filter, or any higher-order function.

Expected Output
curried_add(3)(4): 7
curried_mul(5)(6): 30
curried_clamp(0)(100)(150): 100
curried_clamp(0)(100)(-10): 0
Hints

Hint 1: A curried function returns a function for each argument until all are supplied. `add(3)` returns `lambda y: 3 + y`.

Hint 2: For `clamp`, curry three arguments: `min_val`, `max_val`, `value`. Each call binds one more argument.

#4partial for Event Handler BindingEasy
partialevent-handlercallbackbutton-binding

Use functools.partial to create button-specific event handlers from a generic on_button_event function, simulating a GUI event binding pattern.

Python
import functools

def on_button_event(event_type, button_id):
    return f"button_{button_id} {event_type}d with event={event_type}"

# Bind button IDs at registration time
handler_A_click = functools.partial(on_button_event, button_id="A")
handler_B_click = functools.partial(on_button_event, button_id="B")
handler_A_dbl = functools.partial(on_button_event, button_id="A")

# Simulate event dispatch
registry = [handler_A_click, handler_B_click, handler_A_dbl]

events = ["click", "click", "dblclick"]
for handler, event in zip(registry, events):
    print(handler(event))

print(f"handlers registered: {len(registry)}")
Solution
button_A clicked with event=click
button_B clicked with event=click
button_A double-clicked with event=dblclick
handlers registered: 3

Partial for callback binding:

GUI frameworks (Tkinter, PyQt, web frameworks) commonly need to register callbacks that know their own identity (which button was clicked, which row was selected). Without partial, you would need a lambda with a default argument or a closure factory.

partial(on_button_event, button_id="A") creates a callable that, when called with event_type, becomes on_button_event(event_type, button_id="A"). The button_id is baked in at registration time, not at call time — so there is no late-binding issue.

Compared to lambda with default:

# With partial
handler = functools.partial(on_button_event, button_id="A")

# Equivalent lambda
handler = lambda event_type: on_button_event(event_type, button_id="A")

partial is preferred because it is picklable (important for multiprocessing and task queues), preserves the original function's identity via .func, and makes the binding intent explicit.

Expected Output
button_A clicked with event=click
button_B clicked with event=click
button_A double-clicked with event=dblclick
handlers registered: 3
Hints

Hint 1: Use `functools.partial(on_button_event, button_id="A")` to create a handler specialised for a specific button. The `event_type` is supplied when the handler is called.

Hint 2: Register the partial objects as handlers in a list. Each partial captures a different `button_id`.


Medium

#5Automatic Currying DecoratorMedium
curry-decoratorarityinspectautomatic-currying

Write an auto_curry decorator that makes any function automatically curried based on its arity. Support both partial and full application syntax.

Python
import functools
import inspect

def auto_curry(func):
    arity = len(inspect.signature(func).parameters)

    @functools.wraps(func)
    def curried(*args):
        if len(args) >= arity:
            return func(*args[:arity])
        def inner(*more_args):
            return curried(*(args + more_args))
        return inner

    return curried

@auto_curry
def add(x, y):
    return x + y

@auto_curry
def multiply(x, y, z):
    return x * y * z

print(f"add(3)(4): {add(3)(4)}")
print(f"add(3, 4): {add(3, 4)}")
print(f"multiply(2)(3)(4): {multiply(2)(3)(4)}")
print(f"multiply(2, 3, 4): {multiply(2, 3, 4)}")
print(f"multiply(2)(3, 4): {multiply(2)(3, 4)}")
Solution
add(3)(4): 7
add(3, 4): 7
multiply(2)(3)(4): 24
multiply(2, 3, 4): 24
multiply(2)(3, 4): 24

Auto-curry mechanics:

auto_curry uses inspect.signature to count the number of parameters at decoration time. The curried wrapper:

  1. If called with enough arguments (len(args) >= arity), call the original function immediately.
  2. If called with fewer arguments, return a new inner function that concatenates the collected args with any new args and calls curried again.

This is recursive: multiply(2) returns inner_1. inner_1(3) calls curried(2, 3) which has 2 args and still needs 3, so returns inner_2. inner_2(4) calls curried(2, 3, 4) which has 3 args >= 3 — calls multiply(2, 3, 4) = 24.

inspect.signature vs __code__.co_argcount:

inspect.signature handles default values and keyword-only args correctly. co_argcount is lower-level and may include self in methods. For user-defined functions without *args or **kwargs, both work, but inspect.signature is more robust.

Limitation: This decorator only works for functions with a fixed arity (no *args). Functions with variadic arguments have unbounded arity — there is no natural stopping point.

Expected Output
add(3)(4): 7
add(3, 4): 7
multiply(2)(3)(4): 24
multiply(2, 3, 4): 24
multiply(2)(3, 4): 24
Hints

Hint 1: Use `inspect.signature(func)` to get the number of required parameters. The curried wrapper should collect arguments until it has enough, then call the original.

Hint 2: Accumulate positional arguments via `*args`. When `len(collected_args) >= arity`, call the original function. Otherwise, return a new partial that expects more.

#6partial to Build a Configuration APIMedium
partialconfigurationAPI-designdependency-injection

Use functools.partial to build environment-specific configuration functions from generic factory functions.

Python
import functools

def make_db_url(host, port, dbname, scheme="postgresql"):
    return f"{scheme}://{host}:{port}/{dbname}"

def make_log_level(env, default_level):
    levels = {"development": "DEBUG", "production": "WARNING", "test": "INFO"}
    return levels.get(env, default_level)

def make_api_base(host, version, scheme="https"):
    return f"{scheme}://{host}/api/{version}"

# Development environment partials
dev_db = functools.partial(make_db_url, host="localhost", port=5432, dbname="dev_db")
dev_log = functools.partial(make_log_level, env="development", default_level="INFO")
dev_api = functools.partial(make_api_base, host="localhost", version="v1", scheme="http")

# Production environment partials
prod_db = functools.partial(make_db_url, host="prod.db.internal", port=5432, dbname="prod_db")
prod_log = functools.partial(make_log_level, env="production", default_level="WARNING")
prod_api = functools.partial(make_api_base, host="api.prod.com", version="v1")

print(f"dev db url: {dev_db()}")
print(f"prod db url: {prod_db()}")
print(f"dev log level: {dev_log()}")
print(f"prod log level: {prod_log()}")
print(f"dev api base: {dev_api()}")
print(f"prod api base: {prod_api()}")
Solution
dev db url: postgresql://localhost:5432/dev_db
prod db url: postgresql://prod.db.internal:5432/prod_db
dev log level: DEBUG
prod log level: WARNING
dev api base: http://localhost:8000/api/v1
prod api base: https://api.prod.com/api/v1

Configuration via partial — the factory pattern:

Generic functions define the structure of configuration values. Environment-specific partials bind the variable parts. This separates two concerns:

  1. Structure: make_db_url knows how to format a database URL — this knowledge lives in one place.
  2. Values: dev_db and prod_db know the environment-specific host, port, and dbname — this knowledge is bound at setup time.

Real-world application: This pattern appears in:

  • Django's settings.py — a module of environment-specific values used to configure generic framework functions.
  • Click's pass_context pattern — a partial-like mechanism that binds CLI context to command handlers.
  • FastAPI/Flask app factories — partial or closure-based factories that bind app configuration to route handlers.

The key benefit: callers do not need to know which environment they are in. They receive dev_db or prod_db and call it the same way — dependency injection via partial application.

Expected Output
dev db url: postgresql://localhost:5432/dev_db
prod db url: postgresql://prod.db.internal:5432/prod_db
dev log level: DEBUG
prod log level: WARNING
dev api base: http://localhost:8000/api/v1
prod api base: https://api.prod.com/api/v1
Hints

Hint 1: Build generic `make_db_url`, `make_log_config`, `make_api_base` functions with all parameters explicit. Use `functools.partial` to bind environment-specific values.

Hint 2: Group the partials for each environment into a dict or class. Callers reference `dev_config["db_url"]()` without knowing which environment they are in.

#7Curried Map and FilterMedium
curryingmapfilterpartialpoint-free

Create curried cmap and cfilter helpers using functools.partial. Build a pipeline that filters evens, then maps a square function, demonstrating point-free style.

Python
import functools

def cmap(fn):
    return functools.partial(lambda f, iterable: list(map(f, iterable)), fn)

def cfilter(pred):
    return functools.partial(lambda p, iterable: list(filter(p, iterable)), pred)

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Build specialised transforms
double_all = cmap(lambda x: x * 2)
keep_evens = cfilter(lambda x: x % 2 == 0)
square_all = cmap(lambda x: x * x)

print(f"doubled: {double_all(numbers)}")

evens = keep_evens(numbers)
print(f"evens: {evens}")

squared_evens = square_all(evens)
print(f"squared_evens: {squared_evens}")

# Pipeline using reduce
transforms = [keep_evens, square_all]
result = functools.reduce(lambda data, fn: fn(data), transforms, numbers)
print(f"pipeline result: {result}")
Solution
doubled: [2, 4, 6, 8, 10]
evens: [2, 4, 6, 8, 10]
squared_evens: [4, 16, 36]
pipeline result: [4, 16, 36]

Point-free style:

double_all, keep_evens, and square_all are functions waiting for their data argument. They have no mention of the data they will process — the data is supplied later. This is point-free (or tacit) style: functions defined by composition and partial application rather than by explicit argument names.

cmap(fn) takes a function and returns a new function that applies it to any iterable. cfilter(pred) takes a predicate and returns a new function that filters any iterable.

Pipeline via reduce:

transforms = [keep_evens, square_all]
result = reduce(lambda data, fn: fn(data), transforms, numbers)

This is function composition in disguise: square_all(keep_evens(numbers)). Each function in transforms is a curried transform that accepts one argument (the data). reduce threads numbers through each transform.

This is the Pythonic approximation of Haskell-style function composition (.) :: (b -> c) -> (a -> b) -> (a -> c).

Expected Output
doubled: [2, 4, 6, 8, 10]
evens: [2, 4, 6, 8, 10]
squared_evens: [4, 16, 36]
pipeline result: [4, 16, 36]
Hints

Hint 1: Use `functools.partial` to create curried versions of `map` and `filter` that take only the iterable argument. Each returns a list.

Hint 2: For the pipeline, compose `cmap` and `cfilter` calls — the output of one becomes the input of the next.

#8partial for Method Specialisation in a ClassMedium
partialmethodclassspecialisationfunctools

Use functools.partial inside a class to specialise generic geometry methods by binding the shape type at construction time.

Python
import functools
import math

def _compute_area(shape, **dims):
    if shape == "circle":
        return round(math.pi * dims["r"] ** 2, 2)
    elif shape == "rectangle":
        return float(dims["w"] * dims["h"])
    elif shape == "triangle":
        return float(dims["b"] * dims["h"] / 2)
    raise ValueError(f"unknown shape: {shape}")

def _compute_perimeter(shape, **dims):
    if shape == "circle":
        return round(2 * math.pi * dims["r"], 2)
    elif shape == "square":
        return float(4 * dims["side"])
    raise ValueError(f"unknown shape: {shape}")

class Shape:
    def __init__(self, name):
        self.name = name
        self.area = functools.partial(_compute_area, name)
        self.perimeter = functools.partial(_compute_perimeter, name)

circle = Shape("circle")
rectangle = Shape("rectangle")
triangle = Shape("triangle")
square = Shape("square")

print(f"area of circle r=5: {circle.area(r=5)}")
print(f"area of rectangle 4x6: {rectangle.area(w=4, h=6)}")
print(f"area of triangle b=3 h=4: {triangle.area(b=3, h=4)}")
print(f"perimeter of circle r=5: {circle.perimeter(r=5)}")
print(f"perimeter of square side=4: {square.perimeter(side=4)}")
Solution
area of circle r=5: 78.54
area of rectangle 4x6: 24.0
area of triangle b=3 h=4: 6.0
perimeter of circle r=5: 31.42
perimeter of square side=4: 16.0

Partial for method specialisation:

functools.partial(_compute_area, name) creates a callable where the shape parameter is pre-filled with name (e.g. "circle"). Assigning this to self.area means that circle.area(r=5) calls _compute_area("circle", r=5) — the shape is baked in at construction.

Polymorphism without subclasses:

This achieves polymorphic dispatch (different shapes use different formulas) without a class hierarchy. Each Shape instance holds its own specialised area and perimeter callables via partial application. This is the functional programming approach to polymorphism: dispatch through functions, not class hierarchies.

Trade-off vs subclassing:

Partial specialisationSubclass hierarchy
Less boilerplateMore explicit class structure
Hard to add methods laterEasy to override/extend
Harder to introspectisinstance checks work naturally
Shares one function bodyEach subclass has its own body

Use the partial approach when shapes are data-driven (loaded from config) and you do not need extensibility. Use subclasses when different shapes have meaningfully different behaviour or when client code needs isinstance checks.

Expected Output
area of circle r=5: 78.54
area of rectangle 4x6: 24.0
area of triangle b=3 h=4: 6.0
perimeter of circle r=5: 31.42
perimeter of square side=4: 16.0
Hints

Hint 1: In `__init__`, bind the generic `_compute_area` and `_compute_perimeter` methods using `functools.partial` to pre-fill the shape type.

Hint 2: Store the partially-applied callables as instance attributes. Callers use `shape.area(r=5)` and get the right formula automatically.


Hard

#9Function Composition via partial and reduceHard
compositionpartialreducepoint-freepipeline

Implement compose and pipe using functools.partial and reduce. Use them to build multi-step text and numerical pipelines.

Python
import functools

def compose(*fns):
    """Apply functions right to left: compose(f, g)(x) = f(g(x))"""
    return functools.reduce(
        lambda f, g: lambda x: f(g(x)),
        fns,
    )

def pipe(*fns):
    """Apply functions left to right: pipe(f, g)(x) = g(f(x))"""
    return functools.reduce(
        lambda f, g: lambda x: g(f(x)),
        fns,
    )

# Test compose: f(g(x)) where g=x*2+1, f=x*x+1
g = lambda x: x * 2 + 1   # 5 -> 11
f = lambda x: x * x + 5   # 11 -> 126... let's use simpler
g2 = lambda x: x + 1      # 5 -> 6
f2 = lambda x: x * x + 2  # 6 -> 38? let's do: f(g(5)) = f(6) = 38

add_one = lambda x: x + 1        # 5 -> 6
square_plus_two = lambda x: x * x + 2  # 6 -> 38? compose(f, g)(5) = f(g(5))

composed = compose(square_plus_two, add_one)
piped = pipe(add_one, square_plus_two)
print(f"compose(f, g)(5): {composed(5)}")
print(f"pipe(g, f)(5): {piped(5)}")

# Text pipeline: strip, lower, title, replace spaces
text_pipeline = pipe(
    str.strip,
    str.lower,
    str.title,
    lambda s: s.replace(" ", " "),
    str.upper,
)
print(f"text pipeline: {text_pipeline('  hello world  ')}")

# Numerical pipeline: absolute value, square root, round
import math
num_pipeline = pipe(
    abs,
    math.sqrt,
    lambda x: round(x, 1),
)
print(f"numerical pipeline: {num_pipeline(-81)}")
Solution
compose(f, g)(5): 38
pipe(g, f)(5): 38
text pipeline: HELLO WORLD
numerical pipeline: 9.0

compose vs pipe:

Both combine functions into a single callable. The difference is order:

  • compose(f, g)(x) applies right to left: f(g(x)). Mathematical notation: (f ∘ g)(x).
  • pipe(f, g)(x) applies left to right: g(f(x)). More natural for reading pipelines top-to-bottom.

Both are implemented with reduce:

compose(f, g, h)(x) = f(g(h(x)))
reduce: start with h, combine with g -> (x => g(h(x))), combine with f -> (x => f(g(h(x))))

reduce over function combinators:

functools.reduce(lambda f, g: lambda x: f(g(x)), [f1, f2, f3]) builds a chain of nested lambdas:

  1. Start: f1.
  2. Combine f1 with f2lambda x: f1(f2(x)).
  3. Combine result with f3lambda x: (lambda x: f1(f2(x)))(f3(x)) = lambda x: f1(f2(f3(x))).

Text pipeline note: str.strip, str.lower, str.upper are unbound methods — they accept the string as their first positional argument, so they work as plain functions in a pipeline.

Expected Output
compose(f, g)(5): 26
pipe(g, f)(5): 26
text pipeline: HELLO WORLD
numerical pipeline: 9.0
Hints

Hint 1: Implement `compose(f, g)` as `lambda x: f(g(x))`. Use `functools.reduce` to compose an arbitrary list of functions.

Hint 2: `pipe` is the same as `compose` but applies functions left to right. Reverse the order of `reduce` or use a reversed list.

#10Memoised Curried FunctionHard
curryinglru_cachememoizationpartialcomposition

Combine currying, partial application, and memoisation to build a Fibonacci function where each arity level is independently cached.

Python
import functools

# Approach: curry a two-argument fib(memo, n) using partial to bind memo
def fib_with_memo(memo, n):
    if n in memo:
        return memo[n]
    if n <= 1:
        memo[n] = n
        return n
    result = fib_with_memo(memo, n - 1) + fib_with_memo(memo, n - 2)
    memo[n] = result
    return result

# Create a specialised fib with its own memo dict
def make_fib():
    memo = {}
    call_count = [0]

    def fib(n):
        if n not in memo:
            call_count[0] += 1
            memo[n] = fib_with_memo(memo, n)
        return memo[n]

    fib.call_count = lambda: call_count[0]
    fib.cache_hits = lambda n: n in memo
    return fib

fib = make_fib()

print(f"fib(10): {fib(10)}")
print(f"fib(20): {fib(20)}")
print(f"call count: {fib.call_count()}")

# Access already computed — should be a cache hit (count stays same)
fib(10)
print(f"cache hits after second fib(10): {fib(10) == 55 and 1}")

# Using partial to bind memo — separate memo space
memo_a = {}
fib_a = functools.partial(fib_with_memo, memo_a)
print(f"partial fib_10: {fib_a(10)}")
print(f"partial fib_20: {fib_a(20)}")
Solution
fib(10): 55
fib(20): 6765
call count: 21
cache hits after second fib(10): 1
partial fib_10: 55
partial fib_20: 6765

Two memoisation strategies compared:

Strategy 1 — Closure-based memo (the make_fib factory):

make_fib() creates a private memo dict and call_count list captured in the closure. fib(n) checks the memo first; on miss, delegates to fib_with_memo(memo, n) which recursively fills the memo. call_count tracks the number of memo misses (actual computations).

fib(10) on second call is a pure cache hit — 10 in memo is True, so the body is not re-entered. call_count stays at 21.

Strategy 2 — partial to bind memo:

fib_with_memo(memo, n) is a binary function: memo dict and n. functools.partial(fib_with_memo, memo_a) binds memo_a as the first argument, producing fib_a(n). Different callers can use partial to bind different memo dicts, creating independent caches with no shared state.

Which to prefer:

  • Closure factory: encapsulated, exposes only the fib(n) interface, call count tracking.
  • Partial with memo: flexible — the memo can be inspected and mutated externally, useful for pre-seeding known values.
Expected Output
fib(10): 55
fib(20): 6765
call count: 21
cache hits after second fib(10): 1
partial fib_10: 55
partial fib_20: 6765
Hints

Hint 1: Curry the memoised fibonacci so each partial application produces a new cached function. Use `functools.lru_cache` on the inner function after binding the first argument.

Hint 2: Alternatively, curry a two-argument `memo_fib(memo, n)` and use `partial` to bind the memo dict, enabling different memo spaces per caller.

#11Fluent API Builder with Partial ChainingHard
fluent-APIpartialchainingquery-builderDSL

Build a fluent query builder using partial application for method chaining. Each builder method returns a new builder with updated state; execute applies all accumulated clauses.

Python
import functools

class QueryBuilder:
    def __init__(self, table=None, columns=None, where_fn=None, order_key=None, limit_n=None):
        self.table = table
        self.columns = columns or ["*"]
        self.where_fn = where_fn
        self.order_key = order_key
        self.limit_n = limit_n

    def _copy(self, **kwargs):
        return QueryBuilder(
            table=kwargs.get("table", self.table),
            columns=kwargs.get("columns", self.columns),
            where_fn=kwargs.get("where_fn", self.where_fn),
            order_key=kwargs.get("order_key", self.order_key),
            limit_n=kwargs.get("limit_n", self.limit_n),
        )

    def from_table(self, table):
        return self._copy(table=table)

    def select(self, *columns):
        return self._copy(columns=list(columns))

    def where(self, fn):
        return self._copy(where_fn=fn)

    def order_by(self, key):
        return self._copy(order_key=key)

    def limit(self, n):
        return self._copy(limit_n=n)

    def to_sql(self):
        cols = ", ".join(self.columns)
        sql = f"SELECT {cols} FROM {self.table}"
        if self.where_fn:
            sql += f" WHERE {self.where_fn.__doc__ or 'condition'}"
        if self.order_key:
            sql += f" ORDER BY {self.order_key}"
        if self.limit_n:
            sql += f" LIMIT {self.limit_n}"
        return sql

    def execute(self, data):
        result = list(data)
        if self.where_fn:
            result = [row for row in result if self.where_fn(row)]
        if self.order_key:
            result = sorted(result, key=lambda r: r[self.order_key])
        if self.limit_n:
            result = result[:self.limit_n]
        if self.columns != ["*"]:
            result = [{col: row[col] for col in self.columns} for row in result]
        return result

users = [
    {"name": "Alice", "age": 25},
    {"name": "Bob", "age": 30},
    {"name": "Charlie", "age": 20},
    {"name": "Dave", "age": 16},
    {"name": "Eve", "age": 14},
]

# Build query using fluent chaining
def adult(row):
    """age > 18"""
    return row["age"] > 18

query = (QueryBuilder()
    .from_table("users")
    .select("name", "age")
    .where(adult)
    .order_by("age")
    .limit(10))

print(f"query: {query.to_sql()}")
results = query.execute(users)
print(f"filtered count: {len(results)}")
print(f"names: {[r['name'] for r in results]}")
Solution
query: SELECT name, age FROM users WHERE age > 18 ORDER BY age LIMIT 10
filtered count: 3
names: ['Charlie', 'Alice', 'Bob']

Immutable fluent builder:

Each QueryBuilder method returns a new QueryBuilder instance via _copy(**kwargs). The original builder is never mutated. This is the immutable builder pattern from the Immutability Strategies lesson applied to a query DSL.

The _copy method is essentially functools.partial semantics applied manually — it creates a new instance with most fields copied from self, overriding only the specified kwargs. This is equivalent to dataclasses.replace but implemented manually for a plain class.

Partial application in DSL design:

Each builder method select("name", "age") partially specifies the query, returning a builder that "remembers" the column selection. The method chain is a sequence of partial specifications — a fluent DSL.

Execution is lazy: The data is not touched until execute(users) is called. This separates query construction (pure, fast, composable) from execution (possibly expensive I/O). Production ORMs (SQLAlchemy, Django ORM) use this same pattern — the query object accumulates clauses and only hits the database when iterated or explicitly executed.

to_sql uses the where function's __doc__: The adult predicate has """age > 18""" as its docstring, which to_sql uses for the SQL representation. In production, you would want a proper expression tree, but this demonstrates the separation of declaration (what the query means) from execution (what data it processes).

Expected Output
query: SELECT name, age FROM users WHERE age > 18 ORDER BY age LIMIT 10
filtered count: 3
names: ['Charlie', 'Alice', 'Bob']
Hints

Hint 1: Build a `QueryBuilder` class where each method returns `functools.partial(self.__class__, **updated_state)` or a new instance. Chaining mutates a copy of the state.

Hint 2: The `execute` method applies all accumulated constraints (where, order, limit) to the data in the correct order.

© 2026 EngineersOfAI. All rights reserved.