Typorithm · medium

Decorators that preserve a callable’s interface

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

Step 1 of 4 · intro card

Decorators that preserve a callable’s interface

0%

Concept and contract

Forward arguments correctly while retaining metadata and access to the wrapped function.

A decorator receives a callable and returns the callable that the decorated name will reference. A forwarding wrapper can preserve behavior while still changing visible metadata. These are separate contracts, and frameworks or developer tools may depend on both.

The example uses wraps to copy common metadata and expose the original through __wrapped__. It also forwards both positional and keyword arguments. This lesson does not claim that wraps enforces types or validates arguments: the original scale function still owns those decisions when the wrapper calls it with the received arguments.

Mechanism and worked example

Start at the decorated scale definition, then follow one request through wrapper into the original function.

Start at the decorated scale definition, then follow one request through wrapper into the original function. Three with factor four returns twelve. The wrapper accepts arbitrary argument shapes so it can forward them, while inspect.signature follows the wrapped relationship to describe the original public parameters.

The returned report checks the product, the name and the parameter order. These observations make a transparent wrapper testable rather than merely plausible. Reading signature metadata is an inspection feature; calling the function remains the behavioral check. The two should agree about the intended public interface.

Complete worked example

python

from functools import wraps
import inspect

def traced(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        return function(*args, **kwargs)
    return wrapper

@traced
def scale(value, *, factor=2):
    """Scale one value."""
    return value * factor

def inspect_example(value, factor):
    return {'value': scale(value, factor=factor), 'name': scale.__name__,
            'parameters': list(inspect.signature(scale).parameters)}
Reasoning
Start at the decorated scale definition, then follow one request through wrapper into the original function. Three with factor four returns twelve. The wrapper accepts arbitrary argument shapes so it can forward them, while inspect.signature follows the wrapped relationship to describe the original public parameters.
Download the reusable Python and packaging computer lab ↗

Counterexamples and common mistakes

The controlled bug removes wraps but leaves forwarding in place.

The controlled bug removes wraps but leaves forwarding in place. Products remain correct, yet the callable is now named wrapper and its inspected parameters no longer describe the original value and factor pair. An arithmetic-only test would miss the regression.

Another common mistake forwards only positional arguments and silently drops keyword options. That would break the explicit factor call even if a default-only example appeared to work. The supplied cases retain a nondefault keyword, zero and a negative value so the example checks the wrapper without relying on a single convenient product.

Controlled bug: the executable contract rejects this version

python

from functools import wraps
import inspect

def traced(function):
    def wrapper(*args, **kwargs):
        return function(*args, **kwargs)
    return wrapper

@traced
def scale(value, *, factor=2):
    """Scale one value."""
    return value * factor

def inspect_example(value, factor):
    return {'value': scale(value, factor=factor), 'name': scale.__name__,
            'parameters': list(inspect.signature(scale).parameters)}
Reasoning
The controlled bug removes wraps but leaves forwarding in place. Products remain correct, yet the callable is now named wrapper and its inspected parameters no longer describe the original value and factor pair. An arithmetic-only test would miss the regression.
Download the reusable Python and packaging computer lab ↗

Independent practice

Extend the report with the original function's name obtained through __wrapped__.

Extend the report with the original function's name obtained through __wrapped__. Preserve all existing fields and the forwarding behavior. The first hint is that wraps sets the relationship used by tools; the second is to inspect that relationship rather than guessing a name from a string constant.

Keep the accepted wrapper and add one observation to the report. The test cases expect the original name for positive, zero and negative products. Avoid returning wrapper's own name as a substitute: the exercise specifically asks you to navigate to the wrapped callable and explain why that reference exists.

Step 1: Preserve the wrapped callable and add its original name to the practice report. 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

from functools import wraps
import inspect

def traced(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        return function(*args, **kwargs)
    return wrapper

@traced
def scale(value, *, factor=2):
    """Scale one value."""
    return value * factor

def inspect_example(value, factor):
    return {'value': scale(value, factor=factor), 'name': scale.__name__,
            'parameters': list(inspect.signature(scale).parameters)}

# Task: Preserve the wrapped callable and add its original name to the practice report.
# Practice contract: Preserve the wrapped callable and add its original name to the practice report.
Reasoning
Preserve the wrapped callable and add its original name to the practice report.
Download the reusable Python and packaging computer lab ↗
  • Hint 1: Extend the report with the original function's name obtained through __wrapped__. Preserve all existing fields and the forwarding behavior. The first hint is that wraps sets the relationship used by tools; the second is to inspect that relationship rather than guessing a name from a string constant.
  • Hint 2: Keep the accepted wrapper and add one observation to the report. The test cases expect the original name for positive, zero and negative products. Avoid returning wrapper's own name as a substitute: the exercise specifically asks you to navigate to the wrapped callable and explain why that reference exists.

Solution, tests and trade-offs

The solution accesses scale.__wrapped__.__name__ after wraps has established the reference.

The solution accesses scale.__wrapped__.__name__ after wraps has established the reference. It still reports scale for the decorated name and retains the original parameter order. Acceptance therefore covers computation, common metadata and the explicit link to the original callable.

A decorator that changes semantics may need a new signature or more explicit documentation instead of pretending to be transparent. Also distinguish decorating a synchronous function from decorating a coroutine: a synchronous forwarding wrapper does not automatically become an async function for inspection or scheduling. Design the wrapper for the actual callable contract.

Practice solution and acceptance checks

python

from functools import wraps
import inspect

def traced(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        return function(*args, **kwargs)
    return wrapper

@traced
def scale(value, *, factor=2):
    """Scale one value."""
    return value * factor

def inspect_example(value, factor):
    return {'value': scale(value, factor=factor), 'name': scale.__name__, 'original': scale.__wrapped__.__name__,
            'parameters': list(inspect.signature(scale).parameters)}
Reasoning
The solution accesses scale.__wrapped__.__name__ after wraps has established the reference. It still reports scale for the decorated name and retains the original parameter order. Acceptance therefore covers computation, common metadata and the explicit link to the original callable.
Download the reusable Python and packaging computer lab ↗

Interview reasoning and next step

Explain why correct products are insufficient evidence of a transparent decorator.

Explain why correct products are insufficient evidence of a transparent decorator. Mention the forwarded keyword, visible name, inspected signature and original callable reference. Then explain which of those observations wraps supplies and which the wrapper body must implement itself.

For a stretch, stack two simple decorators and inspect the __wrapped__ chain one level at a time. Add counters to owned local functions to observe call order. Avoid introducing network logging or timing claims: deterministic return values and explicit events make decorator ordering easier to verify and discuss.

Optional stretch — after the core task works: For a stretch, stack two simple decorators and inspect the __wrapped__ chain one level at a time. Add counters to owned local functions to observe call order. Avoid introducing network logging or timing claims: deterministic return values and explicit events make decorator ordering easier to verify and discuss.

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-decorators.py and run --practice pye-decorators. 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.

Forward arguments correctly while retaining metadata and access to the wrapped function.