# Execution Timer Wrapper

> Function wrapped with a timer. Duration captured on exit.

Canonical URL: <https://datadriven.io/problems/execution_timer_wrapper>

Domain: Python · Difficulty: medium · Seniority: L6 · Asked in: Agoda

## Problem

The jobs in `JOBS` are plain functions of different arity, each wrapped at definition by the `make_timed` decorator so its run time gets logged. Implement `make_timed` and `run_timed`, which runs the named `job` with the positional `args` it is given and hands back exactly what the job itself returned: the measured seconds are logged, never returned.

## Example 1

Input:

```
job = "answer"
```

Output:

```
42
```

## Example 2

Input:

```
job = "add", args = [3,4]
```

Output:

```
7
```

## Worked solution and explanation

### What this is really asking

This is a transparency test wearing a profiler costume. The skill being probed: can you write a wrapper that sits between every caller and every job, serves any arity through `*args`, and leaves **no fingerprint on the return value**? Anyone can call `time.perf_counter()` twice. What separates candidates is where the elapsed seconds go. Fold them into the return as `(result, elapsed)` and `42` comes back as `(42, 0.0001)`: every caller of every job in `JOBS` breaks at once.

Notice the blast radius. `make_timed` is applied at definition time with `@make_timed`, so the name `add` no longer points at the function you read; it points at whatever `make_timed` returned. A wrapper that leaks timing, or a `make_timed` that forgets `return wrapper`, poisons the whole registry before `run_timed` ever runs.

---

### Building it

#### Step 1: Return a replacement, not a result

`make_timed(func)` defines an inner `wrapper` that closes over `func` and returns that wrapper. The decorator never calls the job itself; it builds a replacement that will.

#### Step 2: Forward whatever arrives

`answer` takes nothing, `double` takes one value, `add` takes two. One wrapper serves all of them only if it accepts `*args, **kwargs` and passes them straight through to `func`.

#### Step 3: Clock around the call, return the job's value

Read the clock, call `func`, read the clock again, and log the difference. Then `return result`, the exact object the job produced. The elapsed time is a side effect, never part of the contract.

#### Step 4: Dispatch by name in `run_timed`

`run_timed` treats a missing `args` as an empty list, looks the timed job up in `JOBS` by name, and spreads the arguments with `*args`. Calling `timed_job(args)` instead would hand `add` a single list and blow up on arity.

**A timer the callers never see**

```python
import time


def make_timed(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.6f}s")
        return result
    return wrapper


@make_timed
def answer():
    return 42


@make_timed
def double(n):
    return n * 2


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


@make_timed
def greet(greeting, name):
    return greeting + name


@make_timed
def pair(a, b):
    return [a, b]


JOBS = {
    "answer": answer,
    "double": double,
    "add": add,
    "greet": greet,
    "pair": pair,
}


def run_timed(job, args=None):
    if args is None:
        args = []
    timed_job = JOBS[job]
    return timed_job(*args)
```

**Leaky wrapper**

`return result, elapsed` changes every job's return type. `add(3, 4)` now yields `(7, 0.00001)` and every equality check downstream fails.

**Transparent wrapper**

`print(...)` then `return result`. `add(3, 4)` still yields `7`; the timing lives in the log, where a profiler belongs.

> **The missing return erases the registry**
>
> Drop `return wrapper` and `make_timed` returns `None`. Because the decorator runs at import, `JOBS` fills up with `None` values and the first call raises `TypeError: 'NoneType' object is not callable`, far from the actual bug.

> **Pick the clock that cannot go backwards**
>
> Reaching for `time.perf_counter()` over `time.time()` without being asked. Wall-clock time can jump when the system clock is adjusted; a monotonic counter cannot, which is what you want for measuring durations.

> **Overhead is two clock reads**
>
> The wrapper adds two clock reads and one print per call: constant overhead, independent of the job. The cost of the run is entirely the job's own.

## Common follow-up questions

- After decorating, `add.__name__` reads 'wrapper'. How do you keep the original name and docstring? _(Tests `functools.wraps` on the inner function.)_
- What if a job raises? Should the time still be logged? _(Tests wrapping the call in `try` / `finally`.)_
- How would you turn `make_timed` into `make_timed(threshold)` that only logs slow runs? _(Tests a decorator factory: one more level of closure.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/execution_timer_wrapper)
- [Python Interview Questions](https://datadriven.io/python-interview-questions)
- [Data Engineering Interview Prep Guide](https://datadriven.io/data-engineer-interview-prep)
- [Daily Challenge](https://datadriven.io/daily)

---

Source: DataDriven (https://datadriven.io). DataDriven is the data engineering interview community. Live code execution in SQL, Python, and Spark sandboxes. Every feature is open to every member.