Overview
Profile first, then fix the algorithm, then vectorize, then change the concurrency model, and only then compile hot code. Most slow Python is an O(n^2) loop, I/O that waits serially, or needless allocation. Project defaults are in python.
Profile with cProfile and py-spy before changing code
Use cProfile for a whole run and py-spy to sample a live process without restarting it. Fix the top function by cumulative time, re-profile, and stop when the remaining functions are small.
python -m cProfile -o profile.out -s cumulative myscript.py
python -m pstats profile.out
uvx py-spy record -o profile.svg --pid <PID>For memory, take tracemalloc snapshots and compare lines with snapshot.statistics("lineno").
Fix the algorithm before the implementation
Compiling an O(n^2) loop leaves it O(n^2). Replace list membership tests with a set, counting loops with collections.Counter, repeated min extraction with heapq, and sorted-list inserts with bisect.
other_set = set(other_list)
result = [x for x in big_list if x in other_set]Vectorize numeric loops with numpy
Python integers and floats are boxed objects; numpy operates on contiguous C arrays. Replace element-wise loops with array operations. Do not count on np.vectorize for speed: numpy documents it as a convenience wrapper around a Python loop.
Match the concurrency model to the workload
| Workload | Use |
|---|---|
| Many network or disk waits | asyncio (python-async) or threads |
| CPU-bound, data picklable | ProcessPoolExecutor |
| CPU-bound, want isolation without processes | InterpreterPoolExecutor (3.14) |
| CPU-bound, shared in-memory data | threads on a free-threaded build |
Python 3.14 changed the default multiprocessing start method on Linux and other non-macOS Unix systems from fork to forkserver. Worker arguments must be picklable, the entry module needs an if __name__ == "__main__": guard, and code that relied on fork-inherited globals breaks.
Free-threaded builds (python3.14t; install with uv python install 3.14t) are officially supported since 3.14 (PEP 779), and the single-thread penalty is roughly 5 to 10 percent. They are optional builds, not the default. Test your whole dependency tree under 3.14t before moving a service, and do not assume a package is thread-safe because it imports.
The experimental JIT in 3.14 is off by default; do not plan around it.
Compile hot paths only after profiling proves it
mypyccompiles fully typed Python to C extensions with the same interface, so it suits code that already passes a strict checker (python-typing).Cythonis the established option when you need C-level control and accept.pyxsyntax.- Rust through PyO3 and
maturinsuits allocation-heavy, algorithmically complex code.
Measure the speedup on your workload; do not assume one.
Cut memory on large collections
Use @dataclass(slots=True) for classes with millions of instances, generators instead of full lists, and streaming instead of loading whole datasets.