Python performance
Python overhead is per-operation, so wins come from doing fewer Python-level operations: better algorithms first, then batch work into C-backed calls, then compile the hot loop.
Method
- Profile before touching code.
python -m cProfile -s cumtime app.pyfor call-level,py-spy top --pid Nfor live processes without restarting,line_profilerfor a single hot function. Optimizing an unprofiled guess wastes days; the hot spot is rarely where you think. - Fix the algorithm before the language. An O(n^2) membership scan beaten into a set lookup outruns any rewrite. Check data-structure fit: list for order, set/dict for membership, deque for both-ends, heapq for top-k.
- Vectorize numeric loops. A numpy expression over an array replaces a
Python loop with one C loop, commonly 10-100x. The trap is accidental
element-wise Python (calling
float()per item, object-dtype arrays); keep dtypes numeric and operations whole-array. - Cut allocation and attribute churn in hot loops. Hoist
self.methodand global lookups to locals, reuse buffers, prefer comprehensions overappendloops. These 10-30% wins are free but only matter inside the measured hot path. - Escalate to compiled code deliberately. Order of preference: an
existing C-backed library (numpy, polars, re2),
functools.lru_cacheover recomputation, Cython or a small Rust extension (pyo3/maturin) for the one hot function. Keep the compiled surface minimal; it is the code you can no longer read in a debugger. - Verify with a benchmark, not a feeling.
timeitorpytest-benchmarkwith realistic data sizes, before and after, committed next to the code so regressions are visible.
Boundaries
- Do not optimize I/O-bound code with these techniques; overlap the waiting instead (async, threads, batching requests).
- Free-threaded and JIT builds change constants, not complexity; algorithmic wins carry across interpreters, micro-tricks may not.
- Readability is a cost you pay forever; take the 2x that keeps the code plain over the 2.3x that obfuscates it.