Execution Timer Wrapper

A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.

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.

Function wrapped with a timer. Duration captured on exit.

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
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.

Common follow-up questions