Performance Tuning
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.pyOutput:
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.pyOutput:
100
True
From a terminal, the human-readable table is:
uv run python -m cProfile -s tottime count_open.pySort 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.pyOutput:
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.pyOutput:
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.
timeitfor a snippet.python -m cProfile -s tottimefor 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
- Print
timeit.timeit(count_open, number=1000)incount_open.pyand note it changes run to run. - Add a third function that uses a list comprehension and check it against
count_sum. - Run
uv run python -m cProfile -s tottime profile_open.pyand findcount_openin the table.