Performance Tuning

Updated

September 8, 2026

Performance Tuning

Measure first. The boring tools are timeit for a small expression and cProfile for “where did the time go.” Do not rewrite a ticket loop because it feels slow.

Mental model

CPU time and wall time are different. timeit repeats a snippet to damp noise. cProfile records function calls in one run. Both answer questions. Neither is a reason to add a cache on day one.

The desk is usually waiting on disk, the network, or a human. Faster JSON parsing will not fix that. Profile, then change the hot function, then measure again.

Timings themselves are not stable across machines. The listings below print results and the fact that a profiler saw the function — not a number of seconds you would treat as a fixture.

Worked examples

Case 1: Get the answer right, then time it

Save as count_open.py. timeit.timeit runs count_open 1000 times. We print the count, then a marker that the timer finished.

# count_open.py
import timeit


def count_open() -> int:
    tickets = [{"status": "open"}] * 100 + [{"status": "paid"}] * 50
    return sum(1 for t in tickets if t["status"] == "open")


def main() -> None:
    print(count_open())
    timeit.timeit(count_open, number=1000)
    print("measured")


if __name__ == "__main__":
    main()

Run:

uv run python count_open.py

Output:

100
measured

To see seconds on your machine, print the return value of timeit.timeit. Do not commit that float as a test.

Case 2: cProfile names the function

Save as profile_open.py. runcall profiles one invocation. We only assert the profiler recorded count_open.

# profile_open.py
import cProfile


def count_open() -> int:
    tickets = [{"status": "open"}] * 100 + [{"status": "paid"}] * 50
    return sum(1 for t in tickets if t["status"] == "open")


def main() -> None:
    profiler = cProfile.Profile()
    n = profiler.runcall(count_open)
    names = [
        s.code.co_name
        for s in profiler.getstats()
        if hasattr(s.code, "co_name")
    ]
    print(n)
    print("count_open" in names)


if __name__ == "__main__":
    main()

Run:

uv run python profile_open.py

Output:

100
True

From a terminal, the human-readable table is:

uv run python -m cProfile -s tottime count_open.py

Sort by tottime. Read the top rows. Ignore the rest until they matter.

Case 3: Two implementations, same answer

Save as scan_vs_sum.py. Correctness first. Timing second.

# scan_vs_sum.py
def count_loop(tickets: list[dict[str, str]]) -> int:
    n = 0
    for t in tickets:
        if t["status"] == "open":
            n += 1
    return n


def count_sum(tickets: list[dict[str, str]]) -> int:
    return sum(1 for t in tickets if t["status"] == "open")


def main() -> None:
    tickets = [{"status": "open"}] * 100 + [{"status": "paid"}] * 50
    a = count_loop(tickets)
    b = count_sum(tickets)
    print(a)
    print(a == b)


if __name__ == "__main__":
    main()

Run:

uv run python scan_vs_sum.py

Output:

100
True

If both are correct and neither shows up in a profile, keep the one that reads better (count_sum here).

The trap

Micro-optimizing a string that is not the bottleneck.

Save as clever_label.py:

# clever_label.py
def label_parts(ticket_id: int, table: int) -> str:
    return "ticket " + str(ticket_id) + " → table " + str(table)


def label_f(ticket_id: int, table: int) -> str:
    return f"ticket {ticket_id} → table {table}"


def main() -> None:
    print(label_parts(7, 12))
    print(label_f(7, 12))
    print(label_parts(7, 12) == label_f(7, 12))


if __name__ == "__main__":
    main()

Run:

uv run python clever_label.py

Output:

ticket 7 → table 12
ticket 7 → table 12
True

The + version is not a win you will measure on a desk. The f-string is the default. Optimize after cProfile points at a function that actually runs a million times.

The boring rule

  • Make it correct. Then measure. Then change one thing.
  • timeit for a snippet. python -m cProfile -s tottime for a program.
  • Do not treat wall-clock floats as golden tests.
  • I/O and algorithms beat micro-syntax.
  • Keep the readable version when the profile is a tie.

Try this

  1. Print timeit.timeit(count_open, number=1000) in count_open.py and note it changes run to run.
  2. Add a third function that uses a list comprehension and check it against count_sum.
  3. Run uv run python -m cProfile -s tottime profile_open.py and find count_open in the table.