"Python is slow" is true and mostly irrelevant almost no production slowdown is caused by the language itself. It's caused by doing avoidable work: re-computing the same result, blocking on I/O that could run concurrently, or reaching for a data structure with the wrong complexity for the job.
Step 1: profile before you optimize
Guessing where time goes is the single biggest waste of engineering effort in performance work. Start here:
import cProfile
import pstats
profiler = cProfile.Profile()
profiler.enable()
run_the_slow_function()
profiler.disable()
stats = pstats.Stats(profiler).sort_stats("cumulative")
stats.print_stats(15)
This tells you where time is actually spent usually somewhere surprising.
Step 2: cache what doesn't change per request
functools.lru_cache is underused for how cheap it is to apply:
from functools import lru_cache
@lru_cache(maxsize=512)
def get_exchange_rate(currency: str) -> float:
return fetch_from_slow_api(currency)
For anything shared across requests (not per-user), this alone can remove entire categories of redundant network calls.
Step 3: stop blocking on I/O
CPU-bound code doesn't benefit from asyncio I/O-bound code (API calls, database queries, file reads) does, dramatically:
import asyncio
import httpx
async def fetch_all(urls: list[str]) -> list[str]:
async with httpx.AsyncClient() as client:
responses = await asyncio.gather(*[client.get(u) for u in urls])
return [r.text for r in responses]
Fetching 20 URLs sequentially at 200ms each is 4 seconds. Concurrently, it's close to 200ms total.
Step 4: reach for the right data structure
| Operation | List | Set | Dict |
|---|---|---|---|
Membership check (x in y) | O(n) | O(1) | O(1) |
| Insert | O(1) amortized | O(1) | O(1) |
| Ordered iteration | Yes | No | Insertion order (3.7+) |
A user_id in allowed_list check inside a hot loop is a common, easy-to-miss O(n) cost swapping allowed_list for a set is a one-line fix with a real measurable effect at scale.
Step 5: drop to native code only when profiling says so
For genuinely CPU-bound hot paths, Cython or Rust (via PyO3) can help but only after profiling confirms pure-Python logic is the bottleneck, not I/O or architecture:
# cython: language_level=3
def sum_squares(int n):
cdef int i
cdef long total = 0
for i in range(n):
total += i * i
return total
Optimization checklist
- Profiled the actual hot path with
cProfile - Cached pure functions with repeated inputs
- Moved I/O-bound work to
asyncio - Audited data structures in hot loops
- Considered native extensions only for confirmed CPU-bound bottlenecks
The takeaway
Performance work pays off in this exact order: profile, cache, concurrency, data structures, native code in that order, not reversed. Most teams jump straight to "let's rewrite it in Rust" before confirming the bottleneck isn't a single missing cache.
Need a performance audit on an existing Python service? Reach out we'll profile it before recommending anything.






