Typorithm · medium

Closures, late binding and deliberate capture

pythonlibrariespackagingindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Closures, late binding and deliberate capture

0%

Concept and contract

Build independent callbacks without accidentally reading the last loop value.

A nested function can read names from an enclosing scope. That does not mean every function created in a loop receives a separate snapshot of each changing name. A reference to offset is resolved when the function runs unless another binding deliberately captures a value.

The worked example uses a default parameter to retain the current numeric offset during function creation. These offsets are immutable integers, so later loop assignments do not alter them. This technique needs more care with mutable defaults: retaining a shared list would preserve an object reference rather than an independent deep copy.

Mechanism and worked example

For offsets one, three and five, the loop creates three callable objects.

For offsets one, three and five, the loop creates three callable objects. Each shifted function receives its own default value and the functions are invoked only after construction finishes. Calling them with ten returns eleven, thirteen and fifteen in the original order.

The function list preserves order, while the default expression supplies the captured value. These are different responsibilities. An empty input creates no callbacks and returns an empty result. The caller's offset list is read without mutation, so it can be reused by another report or another test.

Complete worked example

python

def inspect_example(offsets, value):
    functions = []
    for offset in offsets:
        def shifted(number, offset=offset):
            return number + offset
        functions.append(shifted)
    return [function(value) for function in functions]
Reasoning
For offsets one, three and five, the loop creates three callable objects. Each shifted function receives its own default value and the functions are invoked only after construction finishes. Calling them with ten returns eleven, thirteen and fifteen in the original order.
Download the reusable Python and packaging computer lab ↗

Counterexamples and common mistakes

The controlled counterexample removes the default capture and leaves offset as a free variable.

The controlled counterexample removes the default capture and leaves offset as a free variable. After the loop ends, every callback reads the final offset. The first acceptance case therefore observes fifteen three times rather than three distinct results.

This is a behavioral defect, not a syntax error and not a special property of lambda expressions. Replacing def with lambda would preserve the same late-binding issue. Calling callbacks immediately during construction might hide the mistake, so the verifier waits until the entire list has been created before checking the results.

Controlled bug: the executable contract rejects this version

python

def inspect_example(offsets, value):
    functions = []
    for offset in offsets:
        def shifted(number):
            return number + offset
        functions.append(shifted)
    return [function(value) for function in functions]
Reasoning
The controlled counterexample removes the default capture and leaves offset as a free variable. After the loop ends, every callback reads the final offset. The first acceptance case therefore observes fifteen three times rather than three distinct results.
Download the reusable Python and packaging computer lab ↗

Independent practice

Change each callback from adding its retained offset to multiplying by it.

Change each callback from adding its retained offset to multiplying by it. Keep the binding decision intact and preserve the output order. Start with the positive offsets, then use zero and a negative offset to distinguish multiplication from a superficial constant adjustment.

The existing named parameter provides the first hint: the arithmetic body is the part that changes. The second hint is to compare a callback's retained default with the enclosing loop variable. Do not replace the callbacks with precomputed answers; the exercise should still demonstrate independently constructed callable objects.

Step 1: Bind each loop value when defining the function, then change the practice transformation to multiplication. Step 2: Run the accepted baseline, edit the exercise, then check its independent solution cases; preserve caller inputs and the documented exception behavior.

Practice starter: solve before reading the solution

python

def inspect_example(offsets, value):
    functions = []
    for offset in offsets:
        def shifted(number, offset=offset):
            return number + offset
        functions.append(shifted)
    return [function(value) for function in functions]

# Task: Bind each loop value when defining the function, then change the practice transformation to multiplication.
# Practice contract: Bind each loop value when defining the function, then change the practice transformation to multiplication.
Reasoning
Bind each loop value when defining the function, then change the practice transformation to multiplication.
Download the reusable Python and packaging computer lab ↗
  • Hint 1: Change each callback from adding its retained offset to multiplying by it. Keep the binding decision intact and preserve the output order. Start with the positive offsets, then use zero and a negative offset to distinguish multiplication from a superficial constant adjustment.
  • Hint 2: The existing named parameter provides the first hint: the arithmetic body is the part that changes. The second hint is to compare a callback's retained default with the enclosing loop variable. Do not replace the callbacks with precomputed answers; the exercise should still demonstrate independently constructed callable objects.

Solution, tests and trade-offs

The accepted solution multiplies number by the bound default.

The accepted solution multiplies number by the bound default. For ten and offsets one, three and five, the resulting list is ten, thirty and fifty. Empty input remains empty, and the mixed zero and negative case proves the new transformation uses each offset.

Default-argument capture is compact, but a factory function can communicate intent more clearly when a callback needs several independent values. A callable dataclass is another option when the state needs an explicit name. Choose a representation whose ownership and mutation rules a reviewer can explain without relying on timing assumptions.

Practice solution and acceptance checks

python

def inspect_example(offsets, value):
    functions = []
    for offset in offsets:
        def shifted(number, offset=offset):
            return number * offset
        functions.append(shifted)
    return [function(value) for function in functions]
Reasoning
The accepted solution multiplies number by the bound default. For ten and offsets one, three and five, the resulting list is ten, thirty and fifty. Empty input remains empty, and the mixed zero and negative case proves the new transformation uses each offset.
Download the reusable Python and packaging computer lab ↗

Interview reasoning and next step

Describe the difference between retaining a value through a default and reading a variable through a closure cell.

Describe the difference between retaining a value through a default and reading a variable through a closure cell. Then explain why the timing of callback invocation makes the counterexample visible. A correct answer should account for all three callbacks, not only the last one.

As a stretch, replace default capture with a small factory that returns a nested function and run the same cases unchanged. Add a separate mutable-state experiment if useful, but state whether callbacks should share that object. Copying data is a domain decision; closure syntax alone cannot decide the intended ownership.

Optional stretch — after the core task works: As a stretch, replace default capture with a small factory that returns a nested function and run the same cases unchanged. Add a separate mutable-state experiment if useful, but state whether callbacks should share that object. Copying data is a domain decision; closure syntax alone cannot decide the intended ownership.

Computer practice: https://applydjinn.com/typorithm/python-engineering-lab-20261107.zip. Use an isolated Python 3.13 environment, install requirements.txt, then run verify.py. Edit exercises/pye-closures.py and run --practice pye-closures. Fixture files are created in fresh temporary directories for every case. Packaging checks build real local artifacts, install the exact wheel without an index and run outside the source tree. No package is published to PyPI. The app includes static examples, prepared results and questions; it does not run Python or provide an editor.

Build independent callbacks without accidentally reading the last loop value.