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:
profiling.samplingprovides statistical profiling through Tachyon.profiling.tracingprovides deterministic profiling.cProfileremains available as a compatibility interface for deterministic profiling.
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.pyThe 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.pyThe 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 12345Replace 12345 with the actual process ID.
You can limit the profiling session to a particular duration:
python -m profiling.sampling attach -d 30 12345This 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 |
|---|---|---|
| Samples across elapsed time, including waiting | Investigating slow requests and overall latency |
| CPU execution time | Finding computation-heavy functions |
| Activity related to the Global Interpreter Lock | Investigating thread contention |
| 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.pyThis 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.pyUse 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.pyThis 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.pyThis 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
Reproduce the problem with a representative workload whenever possible.
Start with sampling before introducing more intrusive tracing.
Select wall-clock or CPU mode based on the performance question.
Use flame graphs to understand call hierarchies rather than relying on a single number.
Profile long enough to capture the behavior you want to investigate.
Record the Python version, workload, and profiler settings so results can be compared.
Obtain appropriate approval before attaching to production processes.
Confirm improvements with repeatable benchmarks and application-level measurements.
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.
Join the conversation! Your thoughts help the community grow.