Skip to content

Which Python profiler should I use?

Start with profiling.sampling on Python 3.15 or later. For an already-running process on an older interpreter, start with py-spy. Change tools when the question requires a different measurement, such as allocations, coroutine waiting, or native call stacks.

What needs investigating? Start with Check before recording
CPU or wall time on Python 3.15+ profiling.sampling Profiler and target must match minor version and standard/free-threaded build type
A running process without a restart py-spy Interpreter support and OS process-inspection permissions
A local script or request whose elapsed time is hard to explain Pyinstrument Run under the profiler or add its profiling integration
Python/native CPU attribution alongside memory growth and copies Scalene Launch under the profiler; check platform-specific limitations
Async task relationships or external sampling of a free-threaded process Austin Async task graphs require CPython 3.14+; check the target build and OS
Allocation call paths and memory leaks Memray Supports Linux and macOS; choose allocation and lifetime reports deliberately
Exact function-call counts cProfile, also exposed as profiling.tracing on Python 3.15+ Recording calls changes execution cost

These are starting points, not an overhead ranking. A profiler that answers the wrong question produces an unhelpful report even when its runtime cost is low.

Decide whether waiting belongs in the result

CPU time asks where computation happens. Wall-clock time asks where elapsed time goes, including I/O and other waits. A server may respond slowly while using little CPU, so a CPU-only profile can miss the reason a user is waiting.

Tachyon, the codename for profiling.sampling, has separate wall-clock, CPU, and GIL modes. Pyinstrument emphasizes wall-clock call trees and hides library detail by default, which makes it a useful first local view. py-spy’s idle and GIL filters change which thread samples contribute to its report.

Keep the chosen mode with the saved report. Comparing a wall-clock percentage with a CPU percentage does not establish a performance difference. The sampling versus deterministic explanation covers what each measurement method tells you.

Attach to running work with an external sampler

py-spy, Austin, and Tachyon run outside the target process. They read interpreter state through OS process-inspection interfaces, allowing a recording without adding an import or restarting the application. OS permissions and container capabilities still apply.

Use py-spy for a first recording from an existing conventional CPython process; the py-spy how-to covers recording and attachment. For a free-threaded target, Austin is a documented option; Tachyon also supports it when the profiler’s build type matches. A tool’s support for a Python minor version does not automatically cover that version’s free-threaded build.

Scalene and Pyinstrument normally collect profiles by launching the program under the profiler or by using an in-process integration. Choose them when changing how the local program starts is practical.

Follow async tasks across waits

An ordinary sample of the event-loop thread sees whichever coroutine is running at that moment. It does not reconstruct all suspended tasks or their dependencies.

Austin tracks asyncio task graphs on CPython 3.14 and later; its VS Code integration shows task traces. Tachyon reconstructs task-aware stacks on Python 3.15 and later. Scalene attributes suspended await time and concurrency to source lines, which helps distinguish serialized I/O from concurrent waits.

Choose task graphs when the question is which task waits for another. Choose line-level wait attribution when the question is which await in the code accounts for delays. Async profiling and native-stack options are not freely interchangeable: Austin and Tachyon restrict combinations of these modes.

Distinguish native markers from native functions

Tachyon’s --native option inserts a synthetic <native> frame to mark execution outside Python. It does not name the C/C++ functions inside that call. A wide marker is a reason to investigate further, rather than proof of a particular native bottleneck.

py-spy unwinds native frames on supported Linux architectures and Windows x86-64, but not macOS. Austin provides native stack sampling on supported Linux, macOS, and Windows systems. Its native mode briefly suspends threads, so its documentation advises avoiding that mode in production.

Scalene provides Python/native attribution and stitched stack views where supported. Check its platform restrictions, including unavailable native stacks on Windows and Apple M4 and later chips with SME instructions. Use a compatible native-stack tool when the investigation needs named functions inside an extension.

Separate memory residency from allocations

Resident set size (RSS) measures physical memory resident for the process. Allocation tracking records memory requests and their call paths. The two measurements answer different questions: memory may be allocated without every page becoming resident, and an allocator may keep freed memory for reuse.

Austin’s memory mode samples RSS deltas. Use it to correlate changing residency with sampled execution, rather than treating its report as a complete allocation ledger. Attribution is less reliable on free-threaded builds because another thread may cause a delta.

Use Scalene when a combined line-level CPU, memory-growth, and copying report is useful. Use Memray when the investigation centers on allocation stacks, native allocation activity, or retained allocations. Reproduce the workload before concluding that growth represents a leak.

Verify the improvement outside the profile

A profile’s percentage is relative to the rest of that recording. A function’s share may grow because another function became faster, even when the first function’s absolute runtime stays the same.

Record comparable inputs before and after a change, repeat the measurement, and check unprofiled elapsed time or throughput. Sampling noise, process load, and profiler options all affect the report. For small isolated operations, use timeit or pyperf to measure the change.

Last updated on