A practical guide to finding and fixing performance bottlenecks in Python applications.
Performance optimization should always follow this order:
1. Measure — Profile before changing anything
2. Identify — Find the actual bottleneck (not the suspected one)
3. Optimize — Apply targeted fixes
4. Verify — Benchmark before and after
Start with cProfile to find which functions consume the most time:
from src.profilers.cpu_profiler import CPUProfiler
profiler = CPUProfiler()
with profiler.profile():
run_your_application()
profiler.print_stats(top_n=20, sort_by="cumulative")Look for functions with high cumulative_time — they're the bottleneck entry points.
If your application uses too much memory or leaks:
from src.profilers.memory_profiler import MemoryProfiler
profiler = MemoryProfiler()
with profiler.track():
process_data()
profiler.print_summary(top_n=10)
report = profiler.detect_leaks()Once you've identified the slow function, profile it line by line:
from src.profilers.line_profiler import LineProfiler
lp = LineProfiler()
@lp.profile
def process_records(records):
...
process_records(data)
lp.print_stats()Slow: result += string in a loop (O(n^2) total)
Fast: "".join(parts) (O(n) total)
Slow: Querying related objects one at a time
Fast: Batch fetch with WHERE id IN (...) or ORM eager loading
Slow: Recomputing the same value in every function call
Fast: Use @ttl_cache or @memoize from src/optimizers/caching.py
Slow: Sequential HTTP requests to multiple APIs
Fast: asyncio.gather() or parallel_map() from src/optimizers/batch_processing.py
Slow: Creating new lists/dicts when filtering
Fast: Use generators, itertools, or views
| Approach | Best For | GIL Impact |
|---|---|---|
asyncio | I/O-bound: HTTP, DB, file I/O | Not affected |
threading | I/O-bound with legacy sync code | Limited by GIL for CPU |
multiprocessing | CPU-bound: math, compression, image processing | Bypasses GIL |
Rule of thumb:
asyncio or threadingmultiprocessingasyncio for I/O + ProcessPoolExecutor for CPUBefore optimizing code, check your algorithm's time complexity:
| Operation | list | dict | set |
|---|---|---|---|
| Lookup | O(n) | O(1) | O(1) |
| Insert | O(1)* | O(1) | O(1) |
| Delete | O(n) | O(1) | O(1) |
| Sort | O(n log n) | N/A | N/A |
*append is amortized O(1), insert at index is O(n)
if x in list with if x in set for large collectionscollections.Counter instead of manual counting loopsheapq.nlargest() instead of sorting when you need top-Kbisect.insort() for maintaining sorted order (see SortedList)When pure Python isn't fast enough:
1. NumPy — Vectorized array operations (100x faster for math)
2. Cython — Compile Python to C with type declarations
3. PyPy — Alternative interpreter with JIT compilation
4. Rust/C extensions — Write critical paths in compiled languages
By Datanest Digital | Python Performance Toolkit v1.0.0