ServicesWorkBlogAboutContactStart Project

Python Performance Optimization: A Practical Guide

Zeeshan Ahmad

Zeeshan Ahmad

June 15, 2026 · 2 min read

Python Performance Optimization: A Practical Guide

"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:

python
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:

python
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:

python
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

OperationListSetDict
Membership check (x in y)O(n)O(1)O(1)
InsertO(1) amortizedO(1)O(1)
Ordered iterationYesNoInsertion 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:

python
# 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.

Get new articles in your inbox

No spam just engineering write-ups on AI, backend, and software architecture, sent when we publish something worth reading.