Python Performance and Profiling
Make Python code faster with evidence: timeit, cProfile, line_profiler and py-spy to find hotspots, algorithmic fixes first, then caching, built-ins, vectorization, native code and parallelism.
🎯 What you'll learn
- Profiling tools and when to use each
- Algorithmic wins: the right data structures
- Python-level speedups and caching
- NumPy, native extensions and parallelism
📚 Before you start
1Measure Before Optimizing
| Question | Tool |
|---|---|
| How long does this snippet take? | timeit (python -m timeit "..."), pytest-benchmark |
| Which functions use the most time? | cProfile + pstats, snakeviz to visualize |
| Line-by-line hot spots? | line_profiler |
| Production process, no code changes? | py-spy (sampling profiler: py-spy top --pid, flame graphs) |
| Where does memory go? | tracemalloc, memray, scalene |
import cProfile, pstats
with cProfile.Profile() as pr:
run_report()
pstats.Stats(pr).sort_stats("cumulative").print_stats(10)tottime is time spent inside the function itself; cumtime includes everything it calls.
2Algorithmic Wins First
Most speedups come from doing less work:
x in listis O(n);x in set/dictis O(1). Converting a list to a set before repeated membership tests can turn O(n²) into O(n).- Avoid repeated work in loops (hoist invariant computations, precompute lookups).
- Use the right structure:
collections.dequefor queues,heapqfor top-k,bisectfor sorted inserts,Counterfor counting. - Batch I/O: one query returning 1,000 rows beats 1,000 queries; reuse HTTP sessions.
3Python-Level Speedups
- Built-ins and comprehensions run in C:
sum(x*x for x in xs)beats a manual loop;"".join(parts)beats+=in a loop. - Local variable lookups are faster than globals/attributes in tight loops.
- Caching:
functools.lru_cache/cachefor pure functions with repeated arguments. - Generators avoid building large intermediate lists.
- Newer Python versions are much faster (3.11+ gained 10–60% on many workloads).
4Going Native or Parallel
- NumPy/pandas/polars vectorize numeric work in C/Rust.
- Cython, mypyc, Rust (PyO3) for hot loops that can't be vectorized.
- Multiprocessing for CPU-bound parallel work (the GIL), asyncio for I/O concurrency.
- PyPy (JIT) can speed up pure-Python code dramatically, if your dependencies support it.
5Web-Service Specific
Profile real requests: database query counts and N+1 problems, serialization costs (orjson), connection pooling, caching, and running enough worker processes. APM tracing (OpenTelemetry) shows where request time goes.
⚠️ Common Pitfalls
- Optimizing without profiling: guessing the bottleneck.
- Micro-optimizing code that runs once while a query runs 1,000 times.
timeiton tiny inputs that don't represent production.- Unbounded
lru_cachegrowing memory forever. - Threads for CPU-bound code.
📋 Quick Reference
timeit · cProfile/pstats (tottime vs cumtime) · line_profiler · py-spy in production · tracemalloc/memray
algorithms first: set/dict lookups, right structures, batch I/O · built-ins & comprehensions · "".join
lru_cache for pure functions · NumPy / native extensions · processes for CPU · upgrade Python🧠 Key takeaways
- Profile first (cProfile, py-spy); optimize the real hotspot.
- Biggest wins are algorithmic: set/dict lookups, right structures, batched I/O.
- Use built-ins, comprehensions, join and lru_cache for pure functions.
- Go native/vectorized or multiprocess for CPU-heavy work; newer Python is faster.
🎤 Interview Q&A
Q1How do you find what's slow in a Python program?core▾
Reproduce with realistic data, then cProfile (sorted by cumulative time) or py-spy flame graphs to find hotspots, line_profiler for line-level detail, and tracemalloc/memray for memory; for services, APM traces first.
- Why py-spy in production? It samples externally with low overhead and no code changes.
Q2What are common Python performance wins?core▾
Better algorithms and data structures (sets/dicts for lookups, heapq, deque), avoiding repeated work, batching I/O and queries, built-ins and comprehensions, str.join, caching pure functions, generators for streams, and upgrading Python.
- Biggest single win usually? Fixing an O(n²) pattern or N+1 queries.
Q3When would you reach for NumPy, Cython or Rust?core▾
When a hot loop over numbers can't be made fast enough in Python: vectorize with NumPy/polars first; otherwise compile it (Cython, mypyc) or write an extension in Rust (PyO3) for the critical path.
- Cost? Build complexity and harder debugging.
Q4What are the risks of lru_cache?senior▾
Unbounded growth with maxsize=None, keeping references alive (methods keep self alive), stale results for impure functions, and unhashable arguments. Use bounded caches and cache only pure functions.
- Per-instance caching? functools.cached_property for computed attributes.
Q5Why is Python 3.11+ faster?core▾
The Faster CPython project added a specializing adaptive interpreter, cheaper frames and function calls, and zero-cost exception handling; 3.12/3.13 continue with further optimizations and an experimental JIT.
- Practical advice? Upgrade runtimes regularly and re-measure.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.
x in big_list inside a loop over n items costs…