Skip to content

Python Performance — Profiling, Optimization, C Extensions, and Benchmarking

DodaTech Updated 2026-06-29 5 min read

In this tutorial, you will learn about Python Performance. We cover key concepts, practical examples, and best practices to help you master this topic.

Python is fast to write, but not always fast to run. For most applications, Python's speed is adequate. But when you need to process millions of records, handle real-time data, or reduce latency, understanding performance optimization is essential.

DodaTech processes large security scan datasets, so performance optimization is critical — scanning thousands of assets requires efficient data pipelines.

What You'll Learn

  • Profiling with cProfile and py-spy
  • Using slots and other memory optimizations
  • C extensions with Cython
  • Numba for numerical JIT Compilation
  • Multiprocessing vs threading vs asyncio
  • Benchmarking with timeit and pytest-benchmark
  • Common performance pitfalls

Profiling

import cProfile
import pstats

def slow_function():
    total = 0
    for i in range(1000000):
        total += i ** 2
    return total

# Profile
profiler = cProfile.Profile()
profiler.enable()
result = slow_function()
profiler.disable()

# Print stats sorted by cumulative time
stats = pstats.Stats(profiler)
stats.sort_stats("cumulative")
stats.print_stats(10)  # Top 10

# Save to file
profiler.dump_stats("profile.prof")

# View with:
# python -m snakeviz profile.prof  # Web-based viewer
# python -m pstats profile.prof     # Interactive CLI

Context Manager Profiling

import cProfile, pstats
from contextlib import contextmanager

@contextmanager
def profile(sort="cumulative", limit=20):
    profiler = cProfile.Profile()
    profiler.enable()
    try:
        yield
    finally:
        profiler.disable()
        stats = pstats.Stats(profiler)
        stats.sort_stats(sort)
        stats.print_stats(limit)

# Usage
with profile(limit=10):
    slow_function()

Line Profiling

pip install line_profiler
@profile
def expensive_function(n):
    total = 0
    for i in range(n):
        for j in range(100):
            total += i * j
    return total

# Run: kernprof -l -v script.py

slots for Memory Optimization

# Without __slots__: each instance has a __dict__ (overhead)
class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

# With __slots__: no __dict__, attributes stored in tuple
class PointOptimized:
    __slots__ = ("x", "y", "z")

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

# Compare memory usage
import sys

p1 = Point(1, 2, 3)
p2 = PointOptimized(1, 2, 3)

print(f"Regular: {sys.getsizeof(p1)} bytes")      # ~56 bytes + dict
print(f"Slots: {sys.getsizeof(p2)} bytes")         # ~56 bytes, no dict

# For millions of instances, __slots__ saves gigabytes

Cython

# math_ops.pyx
def sum_of_squares(int n):
    cdef int i
    cdef long total = 0
    for i in range(n):
        total += i * i
    return total
# setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("math_ops.pyx"),
)
# Pure Python version (for comparison)
def sum_of_squares_py(n):
    return sum(i * i for i in range(n))

# After building:
from math_ops import sum_of_squares
# Cython version is ~10-50x faster

Numba for Numerical Computing

from numba import njit
import numpy as np
import time

# Pure Python
def monte_carlo_pi_py(n):
    inside = 0
    for _ in range(n):
        x = np.random.random()
        y = np.random.random()
        if x * x + y * y <= 1:
            inside += 1
    return 4 * inside / n

# Numba JIT compiled
@njit
def monte_carlo_pi_jit(n):
    inside = 0
    for _ in range(n):
        x = np.random.random()
        y = np.random.random()
        if x * x + y * y <= 1:
            inside += 1
    return 4 * inside / n

# Compare
n = 10000000
start = time.time()
result_py = monte_carlo_pi_py(n)
print(f"Pure Python: {time.time() - start:.2f}s")

start = time.time()
result_jit = monte_carlo_pi_jit(n)
print(f"Numba JIT: {time.time() - start:.2f}s")
# Typically 50-200x faster

Multiprocessing vs Threading vs Asyncio

import multiprocessing as mp
import threading
import asyncio
import time

# CPU-bound: use multiprocessing
def cpu_intensive(n):
    total = 0
    for i in range(n):
        total += i ** 2
    return total

def parallel_cpu():
    with mp.Pool(processes=mp.cpu_count()) as pool:
        results = pool.map(cpu_intensive, [1000000] * 8)
    return results

# I/O-bound: use asyncio or threading
async def fetch_url_async(session, url):
    async with session.get(url) as response:
        return await response.text()

async def parallel_io():
    import aiohttp
    async with aiohttp.ClientSession() as session:
        tasks = [
            fetch_url_async(session, f"https://api.example.com/{i}")
            for i in range(100)
        ]
        return await asyncio.gather(*tasks)

# Benchmark rule of thumb:
# - CPU-bound: multiprocessing (bypasses GIL)
# - I/O-bound with many connections: asyncio (lightweight)
# - I/O-bound with blocking libraries: threading

Benchmarking with timeit

import timeit

def list_comprehension():
    return [i ** 2 for i in range(1000)]

def manual_loop():
    result = []
    for i in range(1000):
        result.append(i ** 2)
    return result

# Run 10000 times
time_comp = timeit.timeit(list_comprehension, number=10000)
time_loop = timeit.timeit(manual_loop, number=10000)

print(f"List comprehension: {time_comp:.4f}s")
print(f"Manual loop: {time_loop:.4f}s")

# Using pytest-benchmark
# def test_performance(benchmark):
#     result = benchmark(list_comprehension)
#     assert len(result) == 1000

Common Performance Pitfalls

1. String Concatenation in Loops

# BAD: O(n^2) — creates new string each iteration
def bad_concat(items):
    result = ""
    for item in items:
        result += item
    return result

# GOOD: O(n)
def good_concat(items):
    return "".join(items)

2. Attribute Lookup in Loops

# BAD: looks up math.sqrt every iteration
def bad_loop(values):
    import math
    return [math.sqrt(v) for v in values]

# GOOD: local reference
def good_loop(values):
    import math
    sqrt = math.sqrt
    return [sqrt(v) for v in values]

3. Using List Instead of Set for Membership

# BAD: O(n) lookup
def bad_membership(items, target):
    return target in items  # items is a list

# GOOD: O(1) lookup
def good_membership(items, target):
    return target in items  # items is a set

Practice Questions

  1. Profile a function that reads a large CSV and compute which part is slowest.

  2. Optimize a class that stores 1 million user records (use slots and measure).

  3. Write a Numba-accelerated function for matrix multiplication.

  4. Benchmark list comprehension vs map() vs generator for a specific operation.

  5. Use multiprocessing to speed up a CPU-bound image processing pipeline.

Challenge: Data Pipeline Optimizer

Analyze and optimize a data processing pipeline that:

  • Reads 10 million records from a CSV
  • Filters, transforms, and aggregates
  • Writes results to a new file

Measure: memory usage, CPU time, wall time. Optimize using:

  • Chunked processing (not loading everything)
  • Multiprocessing for parallel chunks
  • slots for intermediate objects
  • Cython for hot-path computation

This is similar to DodaTech's scan result processing — millions of records need real-time analysis for Compliance reporting.

Real-World Task: Real-Time Metrics Aggregator

Build a function that consumes a stream of metrics (timestamp, name, value) and computes rolling aggregates (mean, p50, p95, p99) over 1-minute Windows, processing 100,000+ events/second without falling behind. Use asyncio for I/O and optimize the aggregation logic with NumPy or Numba.

This powers DodaTech's live dashboard where security scan metrics update in real time across thousands of assets.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro