Python 3.15 ships with a brand-new profiling package defined in PEP 799. It finally retires the ancient profile module, gives cProfile a new home under profiling.tracing, and adds profiling.sampling, a statistical sampling profiler code-named Tachyon. You can point it at a script, a module, or even a live production process, and it’ll tell you where the time goes with virtually no overhead.
By the end of this tutorial, you’ll understand that:
- Python 3.15 organizes its profilers under a new
profilingpackage, deprecating the oldprofilemodule. - The new sampling profiler peeks at your program’s call stack from the outside, so your code runs at full speed while being profiled.
- You can attach to a running process by its PID without restarting or modifying the target, as long as you have the right permissions.
- Sampling modes let you separate CPU work from I/O waits and isolate code that runs while holding the GIL or handling an exception.
- The profiler supports many output formats, including interactive flame graphs, line-level heatmaps, and a live top-like terminal dashboard.
First, you’ll get a quick refresher on how tracing and sampling profilers differ. Then, you’ll set up Python 3.15 with uv and profile a small 3D renderer with deliberately planted bottlenecks. Along the way, you’ll try out threads, async tasks, native-code boundaries, and the visualizations the profiler offers.
To get the most out of this tutorial, you should be comfortable running commands in a terminal because that’s where the profiler lives. It’ll also help if you’ve dabbled in threading or asyncio before, as you’ll profile both kinds of concurrency. Prior profiling experience is a plus, not a requirement. You’ll review the essential theory as you go.
Note: The examples in this tutorial use Python 3.15.0b4, so some details may shift slightly before the final release in October.
Get Your Code: Click here to download the free sample code you’ll use to explore the sampling profiler in Python 3.15.
Get to Know Python’s New profiling Package
For decades, Python has shipped two deterministic profilers in the standard library:
The first one is a pure-Python implementation that’s mostly of educational value, while the second one does the same job in the C programming language at a fraction of the cost. PEP 799 tidies up this corner in Python 3.15 by introducing a dedicated profiling package with two submodules:
| Module | What It Is |
|---|---|
profiling.tracing |
The deterministic tracing profiler formerly known as cProfile |
profiling.sampling |
The new statistical sampling profiler, code-named Tachyon |
The old modules don’t disappear overnight, though. The cProfile module sticks around indefinitely as a thin alias, so your existing tooling won’t break. On the other hand, the pure-Python profile module enters its retirement. Importing it now triggers a warning, and Python 3.17 will remove it entirely:
>>> import profile
<python-input-0>:1: DeprecationWarning: The profile module is deprecated
⮑ and will be removed in Python 3.17. Use profiling.tracing (or cProfile)
⮑ for tracing profilers instead.
>>> import cProfile
>>> import profiling.tracing
>>> cProfile.Profile is profiling.tracing.Profile
True
The comparison in the last line proves that cProfile and profiling.tracing expose the very same class, so the two names are interchangeable. If you’d like a refresher on using the tracing profiler and interpreting its output, then check out Profiling in Python, which walks through cProfile and friends in detail.
The package reorganization in the standard library is the boring part of PEP 799. The interesting part is profiling.sampling, and that’s what you’ll spend the rest of this tutorial on.
Compare Tracing and Sampling Profilers
Before you fire up the new sampling profiler, it helps to know how it differs from its tracing counterpart. Both tools answer the same question, namely where your program spends most of its time. They just gather evidence in different ways. That difference dictates when you should reach for each of them. Here’s the thirty-second version of the theory before you dive deeper.
How Tracing Profilers Work
A deterministic tracing profiler hooks into the interpreter and registers a callback that runs on every single function call and return. Nothing escapes it. The resulting numbers are exact, down to how many times each function ran.
You can watch this happen below. Press play, and the profiler fires a hook on every call and return of a small demo program:
That precision comes at a price. The constant interruptions can slow your program down severalfold and, worse, distort the measurements themselves. Cheap functions called millions of times suddenly look expensive because the profiler’s bookkeeping dwarfs their actual work. That’s why running a tracing profiler against a production workload is rarely an option.
How Sampling Profilers Work
A statistical sampling profiler takes the opposite approach. Instead of instrumenting your code, it periodically captures snapshots of the call stack—by default, a thousand times per second—and counts which functions it catches in the act. Functions that consume the most time will statistically appear in the most samples.