🐍 Python Q99 / 183

How to profile Python applications?

AI-Powered Answer ✓ Answered

Profiling is a crucial technique for understanding the performance characteristics of your Python code, helping you identify bottlenecks, memory leaks, and other inefficiencies. This guide covers both built-in Python profilers and popular third-party tools.

Built-in Python Profilers

Python includes several built-in modules for profiling. The most commonly used is cProfile (or profile for pure Python implementation), which provides deterministic profiling, meaning it monitors call times and counts for all function calls.

cProfile

cProfile is a C extension for profile and offers minimal overhead, making it suitable for profiling production-like environments. You can run it from the command line or programmatically.

Command Line Usage

bash
python -m cProfile -o output.prof your_script.py

This command runs your_script.py, collects profiling data, and saves it to output.prof.

Programmatic Usage

python
import cProfile
import pstats

def my_function():
    # Simulate some work
    sum(range(1_000_000))

pr = cProfile.Profile()
pr.enable()

my_function()

pr.disable()
pr.dump_stats('output.prof')

# Or print stats directly
# ps = pstats.Stats(pr)
# ps.sort_stats('time').print_stats(10)

Analyzing cProfile Output with pstats

The raw output from cProfile is a binary file. To interpret it, you typically use the pstats module, which can load and format the profiling data in various ways.

python
import pstats

# Load the stats from the dumped file
stats = pstats.Stats('output.prof')

# Sort by cumulative time and print top 10 functions
stats.sort_stats('cumulative').print_stats(10)

# Sort by internal time (excluding calls to sub-functions) and print top 10
stats.sort_stats('tottime').print_stats(10)

# Print stats for a specific function
stats.print_stats('my_function')

# You can also use other sorting keys: 'calls', 'filename', 'line', 'name'

timeit

timeit is a small module for timing small bits of Python code. It's ideal for comparing the performance of different implementations of a small function or statement, especially when you need to exclude setup overhead.

Command Line Usage

bash
python -m timeit "'-'.join(str(n) for n in range(100))"

Programmatic Usage

python
import timeit

setup_code = "import math"
stmt_code = "math.sqrt(2) * math.sqrt(2)"

times = timeit.repeat(stmt=stmt_code, setup=setup_code, number=1_000_000, repeat=5)
print(f"Min execution time: {min(times):.4f} seconds")

# For a single function call
def test_function():
    sum(range(1000))

timer = timeit.Timer(stmt=test_function)
print(f"Average time for 1 million runs: {timer.timeit(number=1_000_000):.4f} seconds")

External Profiling Tools

While built-in tools are powerful, several third-party libraries offer more specialized or user-friendly profiling capabilities.

line_profiler

line_profiler provides line-by-line timing statistics, which can be incredibly useful for pinpointing exact lines of code that consume the most time within a function.

Installation

bash
pip install line_profiler

Usage

You decorate functions you want to profile with @profile (after importing profile from kernprof.py or running with kernprof).

python
# my_script.py

@profile
def slow_function():
    a = [i * i for i in range(100_000)]
    b = [i * 2 for i in range(200_000)]
    return a, b

def main():
    slow_function()

if __name__ == '__main__':
    main()
bash
kernprof -l my_script.py

# View results
python -m line_profiler my_script.py.lprof

memory_profiler

memory_profiler monitors memory consumption line-by-line, helping you identify parts of your code that are memory-intensive or have memory leaks.

Installation

bash
pip install memory_profiler matplotlib # matplotlib for graphical output

Usage

python
# my_memory_script.py

from memory_profiler import profile

@profile
def create_large_list():
    a = [i for i in range(10_000_000)]
    b = a * 2 # Creates a new list with double the elements
    return a, b

if __name__ == '__main__':
    create_large_list()
bash
python -m memory_profiler my_memory_script.py

py-spy

py-spy is a sampling profiler for Python programs. It works by sampling the call stacks of the Python interpreter, meaning it doesn't instrument your code and adds very little overhead. It can even profile running processes without restarting them, making it excellent for production debugging.

Installation

bash
pip install py-spy

Usage (Profiling a running process)

bash
# First, start your Python application in one terminal
# python your_app.py

# In another terminal, find its PID
# ps aux | grep your_app.py

# Then, profile it (replace <PID> with the actual process ID)
py-spy record -o profile.svg --pid <PID>

# Or profile it for a certain duration
py-spy record -o profile.svg --duration 10 --pid <PID>

# Or attach to a command and profile it from start to finish
py-spy record -o profile.svg -- python your_app.py

py-spy generates flame graphs (SVG files) which are interactive and highly visual representations of your code's performance, making it easy to spot hot paths.

Best Practices for Profiling

  • Profile Representative Workloads: Ensure you profile with data and conditions similar to your production environment.
  • Focus on Hotspots: Don't optimize prematurely. Use profiling to identify true bottlenecks (typically 80/20 rule).
  • Iterate: Make changes, then profile again to confirm improvements and check for new bottlenecks.
  • Understand Profiler Overhead: Be aware that profilers themselves introduce some overhead, especially deterministic ones like cProfile.
  • Use the Right Tool: timeit for small snippets, cProfile for overall function call timings, line_profiler for line-by-line analysis, memory_profiler for memory issues, and py-spy for low-overhead production profiling and flame graphs.