Engineering Notes
Home/Languages/Python/Python Performance and Profiling
Languages · Python

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.

💻 Languages● Intermediate⏱ 3 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 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

1Measure Before Optimizing

QuestionTool
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
python
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 list is O(n); x in set/dict is 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.deque for queues, heapq for top-k, bisect for sorted inserts, Counter for 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/cache for 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

  1. Optimizing without profiling: guessing the bottleneck.
  2. Micro-optimizing code that runs once while a query runs 1,000 times.
  3. timeit on tiny inputs that don't represent production.
  4. Unbounded lru_cache growing memory forever.
  5. Threads for CPU-bound code.

📋 Quick Reference

text
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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

1. x in big_list inside a loop over n items costs…
Use a set.
2. tottime vs cumtime in cProfile?
Self vs inclusive.
3. Profile a production process without code changes?
Sampling profiler.
4. Fastest way to build a big string from parts?
Linear.