Challenge 1: A Timing Decorator — Possible Solution ==================================================================== import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} took {end - start:.4f} seconds") return result return wrapper @timer def slow_add(a, b): time.sleep(1) return a + b total = slow_add(3, 4) print(total) Output (timing will vary slightly, but always ~1 second): slow_add took 1.0002 seconds 7 WHY THIS WORKS AS AN ANSWER ------------------------------ def timer(func): / def wrapper(*args, **kwargs): reuses this chapter's own decorator skeleton exactly — a decorator function that takes the original func and returns a new wrapper function, using *args, **kwargs so it works regardless of what arguments the decorated function actually takes, exactly the requirement called out in the chapter's warn-box. @wraps(func) reuses the chapter's own functools.wraps fix, so slow_add.__name__ still correctly reports "slow_add" rather than "wrapper" — which matters here specifically, since the timing message itself prints func.__name__ to identify which function was timed. start = time.time() and end = time.time() bracket the actual call to func(*args, **kwargs), measuring elapsed time around it. Crucially, result = func(*args, **kwargs) captures the original function's return value BEFORE computing end, and wrapper still returns result at the end — meaning the decorator adds timing output as a side effect without changing what the decorated function actually returns to its caller, exactly what the challenge asks for. @timer applied to slow_add is sugar for slow_add = timer(slow_add), the same substitution mechanism the chapter's @shout example demonstrated — calling slow_add(3, 4) now actually runs wrapper(3, 4), which times the real slow_add(3, 4) call inside it and returns 7 unchanged.