When a Python application becomes slow, the first reaction is often to look for an inefficient loop or replace a function with a faster implementation. Sometimes that helps. But in many applications, the actual bottleneck is somewhere else, such as a database call, a third-party library, or a function that gets called far more often than expected.

Profiling helps developers find out where execution time is going before they start changing code.

Python 3.15 introduces a new profiling package that brings its built-in profiling tools under one namespace. One of its main additions is profiling.sampling, a statistical profiler known as Tachyon. It periodically captures call stacks to show which parts of a running application consume time.

Unlike traditional profilers that record every function call, sampling collects snapshots of program execution. This makes it useful for investigating performance problems in larger applications, including running processes that are difficult to reproduce locally.

In this article, we will look at how to use Python 3.15's sampling profiler, generate flame graphs, investigate CPU and waiting time, and decide when sampling is more useful than deterministic profiling.

What Is Statistical Profiling?

A profiler collects information about program execution so developers can identify expensive operations.

Traditional deterministic profilers record function calls and returns as they happen. This provides detailed call counts and timing information, but the instrumentation can add overhead.

A statistical profiler takes a different approach. It periodically captures the call stack of a running program. Functions that appear frequently in the collected samples are likely to be consuming a significant share of the observed execution time.

For example, imagine an API that processes thousands of requests. A function that validates every request may appear repeatedly in the samples, while a function used only during application startup may appear rarely.

Sampling helps identify these patterns without requiring every function call to be recorded.

Python 3.15 organizes the built-in tools under the profiling package:

For most initial performance investigations, sampling is a sensible starting point. When exact function-call counts are required, deterministic profiling may be more appropriate.

Running the Sampling Profiler

The easiest way to begin is to run the profiler directly against a Python script.

Suppose the application is stored in app.py. Run the following command using a Python 3.15 installation that includes the new profiling package:

python -m profiling.sampling run app.py

The profiler runs the script and collects samples while it executes. After the process finishes, it displays a summary of the observed execution.

This approach is useful when a performance problem can be reproduced with a local script, test workload, or staging environment.

Generating a flame graph

A summary can identify expensive functions, but it does not always make the call hierarchy easy to understand. A flame graph provides a visual representation of the collected call stacks.

Run:

python -m profiling.sampling run --flamegraph app.py

The profiler generates an interactive HTML file that can be opened in a browser.

In a flame graph, wider blocks represent call stacks that account for more samples. The nested blocks show how functions call other functions.

For example, a wide block associated with process_request() may indicate that the function or its descendants account for a substantial portion of the observed execution. Moving deeper into the graph can help identify the functions responsible for that cost.

Do not assume that the widest block is automatically the function that should be rewritten. It may spend most of its time waiting for a database, calling native code, or executing another expensive function. Use the surrounding call hierarchy to understand the result.

Profiling an Existing Python Process

Some performance problems appear only after an application has been running for a while. Restarting the process may make the problem disappear temporarily, which makes local reproduction difficult.

Tachyon can attach to a running Python process without requiring changes to the application's source code or a restart.

First, identify the process ID using the operating system's process tools. Then run:

python -m profiling.sampling attach 12345

Replace 12345 with the actual process ID.

You can limit the profiling session to a particular duration:

python -m profiling.sampling attach -d 30 12345

This collects samples for approximately 30 seconds.

Attaching to a live process is useful when investigating CPU spikes, unexpected background work, or performance degradation that occurs only under real traffic.

There are some practical restrictions. The profiler generally needs to match the target process's Python minor version, and operating-system permissions may be required. For example, attaching to another user's process can require elevated privileges.

In production, obtain the appropriate operational approval before attaching a profiler to a service. Even a low-overhead profiler consumes resources and may collect information about function names, execution paths, or application behavior.

Understanding Profiling Modes

The sampling profiler provides several modes for investigating different performance problems.

Mode

What it measures

When to use it

wall

Samples across elapsed time, including waiting

Investigating slow requests and overall latency

cpu

CPU execution time

Finding computation-heavy functions

gil

Activity related to the Global Interpreter Lock

Investigating thread contention

exception

Exception-related activity

Investigating workloads involving repeated exceptions

The default wall-clock mode is useful when you want to understand the overall time distribution of an operation.

For a CPU-intensive task, try:

python -m profiling.sampling run --mode=cpu app.py

This helps focus the investigation on CPU execution rather than time spent waiting for I/O.

For a multithreaded application, the GIL mode can provide additional information about interpreter lock activity:

python -m profiling.sampling run --mode=gil app.py

Use the mode that matches the question you are investigating. A slow application is not necessarily a CPU-bound application, and optimizing CPU usage will not automatically fix a service that spends most of its time waiting for external systems.

Investigating Multithreaded and Asynchronous Applications

Performance problems can be difficult to diagnose when work is spread across multiple threads or asynchronous tasks.

By default, sampling may focus on the main thread. You can request samples from all threads using the --all-threads option:

python -m profiling.sampling run --all-threads app.py

This helps reveal whether one thread is doing most of the work or whether multiple threads are active.

For an asyncio application, the physical call stack does not always show the logical path of an asynchronous operation. A coroutine may suspend while waiting for a network response and resume later.

The profiler supports async-aware profiling for applications that have loaded asyncio:

python -m profiling.sampling run \
  --async-aware \
  --flamegraph \
  app.py

This allows the profiler to reconstruct call stacks around asynchronous task boundaries.

Async-aware profiling is particularly useful when investigating a service that handles many concurrent requests. However, the results still need to be interpreted in context. A task waiting for a response may be experiencing a slow external dependency rather than executing inefficient Python code.

Reading the Results Correctly

Profiling data is useful only when it leads to a clear engineering decision.

Three concepts are particularly important.

Direct time: Time attributed to a function itself, excluding its child calls. A function with high direct time may contain expensive computation.

Cumulative time: Time attributed to a function and the functions it calls. A high cumulative value can indicate an important part of the call hierarchy, even if the function's own code is inexpensive.

Sample count: The number of collected samples in which a function or call stack appears. This is a statistical observation, not an exact function-call count.

Suppose a report shows that generate_report() has high cumulative time but low direct time. The function may simply be calling an expensive database operation or data-processing function. Rewriting generate_report() itself may not improve performance.

A better approach is to inspect the nested calls and identify where the time is actually spent.

Sampling can miss very short-lived functions because they may execute between samples. If the investigation requires exact call counts or detailed information about brief function calls, use deterministic profiling instead.

Advantages and Disadvantages

Advantages

Low-overhead investigation: Sampling avoids recording every function call and is designed to have very little impact on the target program.

No source-code changes required: Developers can profile existing scripts or attach to running processes.

Useful visualizations: Flame graphs and heatmaps help identify expensive call paths and source locations.

Multiple profiling modes: CPU, wall-clock, GIL, and exception-related modes help investigate different types of performance problems.

Disadvantages

Results are statistical: Short-lived functions may not appear in the collected samples.

Operating-system permissions matter: Attaching to a running process can require additional privileges.

Interpretation requires context: A function that appears frequently may be waiting on an external dependency rather than consuming CPU.

Profiling is not benchmarking: Results help identify bottlenecks but do not replace controlled performance measurements.

Common Mistakes and Troubleshooting

Problem

What to check

The profiler module cannot be found

Confirm that the application is running on a Python 3.15 installation that includes the profiling package

Attaching to a process fails

Check the process ID, Python version compatibility, and operating-system permissions

The flame graph does not identify an obvious bottleneck

Inspect the full call hierarchy and choose the profiling mode that matches the problem

CPU profiling looks normal, but requests remain slow

Investigate database calls, network waits, locks, and other external dependencies

A short function does not appear in the report

Increase the observation duration or use deterministic profiling when exact call counts are required

Profiling output differs between runs

Repeat the test under comparable workloads and account for changes in traffic and application state

A common mistake is to optimize the function with the most obvious name rather than the function responsible for the actual cost. Follow the call stack until you find the expensive operation.

Another mistake is to use profiling output as a benchmark. Profilers are designed to explain program behavior. Use controlled benchmarks to verify whether a code change actually improves performance.

Best Practices for Production Profiling

Summary

Python 3.15 introduces a dedicated profiling package and the Tachyon sampling profiler, giving developers another way to investigate performance without instrumenting every function call.

The profiler can run scripts, attach to existing processes, collect samples from multiple threads, and generate flame graphs that make expensive call paths easier to identify.

The best approach is to begin with a specific performance question, select the appropriate profiling mode, and follow the evidence to the underlying bottleneck. Use deterministic profiling when exact call counts are required, and use controlled benchmarks to confirm that an optimization actually helps.

Profiling works best when it guides a targeted change rather than encouraging developers to rewrite code based on assumptions.