Skip to content

Exercises - closures

Exercise 1🟢 Beginner Create a closure that generates a greeting function for a specific language.

# expected:
# greet_en = make_greeter("Hello")
# greet_es = make_greeter("Hola")
# greet_fr = make_greeter("Bonjour")
# greet_en("Alice") → "Hello, Alice!"
# greet_es("Alice") → "Hola, Alice!"
# greet_fr("Alice") → "Bonjour, Alice!"

Exercise 2🟢 Beginner Create a closure that keeps track of how many times a function has been called.

# expected:
# counter = make_call_counter()
# counter() → "Called 1 time"
# counter() → "Called 2 times"
# counter() → "Called 3 times"

Exercise 3🟢 Beginner Create a closure that applies a fixed discount to any price.

# expected:
# ten_percent_off = make_discount(10)
# twenty_percent_off = make_discount(20)
# ten_percent_off(100) → 90.0
# twenty_percent_off(100) → 80.0
# ten_percent_off(50) → 45.0

Exercise 4🟡 Intermediate Create a closure that generates a multiplier function for a given factor, then use it with map() to transform a list.

numbers = [1, 2, 3, 4, 5]
# expected:
# double = make_multiplier(2)
# triple = make_multiplier(3)
# double(5) → 10
# list(map(double, numbers)) → [2, 4, 6, 8, 10]
# list(map(triple, numbers)) → [3, 6, 9, 12, 15]

Exercise 5🟡 Intermediate Create a closure that generates a power function for a given exponent, then use it with map() and filter().

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# expected:
# square = make_power(2)
# cube = make_power(3)
# square(4) → 16
# list(map(square, numbers)) → [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# list(map(cube, filter(lambda x: x % 2 == 0, numbers))) → [8, 64, 216, 512, 1000]

Exercise 6🟡 Intermediate Create a closure that builds a logger with a fixed prefix and log level.

# expected:
# db_info = make_logger("DATABASE", "INFO")
# api_error = make_logger("API", "ERROR")
# db_info("Connection established") → "[INFO] [DATABASE] Connection established"
# api_error("Timeout after 30s") → "[ERROR] [API] Timeout after 30s"

Exercise 7🟡 Intermediate Create a closure that accumulates values and returns the running total each time it is called.

# expected:
# accumulator = make_accumulator()
# accumulator(10) → 10
# accumulator(20) → 30
# accumulator(5) → 35
# accumulator(100) → 135

Exercise 8🟡 Intermediate Create a closure that implements a simple stack with push and pop operations, returning both functions.

# expected:
# push, pop = make_stack()
# push(1)
# push(2)
# push(3)
# pop() → 3
# pop() → 2
# pop() → 1
# pop() → None ← empty stack

Exercise 9🔴 Advanced Create a closure that implements a rate limiter — it allows a function to be called at most n times, then raises an error.

# expected:
# limited = make_rate_limiter(3)
# limited() → "Call 1 of 3"
# limited() → "Call 2 of 3"
# limited() → "Call 3 of 3"
# limited() → ❌ RuntimeError: Rate limit exceeded — max 3 calls allowed

Exercise 10🟢 Beginner Fix this broken code so each function returns its own index value:

# ❌ broken
funcs = [lambda: i for i in range(5)]
print([f() for f in funcs]) # [4, 4, 4, 4, 4]
# ✅ fix it so the output is:
# [0, 1, 2, 3, 4]

Exercise 11🟡 Intermediate Fix this broken code that tries to create a list of multiplier functions:

# ❌ broken
multipliers = [lambda x: x * i for i in range(1, 6)]
print([f(10) for f in multipliers]) # [50, 50, 50, 50, 50]
# ✅ fix it so the output is:
# [10, 20, 30, 40, 50]

Exercise 12🔴 Advanced Create a closure that memoizes a function manually — without using functools.lru_cache. It should cache results and return them on repeated calls.

# expected:
# memoized_square = memoize(lambda x: x ** 2)
# memoized_square(4) → 16 (computed)
# memoized_square(4) → 16 (from cache)
# memoized_square(5) → 25 (computed)
# memoized_square(5) → 25 (from cache)

Exercise 13🔴 Advanced Create a closure that composes two functions together — the output of the first becomes the input of the second. Then use it to build a text processing pipeline.

# expected:
# strip_and_upper = compose(str.upper, str.strip)
# strip_and_upper(" hello ") → "HELLO"
# Build a pipeline:
# clean = compose(str.strip, str.lower)
# clean(" HELLO WORLD ") → "hello world"

Try implementing each solution as a pure closure first — no classes, no functools. The goal is to understand how captured state and returned functions can replace objects entirely.