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
Get the full Python Performance Toolkit and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.