Skip to content

← Under the Hood: Objects, Memory, Speed step 29 of 35

Medium Framework

hot_functions: the column you sort by is the answer you get

Profile a workload with three plausible culprits and rank them — once by tottime and once by cumtime — so the difference between the two answers is visible rather than theoretical.

def hot_functions(fn: Callable[[], None], *, top: int, sort: str) -> list[str]: ...

Run fn under a profiler, keep only the functions whose name starts with _step_, sort descending by inlinetime when sort == "tottime" and by totaltime otherwise, break ties by name ascending, and return the top top names.

The provided driver calls four things:

function shape
_step_tiny trivial body, called 500,000 times
_step_join one call, a genuinely expensive nested loop
_step_rare one call, moderately expensive
_step_wrapper one call, does nothing but delegate to _step_join

The two rankings disagree, and that is the entire lesson. By tottime, _step_wrapper is last, where it belongs — it does no work. By cumtime it ranks second, above the function that is actually slow, because a wrapper’s cumulative time is its callee’s cumulative time. Sorting by cumulative and reading the top rows is the standard way to spend an afternoon optimising a decorator.

Two implementation notes.

Use profiler.getstats(), not pstats.Stats(...).stats. The latter is a raw dict of tuples whose layout is folklore (index 2 is tottime, index 3 is cumtime) and it is not in typeshed, so mypy --strict rejects it. getstats() returns entries with named, typed fields: callcount, inlinetime (= tottime), totaltime (= cumtime), and code, which is a CodeType for Python functions and a str for built-ins — so narrow it with isinstance before reading co_name.

Import version-tolerantly. PEP 799 moves the deterministic profiler to profiling.tracing in 3.15, keeping cProfile as a permanent alias. A sys.version_info gate is the right shape, because mypy prunes the branch that does not apply to your configured python_version — a try/except ImportError is opaque to the checker and it will complain about the module it cannot find.

And the caveat worth carrying out of this exercise: the docs say the profilers are “not for benchmarking purposes”, and that they instrument Python but not C, so C code looks faster than it is. A profile tells you the shape of where time goes; timeit tells you whether your fix helped.

Your submission must pass mypy --strict.

Loading visualization…