Self-hosted service
joerick/pyinstrument avatar
joerick/pyinstrument

Pyinstrument: a sampling profiler that reads like a call stack

🚴 Call stack profiler for Python. Shows you why your code is slow!

8,014 stars302 forksPythonBSD-3-Clause

At a glance

What is it?
Pyinstrument is a call stack profiler for Python that samples the stack instead of tracing every call. It is easy to start with, but the README is upfront about Docker timing problems and pickle scripts, and there is no memory profiling at all.
Who is it for?
Adopt Pyinstrument when you need to know which call path is eating wall-clock time in a Python service or script, especially if you want an HTML report you can open without extra tooling. Do not adopt it for memory analysis: it profiles time only, and the topic list mentions memory profiling but the README and changelog describe no memory feature.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 28 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Pyinstrument is for, and who actually needs it

Pyinstrument answers one question: where did the time go? The README frames it plainly, saying a profiler helps you optimize your code and that you should "focus on the slowest part of your program", linking to Amdahl's law. That framing matters, because it tells you the intended workflow. You have a slow endpoint, a slow batch job, or a slow test, and you want a ranked view of the call paths that consumed the wall clock.

The audience is Python developers on 3.8 or later who want that answer without instrumenting every function by hand. The repository ships examples for aiohttp, Falcon, Flask, and Litestar, plus async examples and a busy_wait script, which suggests the maintainer expects people to profile web handlers and async code, not just plain scripts. If your problem is "this function is called too many times", a tracing profiler gives you exact counts; Pyinstrument gives you a sampled picture of the stack, which is a different trade.

How sampling and the call stack produce the report

Pyinstrument is a sampling profiler. Rather than wrapping every call, it takes periodic samples of the running call stack and attributes time to the frames it observes. The repository layout reflects that: the pyinstrument/low_level directory holds stat_profile.c, pyi_floatclock.c, and pyi_timing_thread.c, and setup.py builds them into a single extension module named pyinstrument.low_level.stat_profile. The timing machinery lives in C, so the sampling loop does not depend on the interpreter's own scheduling.

The output is a call tree, not a flat list of functions. That is the design decision that shapes everything else. When you read the report, you see the path that led to the expensive frame, so a slow function that is only slow because of what it calls is visible as such. Version 5.0.0 changed how library code is detected: previously any path containing the string /lib/ was treated as library and collapsed, and now pyinstrument captures the paths of the Python install and any active virtualenv or conda environment at profile time and treats files stored there as library. That should reduce false positives, but it also means the collapse behaviour depends on how your environment is laid out when the profile runs.

Installing Pyinstrument and profiling your first script

The README gives one install line, and it supports Python 3.8 and above. There is no required runtime dependency: setup.py has an empty install_requires, with optional extras for tests, docs, examples, and a bin extra that pulls in click. Run this in the environment whose code you want to measure, not a separate one.

bash
pip install pyinstrument

To profile a script from the command line, pass the script as an argument. The README shows this exact form, and notes that when script.py contains a class serialized with pickle you may hit errors because the serialization machinery does not know where __main__ is, with a linked issue for workarounds.

bash
pyinstrument script.py

For a specific chunk of code, version 4.7.0 added a context manager and a decorator that print a short readout to the terminal. The examples directory contains context_api.py, which is the shape to copy.

python
from pyinstrument import Profiler

with Profiler() as profile:
    do_the_thing()

print(profile.output_text())

For a web report, the HTML renderer writes a self-contained page. Version 5.0.0 added an interactive timeline you can zoom into, and the renderer now limits the sample count so the browser can load the file. Version 5.1.2 added a --target-description CLI option and a preprocessor hook on HTMLRenderer.

Django, Jupyter and async: the integrations and their configuration

The integrations are where Pyinstrument stops being a one-off CLI tool and becomes part of a stack. The Django middleware is configured through environment variables, and the changelog names several: PYINSTRUMENT_PROFILE_DIR writes profiles to disk, PYINSTRUMENT_SHOW_CALLBACK controls whether a profile is shown, PYINSTRUMENT_INTERVAL sets the sampling interval, and a callback can customize the saved filename. Version 5.1.3 fixed the middleware so PYINSTRUMENT_SHOW_CALLBACK is respected when PYINSTRUMENT_PROFILE_DIR is configured, which means before that release the two settings interacted badly. If you are on an older 5.1.x, that interaction is worth checking.

Jupyter is handled by a %%pyinstrument cell magic. Version 5.1.3 fixed the magic so console-specific render options, including time=percent_of_total, flat, short_mode, color, and Unicode settings, are applied to text output. Before that, setting those options in a notebook would not change what you saw. Async support is represented by the async examples and by aiohttp.web documentation added in 5.1.0, but the README does not describe how the sampler treats tasks versus threads, so the honest position is that async is demonstrated, not explained. For threads, the timing thread is implemented in C, and the changelog for 5.1.1 mentions fixing memory leaks in the low-level C extension, which is a reminder that this component is the part most likely to have sharp edges.

Where Pyinstrument gives you the wrong answer

The README's known issues section is short and specific, and both entries are real failure modes. First, profiling inside a Docker container can produce strange results because the gettimeofday syscall that pyinstrument uses is slow in that environment. This is not a bug in the profiler so much as a measurement problem: if the clock call itself is expensive, the sampler's own overhead leaks into the profile and distorts attribution. If you are profiling a containerized service, the numbers you get may not transfer to bare metal.

Second, running pyinstrument script.py against a script that contains a pickled class can fail, because the serialization machinery does not know where __main__ is. The README links an issue with workarounds rather than describing them inline, so you should read that thread before assuming it will just work.

Beyond those, the tool is a time profiler. The repository topics list includes memory, but neither the README nor the changelog describes a memory profiling capability, and there is no such module in the top-level layout. If your problem is allocation growth or retained objects, Pyinstrument is the wrong instrument. Sampling itself is also a limitation: rare but expensive calls can be missed entirely if your interval is coarse, and the changelog shows the interval is configurable, which means the default is a choice you may need to revisit.

Pyinstrument versus cProfile and py-spy

The comparison people search for is Pyinstrument against cProfile, and the difference is the mechanism. cProfile is a deterministic tracing profiler built into the standard library: it records every call and returns exact call counts and per-function totals. That precision costs overhead, and the flat function list does not tell you which caller was responsible. Pyinstrument samples instead, so it can attribute time along the call path, and the C timing layer keeps the sampling loop out of the interpreter's way. The cost is statistical rather than exact: you get a representative tree, not a complete ledger.

Against py-spy, the split is different again. Pyinstrument runs inside your process and can be started from the CLI, from a with block, from a decorator, or from a Django middleware, and it produces a call tree you can open as HTML. That is a good fit for development and for staged profiling. It is not a tool you attach to a production process you cannot restart. If you need to inspect a running process without touching its code, Pyinstrument is not that tool, and the README does not claim otherwise.

Maintenance, licensing and the cost of upgrading

The project is not archived, and the last push was on 2026-09-01. The most recent release listed is v5.1.3 on 2026-07-29, preceded by v5.1.2 on 2026-01-04 and v5.1.1 on 2025-08-12. The cadence is patch-heavy: the 5.1.x line is mostly fixes to integrations, the HTML renderer, and the C extension, plus expanded wheel coverage for CPython prereleases and GraalPy in 5.1.3. There is no long-term support branch described, so the practical upgrade path is to track the latest 5.x.

Upgrade cost is concentrated in two places. The C extension means wheels matter: if your platform has no prebuilt wheel, you build from source, and the README notes that running from a git checkout requires a build step. The Django middleware is the other place, because the 5.1.3 fix changes behaviour when both PYINSTRUMENT_PROFILE_DIR and PYINSTRUMENT_SHOW_CALLBACK are set. If you rely on profiles being written to disk, test that combination after upgrading.

The licence is BSD-3-Clause, which is permissive and imposes no copyleft obligation on your application. That is a factual statement about the licence identifier, not legal advice; if your organization has rules about bundled C extensions or vendored code, note that the repository contains a pyinstrument/vendor directory and that 5.1.3 replaced a deprecated vendored appdirs implementation with internal platform-specific handling.

Editorial conclusion

Adopt Pyinstrument when you need to know which call path is eating wall-clock time in a Python service or script, especially if you want an HTML report you can open without extra tooling. Do not adopt it for memory analysis: it profiles time only, and the topic list mentions memory profiling but the README and changelog describe no memory feature. Do not adopt it as a drop-in replacement for a tracing profiler when you need exact per-call counts, since it samples. Before rolling it into a Django or async service, verify on your own workload that the gettimeofday cost inside your container does not distort the numbers, and confirm whether you need PYINSTRUMENT_PROFILE_DIR or a filename callback for saved runs.

Frequently asked questions

How do I use Pyinstrument to profile a Python script?

Install it with pip install pyinstrument, then run pyinstrument script.py. The README notes that if the script contains a class serialized with pickle, you may hit errors because the serialization machinery does not know where __main__ is, and it links an issue with workarounds.

How does Pyinstrument compare with cProfile?

Pyinstrument samples the call stack and reports time along the call path, while cProfile traces every call and gives exact counts and per-function totals. Pyinstrument is built for finding the slow part of a program, which the README ties to Amdahl's law.

Is there a Pyinstrument alternative for profiling Python?

cProfile is the standard-library tracing profiler and is the main contrast: it records every call rather than sampling, so it gives exact counts but a flat view of functions. Pyinstrument's advantage is the call tree and the HTML report with an interactive timeline added in version 5.0.0.

Official sources

  1. joerick/pyinstrument on GitHub
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/joerick-pyinstrument.svg)](https://hysenlabs.com/projects/joerick-pyinstrument)