<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Adam Johnson - python</title><link href="https://adamj.eu/" rel="alternate"/><link href="https://adamj.eu/tech/atom-python.xml" rel="self"/><id>https://adamj.eu/</id><updated>2026-04-03T00:00:00+01:00</updated><entry><title>Python: introducing profiling-explorer</title><link href="https://adamj.eu/tech/2026/04/03/python-introducing-profiling-explorer/" rel="alternate"/><published>2026-04-03T00:00:00+01:00</published><updated>2026-04-03T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-04-03:/tech/2026/04/03/python-introducing-profiling-explorer/</id><summary type="html">&lt;p&gt;I’ve made another package!
Like icu4py, which &lt;a class="reference external" href="/tech/2026/02/09/python-introducing-icu4py/"&gt;I made in February&lt;/a&gt;, it was sponsored by my client &lt;a class="reference external" href="https://www.rippling.com"&gt;Rippling&lt;/a&gt;.
And like tprof, which &lt;a class="reference external" href="/tech/2026/01/14/python-introducing-tprof/"&gt;I made in January&lt;/a&gt;, it’s a profiling tool!&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://pypi.org/project/profiling-explorer/"&gt;profiling-explorer&lt;/a&gt; is a tool for exploring profiling data from Python’s built-in profilers, which are stored in pstats …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I’ve made another package!
Like icu4py, which &lt;a class="reference external" href="/tech/2026/02/09/python-introducing-icu4py/"&gt;I made in February&lt;/a&gt;, it was sponsored by my client &lt;a class="reference external" href="https://www.rippling.com"&gt;Rippling&lt;/a&gt;.
And like tprof, which &lt;a class="reference external" href="/tech/2026/01/14/python-introducing-tprof/"&gt;I made in January&lt;/a&gt;, it’s a profiling tool!&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://pypi.org/project/profiling-explorer/"&gt;profiling-explorer&lt;/a&gt; is a tool for exploring profiling data from Python’s built-in profilers, which are stored in pstats files.
Here’s a screenshot of it in action, displaying profiling data from running a subset of Django’s test suite:&lt;/p&gt;
&lt;a href="/tech/assets/2026-04-03-profiling-explorer-screenshot.webp"&gt;
  &lt;img src=/tech/assets/2026-04-03-profiling-explorer-screenshot.webp
    alt="Screenshot of profiling-explorer showing a table of functions and associated call counts, internal time, and cumulative time."
       loading=lazy&gt;
&lt;/a&gt;&lt;p&gt;(Click to enlarge.)&lt;/p&gt;
&lt;p&gt;The table copies the pstats interface, with functions in the right-hand column and stats about them in columns to the left.
However, it has some differences, such as using improved column names and always displaying times in milliseconds, and it comes with these features:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Dark mode (sorry for the blinding screenshot!).&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;calls&lt;/strong&gt;, &lt;strong&gt;internal ms&lt;/strong&gt;, or &lt;strong&gt;cumulative ms&lt;/strong&gt; column headers to sort by that column.&lt;/li&gt;
&lt;li&gt;Use the search box to filter by filename or function name.&lt;/li&gt;
&lt;li&gt;Hover by a filename + line number pair to reveal the copy button, which copies the location to your clipboard for faster opening.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;callers&lt;/strong&gt; or &lt;strong&gt;callees&lt;/strong&gt; links on the right of a row (not pictured above) to see the callers or callees of that function.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, if I put &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;../django/db/migrations/&lt;/span&gt;&lt;/code&gt; in the filter box, I can limit the table to just functions in Django’s migration system, which could be useful for optimizing that system:&lt;/p&gt;
&lt;a href="/tech/assets/2026-04-03-profiling-explorer-screenshot-filtered.webp"&gt;
  &lt;img src=/tech/assets/2026-04-03-profiling-explorer-screenshot-filtered.webp
    alt="Screenshot of profiling-explorer filtering data."
       loading=lazy&gt;
&lt;/a&gt;&lt;p&gt;(Click to enlarge.)&lt;/p&gt;
&lt;p&gt;profiling-explorer was motivated by doing lots of optimization work, at Rippling, in Django, and elsewhere.
I’ve often used Python’s pstats to inspect profiling data, but I’ve found its command line interface clunky and slow.
Visualization tools like &lt;a class="reference external" href="https://pypi.org/project/gprof2dot/"&gt;gprof2dot&lt;/a&gt; can help, but they don’t provide the immediacy of a table interface, where many numbers can be skimmed and compared at a glance.&lt;/p&gt;
&lt;div class="section" id="pythons-built-in-profilers"&gt;
&lt;h2&gt;Python’s built-in profilers&lt;a class="headerlink" href="#pythons-built-in-profilers" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It’s also a great time for profiling in Python, as version 3.15 (expected October 2026) will introduce a new built-in profiler, bringing the total to three.
Here’s a quick list explaining their history…&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;a class="reference external" href="https://docs.python.org/dev/library/profile.html"&gt;&lt;code class="docutils literal"&gt;profile&lt;/code&gt;&lt;/a&gt; - introduced by Guido van Rossum in 1992 (&lt;a class="reference external" href="https://github.com/python/cpython/commit/8176258421df1e5fa8a521930178f6af8c93b52c"&gt;commit&lt;/a&gt;), this module provides a simple profiler for Python code, using a &lt;strong&gt;tracing&lt;/strong&gt; approach that records timings for every function call and return.
However, because it’s implemented purely in Python, it adds a heavy overhead which slows down profiling and skews results, and as such it’s deprecated for removal in Python 3.17.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;a class="reference external" href="https://docs.python.org/dev/library/profiling.tracing.html"&gt;&lt;code class="docutils literal"&gt;profiling.tracing&lt;/code&gt;&lt;/a&gt; (previously &lt;code class="docutils literal"&gt;cProfile&lt;/code&gt;) - this module was built as a C re-implementation of &lt;code class="docutils literal"&gt;profile&lt;/code&gt; by Armin Rigo in 2006 (&lt;a class="reference external" href="https://github.com/python/cpython/commit/a871ef2b3e924f058ec1b0aed7d4c83a546414b7"&gt;commit&lt;/a&gt;).
Being written in C, it has a much lower overhead than &lt;code class="docutils literal"&gt;profile&lt;/code&gt;, and it has served developers for years.
Still, it does make programs run ~50% slower, and since overhead is per-function, it can make oft-called functions look like more of a bottleneck than they really are.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;a class="reference external" href="https://docs.python.org/dev/library/profiling.sampling.html"&gt;&lt;code class="docutils literal"&gt;profiling.sampling&lt;/code&gt;&lt;/a&gt; (“Tachyon”) - new in Python 3.15, thanks to Pablo Galindo Salgado (&lt;a class="reference external" href="https://github.com/python/cpython/commit/59acdba820f75081cfb47ad6e71044d022854cbc"&gt;commit&lt;/a&gt;), this profiler uses a &lt;strong&gt;sampling&lt;/strong&gt; approach, which means it periodically interrupts the profiled program to record the current call stack.
This is a very low overhead approach, meaning programs can run at nearly full speed, and the skew from per-function overhead is gone.
For compatibility, it uses the same pstats file format as the other profilers, allowing profiling-explorer to work with it out of the box.
That said, it can also output several visualization formats, like flame graphs and heat maps, making it very versatile.&lt;/p&gt;
&lt;p&gt;I find Tachyon a very exciting addition to Python, and I cannot wait to use it more.
Previously, we’ve needed to turn to third-party sampling profilers, like &lt;a class="reference external" href="https://github.com/benfred/py-spy"&gt;py-spy&lt;/a&gt;, which is great but presents some difficulties, such as needing sudo permissions and ongoing maintenance to keep up with Python changes.
Having a “batteries included” sampling profiler will make it a lot easier to accurately trace programs.&lt;/p&gt;
&lt;p&gt;For more information, check out:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://docs.python.org/dev/library/profiling.html"&gt;the rewritten profiling documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://youtu.be/veigyI2oK7c?si=ejvUEl9gl9HTUDgq"&gt;The Core.py podcast episode&lt;/a&gt; where Pablo and Łukasz Langa discuss the design and implementation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="give-it-a-try"&gt;
&lt;h2&gt;Give it a try&lt;a class="headerlink" href="#give-it-a-try" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Future hype aside, profiling-explorer is useful whatever (supported) Python version you’re currently using.
To get started, first generate a pstats file for your program by running it under &lt;code class="docutils literal"&gt;profiling.tracing&lt;/code&gt;, under its old name &lt;code class="docutils literal"&gt;cProfile&lt;/code&gt;, with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;name&amp;gt;.pstats&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;program&amp;gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;&amp;lt;program&amp;gt;&lt;/code&gt; can be a Python file, or &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;-m&lt;/span&gt;&lt;/code&gt; with a module name, and further arguments are passed to the program.&lt;/p&gt;
&lt;p&gt;For example, to profile Django’s system check command, you can run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;check.pstats&lt;span class="w"&gt; &lt;/span&gt;manage.py&lt;span class="w"&gt; &lt;/span&gt;check
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or, to profile a set of tests under pytest, you can run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;tests.pstats&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;example/tests/test_rocket.py
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once you have a pstats file, pass it to &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;profiling-explorer&lt;/span&gt;&lt;/code&gt;.
If you use &lt;a class="reference external" href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt;, this is as easy as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;profiling-explorer&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;name&amp;gt;.pstats
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Otherwise, install &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;profiling-explorer&lt;/span&gt;&lt;/code&gt; and invoke it directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;profiling-explorer&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;name&amp;gt;.pstats
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;However you start it, you’ll see:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;profiling-explorer&lt;span class="w"&gt; &lt;/span&gt;migrations.profile
&lt;span class="go"&gt;profiling-explorer running at http://127.0.0.1:8099/&lt;/span&gt;
&lt;span class="go"&gt;Press CTRL+C to quit.&lt;/span&gt;
&lt;span class="go"&gt;Opening in web browser…&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…and your browser will open the interface automatically.&lt;/p&gt;
&lt;p&gt;Happy exploring, I hope you find some easy-to-fix bottlenecks!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Thanks again to Rippling for sponsoring this development.
Several of us are already using profiling-explorer on their giant Django code base to find useful optimizations.
If you’re looking for your next opportunity, and feel smart and ready to go after hard problems on day one, &lt;a class="reference external" href="https://www.rippling.com/careers/open-roles"&gt;check out Rippling’s open roles&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;May you always be improving,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/></entry><entry><title>Python: introducing icu4py, bindings to the Unicode ICU library</title><link href="https://adamj.eu/tech/2026/02/09/python-introducing-icu4py/" rel="alternate"/><published>2026-02-09T00:00:00+00:00</published><updated>2026-02-09T00:00:00+00:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-02-09:/tech/2026/02/09/python-introducing-icu4py/</id><summary type="html">&lt;p&gt;I made a new package!
Thank you to my client &lt;a class="reference external" href="https://rippling.com/"&gt;Rippling&lt;/a&gt; for inspiring and sponsoring its development.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://icu.unicode.org/"&gt;ICU (International Components for Unicode)&lt;/a&gt; is Unicode’s official library for Unicode and Globalization tools.
It’s a de-facto standard for handling text in a locale-aware way, &lt;a class="reference external" href="https://icu.unicode.org/#h.f9qwubthqabj"&gt;used by many major projects&lt;/a&gt;, including …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I made a new package!
Thank you to my client &lt;a class="reference external" href="https://rippling.com/"&gt;Rippling&lt;/a&gt; for inspiring and sponsoring its development.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://icu.unicode.org/"&gt;ICU (International Components for Unicode)&lt;/a&gt; is Unicode’s official library for Unicode and Globalization tools.
It’s a de-facto standard for handling text in a locale-aware way, &lt;a class="reference external" href="https://icu.unicode.org/#h.f9qwubthqabj"&gt;used by many major projects&lt;/a&gt;, including Chrome, Firefox, macOS, VS Code, and so on.
ICU comes in two flavours: ICU4C (for C/C++) and ICU4J (for Java).
(There’s also the newer &lt;a class="reference external" href="https://github.com/unicode-org/icu4x"&gt;ICU4X&lt;/a&gt;, built in Rust, but it’s a bit more of a work-in-progress.)&lt;/p&gt;
&lt;p&gt;My new package, &lt;a class="reference external" href="https://icu4py.readthedocs.io/en/latest/"&gt;icu4py&lt;/a&gt;, exposes some ICU4C functionality in Python, translating the C++ API into a more Pythonic one.
It currently supports boundary analysis (breaking text into words, sentences, etc.) and message formatting (using the ICU &lt;code class="docutils literal"&gt;MessageFormat&lt;/code&gt; syntax).
I’m open to adding more ICU features in the future, depending on demand.&lt;/p&gt;
&lt;div class="section" id="batteries-included"&gt;
&lt;h2&gt;Batteries included&lt;a class="headerlink" href="#batteries-included" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here’s a summary of the two features included in icu4py right now.&lt;/p&gt;
&lt;div class="section" id="boundary-analysis"&gt;
&lt;h3&gt;Boundary analysis&lt;a class="headerlink" href="#boundary-analysis" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For example, ICU supports &lt;a class="reference external" href="https://unicode-org.github.io/icu/userguide/boundaryanalysis/"&gt;text boundary analysis&lt;/a&gt;: finding linguistic boundaries in text, such as words or sentences, based on per-language rules.
These are useful for things like accurate word counts, or wrapping text for display.
icu4py exposes this feature through “breaker” classes.
For example, to split text into sentences using &lt;a class="reference external" href="https://icu4py.readthedocs.io/en/latest/api.html#icu4py.breakers.SentenceBreaker"&gt;&lt;code class="docutils literal"&gt;SentenceBreaker&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;icu4py.breakers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceBreaker&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;You asked &amp;quot;Why?&amp;quot;. We answered &amp;quot;Why not?&amp;quot;&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;breaker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SentenceBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;en_GB&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;breaker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;You asked &amp;quot;Why?&amp;quot;. &amp;#39;, &amp;#39;We answered &amp;quot;Why not?&amp;quot;&amp;#39;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note the quoted sentences are kept within their outer ones.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="brilliant-translation-with-messageformat"&gt;
&lt;h3&gt;Brilliant translation with &lt;code class="docutils literal"&gt;MessageFormat&lt;/code&gt;&lt;a class="headerlink" href="#brilliant-translation-with-messageformat" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;ICU also provides a flexible translation tool called &lt;a class="reference external" href="https://unicode-org.github.io/icu/userguide/format_parse/messages/"&gt;MessageFormat&lt;/a&gt;.
This allows translators to write patterns that capture the nuances of different languages, like varying pluralization rules.
icu4py exposes this API through its &lt;a class="reference external" href="https://icu4py.readthedocs.io/en/latest/api.html#icu4py.messageformat.MessageFormat"&gt;&lt;code class="docutils literal"&gt;MessageFormat&lt;/code&gt;&lt;/a&gt; class:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;icu4py.messageformat&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MessageFormat&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;{count,plural,one {# file} other {# files}}&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MessageFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;en_GB&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;0 files&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;1 file&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;5 files&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this case, our pluralization varies the output based on whether the &lt;code class="docutils literal"&gt;count&lt;/code&gt; variable is one or any other number.&lt;/p&gt;
&lt;p&gt;French has a subtle but important difference—it treats zero as singular, unlike English:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;{count,plural,one {# fichier} other {# fichiers}}&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MessageFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;fr_FR&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;0 fichier&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;1 fichier&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;2 fichiers&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This example shows an important point: the meaning of the &lt;code class="docutils literal"&gt;one&lt;/code&gt; category is defined by the locale.
In French, the “one” category matches both 0 and 1 because they both use the singular form.&lt;/p&gt;
&lt;p&gt;Korean has no plural distinction at all—the same form is used regardless of count:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;{count,plural,other {# 파일}}&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MessageFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;ko_KR&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;0 파일&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;1 파일&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;5 파일&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;ICU handles all these variations automatically based on the locale, so you don't need to know the rules for each language.&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;MessageFormat&lt;/code&gt; version 2 is in the works, complete with &lt;a class="reference external" href="https://messageformat.unicode.org/"&gt;its own site&lt;/a&gt;.
It’s in technical preview in ICU4C, and I aim to expose it in icu4py soon, tracked in &lt;a class="reference external" href="https://github.com/adamchainz/icu4py/issues/17"&gt;Issue #17&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="backstory"&gt;
&lt;h2&gt;Backstory&lt;a class="headerlink" href="#backstory" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Rippling is a global HR, Payroll, IT, and Finance platform, with over 20,000 customers all over the world.
They work hard to make their product available in many languages and locales.
Their Data Globalization team found that ICU Messageformat would present a great next step to handle complex localized messages in their product.
Since they use Python (and Django), they needed to pick a Python library to integrate ICU.&lt;/p&gt;
&lt;p&gt;This is where the challenge started.
In a Slack thread, we surveyed the existing options, and found them all lacking, especially on two points:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;None provided compiled wheels, a strong necessity for smooth installation among Rippling’s 1,000+ engineers.&lt;/li&gt;
&lt;li&gt;They all depend on a system-based install of ICU, which could present different versions between environments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In particular, we found these options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;a class="reference external" href="https://pypi.org/project/pyicu/"&gt;pyicu&lt;/a&gt; - impressively maintained since 2007, but poorly documented, has a clunky C++-style API, and lacks some key Python modernization.
For example, its C++ extensions don’t use multi-phase initialization, per &lt;a class="reference external" href="https://www.python.org/dev/peps/pep-0489/"&gt;PEP 489&lt;/a&gt;, which is required to support sub-interpreters, as Rippling may wish to adopt in the future.&lt;/p&gt;
&lt;p&gt;Additionally, while working on this project, its self-hosted Gitlab went down, and it hasn’t been restored for some weeks.
This does not bode well for long-term maintenance!&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;a class="reference external" href="https://pypi.org/project/icupy/"&gt;icupy&lt;/a&gt; - newer, modern, and better-maintained, but it still copies the C++ API directly, which is quite unwieldy.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;a class="reference external" href="https://pypi.org/project/pyseeyou/"&gt;pyseeyou&lt;/a&gt; - a pure-Python implementation of MessageFormat.
While this library is appealing in terms of ease of installation, it performs all parsing and formatting in Python, making it fairly slow.
Additionally, the package does not provide any locale-specific formatting.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After this survey, I opted to try my hand at creating a proof-of-concept package for calling the ICU4C’s C++ class &lt;code class="docutils literal"&gt;MessageFormat&lt;/code&gt; from Python.
Now, C++ is a beautifully haunted language, and I’m correspondingly uncomfortable with it.
While I have developed some Python C extensions before, like &lt;a class="reference external" href="https://pypi.org/project/time-machine/"&gt;time-machine&lt;/a&gt;, until this point I had not touched C++ since my university days.&lt;/p&gt;
&lt;p&gt;Thankfully, we are living in the LLM era and this kind of “glue two things together” task is something that they excel at.
Within a few hours, my mate Claude and I had a working prototype that could handle basic formatting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;icupoc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MessageFormat&lt;/span&gt;

&lt;span class="c1"&gt;# English plurals&lt;/span&gt;
&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;{count, plural, one {# file} other {# files}}&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MessageFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;en_US&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;quot;1 file&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;quot;5 files&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(You can still see this prototype at &lt;a class="reference external" href="https://github.com/adamchainz/icupoc"&gt;icupoc&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Happy with the PoC, Rippling gave me the go-ahead to build out a full ICU package.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="building"&gt;
&lt;h2&gt;Building&lt;a class="headerlink" href="#building" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I continued to use Claude when building out icu4py proper, and I guess I was &lt;a class="reference external" href="https://addyosmani.com/blog/agentic-engineering/"&gt;“agentic engineering”&lt;/a&gt; (or &lt;a class="reference external" href="https://simonwillison.net/2025/Oct/7/vibe-engineering/"&gt;“vibe engineering”&lt;/a&gt;).
While I used the LLM to generate code, I reviewed every line, making many edits before any step was ready.
I also deployed my usual stack of tools to format code, check types, build robust documentation, etc.&lt;/p&gt;
&lt;p&gt;Using an LLM essentially made this project feasible.
A few years ago, I would have shied away from writing in C++ and it would probably have taken me a lot longer to get running.
But instead of finding C++ big/scary, having many of the small details taken care of made the project fun!&lt;/p&gt;
&lt;p&gt;Things weren’t all smooth sailing, though.
Rippling have several “blessed” languages, including Rust, for which they have a monorepo with lots of tooling set up.
Before deciding to build the open source package, I tried to build an internal Rust-based version.
I made several different attempts to build Rust-to-C++ bindings, spending a silly amount of tokens and time going down rabbit holes to determine that various approaches don’t work or aren’t worth it.
The LLM’s confidence and sycophany led me to trying ideas for a long time before cutting my losses.
If I had known more about C++ or Rust, I think I wouldn’t have tried using Rust to begin with!&lt;/p&gt;
&lt;p&gt;I also learned that LLMs are really poor at using all of their embedded knowledge at once.
Even when writing the proof-of-concept, I prompted Claude to improve its own code and it came up with many changes (&lt;a class="reference external" href="https://github.com/adamchainz/icupoc/commit/84418919798a8e18719dc866dbadf1f7f7994803"&gt;commit&lt;/a&gt;).
That said, re-applying this trick more times made it adopt advanced C++ abstractions, completely unnecessarily.
It seems that to get high quality code, you need to loop an LLM around a few times on the same code while still using your judgement to know when to stop.&lt;/p&gt;
&lt;p&gt;Documentation was critical for getting good results.
I often copied sections or whole pages from Python or ICU’s documentation straight into the LLM chat.
For ICU’s auto-generated class documentation, I used &lt;a class="reference external" href="https://jina.ai/reader/"&gt;Jina Reader&lt;/a&gt; to convert its dense HTML to Markdown.
Docs are king, and even more so now.&lt;/p&gt;
&lt;p&gt;That said, using LLM-generated code did free up my time to write and edit the documentation more than I usually would.
I made sure to include sensible links to the relevant parts of ICU’s documentation, as well as great examples.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="icu4c-builds"&gt;
&lt;h2&gt;icu4c-builds&lt;a class="headerlink" href="#icu4c-builds" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A large part of building icu4py was compiling ICU4C itself.
It quickly became clear that I couldn’t build icu4py wheels that depended on the system ICU library because binding to C++ classes within a dynamic library is very tricky.
Therefore, I built a second repository, &lt;a class="reference external" href="https://github.com/adamchainz/icu4c-builds"&gt;icu4c-builds&lt;/a&gt;, which builds ICU4C in the exact setup I need for icu4py.&lt;/p&gt;
&lt;p&gt;Getting icu4c-builds working across Linux, macOS, and Windows and multiple architectures was a challenge with many long-running failed builds.
Claude was very useful here, though, since it has a lot of embedded knowledge around small details like compiler flags.
At some points, I was just shovelling logs between GitHub Actions and Claude, wondering why I was even there.&lt;/p&gt;
&lt;p&gt;Anyway, now the build system seems to be in a good place, and since ICU is pretty stable, the process is unlikely to break in the future.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="future-work"&gt;
&lt;h2&gt;Future work&lt;a class="headerlink" href="#future-work" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Right now, icu4py only has bindings to two ICU modules: boundary analysis and message formatting.
There are 40+ more to bind to, providing all kinds of features likely to be useful in globalizing Python apps.
If there are modules that you’d like available, please &lt;a class="reference external" href="https://github.com/adamchainz/icu4py/issues"&gt;open an issue&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;I’m pretty confident that with a solid foundation to copy from, LLMs can churn through building further bindings pretty quickly.
That said, I still think it’s key for icu4py’s success to design an API that makes sense in Python, for which I think human taste still prevails.&lt;/p&gt;
&lt;p&gt;On the Django side, I think there could be a place for writing an integration for icu4py with Django’s existing &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/topics/i18n/"&gt;internationalization and localization framework&lt;/a&gt;.
While there’s a lot of overlap, ICU has a lot of extra features that could be useful.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Please check out icu4py for your text boundary analysis and translation needs!&lt;/p&gt;
&lt;p&gt;Thanks again to Rippling for paying for this work. ⭐
Working with them is a fun challenge—even small changes can have a large impact, thanks to their massive scale.
If you’re looking for your next opportunity, and feel smart and ready to go after hard problems on day one, &lt;a class="reference external" href="https://www.rippling.com/careers/open-roles"&gt;check out Rippling’s open roles&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Peek-a-boo, I see you!&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/></entry><entry><title>Python: introducing tprof, a targeting profiler</title><link href="https://adamj.eu/tech/2026/01/14/python-introducing-tprof/" rel="alternate"/><published>2026-01-14T00:00:00+00:00</published><updated>2026-01-14T00:00:00+00:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-01-14:/tech/2026/01/14/python-introducing-tprof/</id><summary type="html">&lt;p&gt;Profilers measure the performance of a whole program to identify where most of the time is spent.
But once you’ve found a target function, re-profiling the whole program to see if your changes helped can be slow and cumbersome.
The profiler introduces overhead to execution and you have to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Profilers measure the performance of a whole program to identify where most of the time is spent.
But once you’ve found a target function, re-profiling the whole program to see if your changes helped can be slow and cumbersome.
The profiler introduces overhead to execution and you have to pick out the stats for the one function you care about from the report.
I have often gone through this loop while optimizing client or open source projects, such as when I optimized Django’s system checks framework (&lt;a class="reference external" href="/tech/2024/03/23/django-optimizing-system-checks/"&gt;previous post&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The pain here inspired me to create &lt;a class="reference external" href="https://pypi.org/project/tprof/"&gt;&lt;strong&gt;tprof&lt;/strong&gt;&lt;/a&gt;, a targeting profiler for Python 3.12+ that only measures the time spent in specified target functions.
Use it to measure your program before and after an optimization to see if it made any difference, with a quick report on the command line.&lt;/p&gt;
&lt;p&gt;For example, say you’ve realized that creating &lt;a class="reference external" href="https://docs.python.org/3/library/pathlib.html#pathlib.Path"&gt;&lt;code class="docutils literal"&gt;pathlib.Path&lt;/code&gt;&lt;/a&gt; objects is the bottleneck for your code.
You could run tprof like so:&lt;/p&gt;
&lt;img alt="tprof in action measuring pathlib.Path performance." src="/tech/assets/2026-01-14-tprof-screenshot.webp" /&gt;
&lt;div class="section" id="benchmark-with-comparison-mode"&gt;
&lt;h2&gt;Benchmark with comparison mode&lt;a class="headerlink" href="#benchmark-with-comparison-mode" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Sometimes when optimizing code, you want to compare several functions, such as “before” and “after” versions of a function you’re optimizing.
tprof supports this with its comparison mode, which adds a “delta” column to the report showing how much faster or slower each function is compared to a baseline.&lt;/p&gt;
&lt;p&gt;For example, given this code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…you can run tprof like this to compare the two functions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;tprof&lt;span class="w"&gt; &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;before&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;after&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;example
&lt;span class="go"&gt;🎯 tprof results:&lt;/span&gt;
&lt;span class="go"&gt; function         calls total  mean ± σ      min … max   delta&lt;/span&gt;
&lt;span class="go"&gt; example:before()   100 227ms   2ms ± 34μs   2ms … 2ms   -&lt;/span&gt;
&lt;span class="go"&gt; example:after()    100  86ms 856μs ± 15μs 835μs … 910μs -62.27%&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The output shows that &lt;code class="docutils literal"&gt;after()&lt;/code&gt; is about 60% faster than &lt;code class="docutils literal"&gt;before()&lt;/code&gt;, in this case.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="python-api"&gt;
&lt;h2&gt;Python API&lt;a class="headerlink" href="#python-api" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;tprof also provides a Python API via a context manager / decorator, &lt;code class="docutils literal"&gt;tprof()&lt;/code&gt;.
Use it to profile functions within a specific block of code.&lt;/p&gt;
&lt;p&gt;For example, to recreate the previous benchmarking example within a self-contained Python file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;tprof&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tprof&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tprof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compare&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…which produces output like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;🎯 tprof results:&lt;/span&gt;
&lt;span class="go"&gt; function          calls total  mean ± σ      min … max delta&lt;/span&gt;
&lt;span class="go"&gt; __main__:before()   100 227ms   2ms ± 83μs   2ms … 3ms -&lt;/span&gt;
&lt;span class="go"&gt; __main__:after()    100  85ms 853μs ± 22μs 835μs … 1ms -62.35%&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="how-it-works"&gt;
&lt;h2&gt;How it works&lt;a class="headerlink" href="#how-it-works" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;tprof uses Python’s &lt;a class="reference external" href="https://docs.python.org/3/library/sys.html#sys.monitoring"&gt;&lt;code class="docutils literal"&gt;sys.monitoring&lt;/code&gt;&lt;/a&gt;, a new API introduced in Python 3.12 for triggering events when functions or lines of code execute.
&lt;code class="docutils literal"&gt;sys.monitoring&lt;/code&gt; allows tprof to register callbacks for only specific target functions, meaning it adds no overhead to the rest of the program.
Timing is done in C to further reduce overhead.&lt;/p&gt;
&lt;p&gt;Thanks to Mark Shannon for contributing sys.monitoring to CPython!
This is the second time I’ve used it—the first time was for tracking down an unexpected mutation (see &lt;a class="reference external" href="/tech/2024/12/30/python-spy-changes-sys-monitoring/"&gt;previous post&lt;/a&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If tprof sounds useful to you, please give it a try and let me know what you think!
Install &lt;a class="reference external" href="https://pypi.org/project/tprof/"&gt;&lt;strong&gt;tprof&lt;/strong&gt;&lt;/a&gt; from PyPI with your favourite package manager.&lt;/p&gt;
&lt;p&gt;May you hit your Q1 targets,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/></entry><entry><title>Python: fix SyntaxWarning: 'return' in a 'finally' block</title><link href="https://adamj.eu/tech/2025/08/29/python-fix-syntaxwarning-finally/" rel="alternate"/><published>2025-08-29T18:13:00+01:00</published><updated>2025-08-29T18:13:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2025-08-29:/tech/2025/08/29/python-fix-syntaxwarning-finally/</id><summary type="html">&lt;p&gt;Take this code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;d6&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Ignore issues in random number generation&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Fallback value chosen by a fair dice roll&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;


&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Your random number is:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d6&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run it on Python 3.14+, and you’ll see …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Take this code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;d6&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Ignore issues in random number generation&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Fallback value chosen by a fair dice roll&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;


&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Your random number is:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d6&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run it on Python 3.14+, and you’ll see:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3.14&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;/.../example.py:12: SyntaxWarning: &amp;#39;return&amp;#39; in a &amp;#39;finally&amp;#39; block&lt;/span&gt;
&lt;span class="go"&gt;  return 4&lt;/span&gt;
&lt;span class="go"&gt;Your random number is: 4&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Python reports a &lt;code class="docutils literal"&gt;SyntaxWarning&lt;/code&gt; that indicates the &lt;code class="docutils literal"&gt;return&lt;/code&gt; within the &lt;code class="docutils literal"&gt;finally&lt;/code&gt; is dubious, likely to be an error, and may be removed from Python in the future.&lt;/p&gt;
&lt;p&gt;And indeed, in this case, it is an error.
&lt;code class="docutils literal"&gt;d6()&lt;/code&gt; always returns its fallback value, &lt;code class="docutils literal"&gt;4&lt;/code&gt;, even though random number generation works.
This is because a &lt;code class="docutils literal"&gt;return&lt;/code&gt; in a &lt;code class="docutils literal"&gt;finally&lt;/code&gt; block overrides the earlier &lt;code class="docutils literal"&gt;return&lt;/code&gt; in the &lt;code class="docutils literal"&gt;try&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;This confusion is why the &lt;code class="docutils literal"&gt;SyntaxWarning&lt;/code&gt; was introduced, through &lt;a class="reference external" href="https://peps.python.org/pep-0765/"&gt;PEP 765&lt;/a&gt;.
The PEP introduced the warning for three statements within &lt;code class="docutils literal"&gt;finally&lt;/code&gt; blocks:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;code class="docutils literal"&gt;return&lt;/code&gt;, as above.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;code class="docutils literal"&gt;break&lt;/code&gt;, as in:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;…which warns like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3.14&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;/.../example.py:5: SyntaxWarning: &amp;#39;break&amp;#39; in a &amp;#39;finally&amp;#39; block&lt;/span&gt;
&lt;span class="go"&gt;  break&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;code class="docutils literal"&gt;continue&lt;/code&gt;, as in:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…which warns like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3.14&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;/.../example.py:5: SyntaxWarning: &amp;#39;continue&amp;#39; in a &amp;#39;finally&amp;#39; block&lt;/span&gt;
&lt;span class="go"&gt;  continue&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To fix the warning, rewrite your code to avoid using &lt;code class="docutils literal"&gt;return&lt;/code&gt;, &lt;code class="docutils literal"&gt;break&lt;/code&gt;, or &lt;code class="docutils literal"&gt;continue&lt;/code&gt; within a &lt;code class="docutils literal"&gt;finally&lt;/code&gt; block.
This may mean removing the statement, or the &lt;code class="docutils literal"&gt;finally&lt;/code&gt; block entirely, or restructuring your code to avoid the need for it.
For example, in the &lt;code class="docutils literal"&gt;d6()&lt;/code&gt; function above, we could move the &lt;code class="docutils literal"&gt;return 4&lt;/code&gt; within the &lt;code class="docutils literal"&gt;except&lt;/code&gt; block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;d6&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Ignore issues in random number generation&lt;/span&gt;
        &lt;span class="c1"&gt;# Fallback value chosen by a fair dice roll&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Even better, we could remove the &lt;code class="docutils literal"&gt;try/except&lt;/code&gt; entirely, since &lt;code class="docutils literal"&gt;random.randint()&lt;/code&gt; is unlikely to raise an exception in normal use:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;d6&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We are finally free of this confusing pattern!&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/></entry><entry><title>Python: capture stdout and stderr in unittest</title><link href="https://adamj.eu/tech/2025/08/29/python-unittest-capture-stdout-stderr/" rel="alternate"/><published>2025-08-29T16:49:00+01:00</published><updated>2025-08-29T16:49:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2025-08-29:/tech/2025/08/29/python-unittest-capture-stdout-stderr/</id><summary type="html">&lt;p&gt;When testing code that outputs to the terminal through either standard out (stdout) or standard error (stderr), you might want to capture that output and make assertions on it.
To do so, use &lt;a class="reference external" href="https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout"&gt;&lt;code class="docutils literal"&gt;contextlib.redirect_stdout()&lt;/code&gt;&lt;/a&gt; and &lt;a class="reference external" href="https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stderr"&gt;&lt;code class="docutils literal"&gt;contextlib.redirect_stderr()&lt;/code&gt;&lt;/a&gt; to redirect the respective output streams to in-memory buffers that you can …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When testing code that outputs to the terminal through either standard out (stdout) or standard error (stderr), you might want to capture that output and make assertions on it.
To do so, use &lt;a class="reference external" href="https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout"&gt;&lt;code class="docutils literal"&gt;contextlib.redirect_stdout()&lt;/code&gt;&lt;/a&gt; and &lt;a class="reference external" href="https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stderr"&gt;&lt;code class="docutils literal"&gt;contextlib.redirect_stderr()&lt;/code&gt;&lt;/a&gt; to redirect the respective output streams to in-memory buffers that you can then inspect and assert on.&lt;/p&gt;
&lt;p&gt;For example, say we wanted to test this function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Alternate the case of each letter&lt;/span&gt;
    &lt;span class="n"&gt;sillified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sillified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We could write a test using &lt;code class="docutils literal"&gt;redirect_stdout()&lt;/code&gt; like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redirect_stdout&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;io&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StringIO&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;print_silly&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PrintSillyTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;redirect_stdout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;What a lovely day!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;WhAt a lOvElY DaY!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;redirect_stdout()&lt;/code&gt; takes a file-like object to which it will redirect output, and then returns that object.
That allows us to use a &lt;a class="reference external" href="https://docs.python.org/3/library/io.html#io.StringIO"&gt;&lt;code class="docutils literal"&gt;StringIO()&lt;/code&gt;&lt;/a&gt; as an in-memory buffer to capture the output, which we can call &lt;code class="docutils literal"&gt;getvalue()&lt;/code&gt; on to retrieve the captured text.&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;redirect_stderr()&lt;/code&gt; works the same way, but for stderr.&lt;/p&gt;
&lt;div class="section" id="simplify-capturing-streams"&gt;
&lt;h2&gt;Simplify capturing streams&lt;a class="headerlink" href="#simplify-capturing-streams" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To simplify capturing both stdout and stderr, you can copy in this extra decorator/context manager made with &lt;a class="reference external" href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager"&gt;&lt;code class="docutils literal"&gt;contextlib.contextmanager()&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirect_stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirect_stdout&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;io&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StringIO&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextIO&lt;/span&gt;


&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;capture_output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TextIO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextIO&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Capture both stdout and stderr into StringIO buffers.&lt;/span&gt;

&lt;span class="sd"&gt;    Source: https://adamj.eu/tech/2025/08/29/python-unittest-capture-stdout-stderr/&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;redirect_stdout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;redirect_stderr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Use it like so (here it has been placed in a &lt;code class="docutils literal"&gt;testing&lt;/code&gt; module):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;print_silly&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;testing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PrintSillyTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;What a lovely day!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;WhAt a lOvElY DaY!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I like this pattern as it’s clear without being too long.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="assert-on-log-messages"&gt;
&lt;h2&gt;Assert on log messages&lt;a class="headerlink" href="#assert-on-log-messages" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If the output you’re checking comes through the standard library’s &lt;a class="reference external" href="https://docs.python.org/3/library/logging.html"&gt;&lt;code class="docutils literal"&gt;logging&lt;/code&gt;&lt;/a&gt; module, rather than directly through &lt;code class="docutils literal"&gt;print()&lt;/code&gt;, it’s better to use &lt;a class="reference external" href="https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertLogs"&gt;&lt;code class="docutils literal"&gt;TestCase.assertLogs()&lt;/code&gt;&lt;/a&gt; context manager instead of redirecting stdout or stderr.
For example, if we had this function that logged a message:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sillified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sillified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We could test it like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;log_silly&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LogSillyTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_log_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLogs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;INFO&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;log_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Max. My Name&amp;#39;s Max. That&amp;#39;s My Name.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;INFO:example:MaX. mY NaMe&amp;#39;s mAx. ThAt&amp;#39;s mY NaMe.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="in-pytest"&gt;
&lt;h2&gt;In pytest&lt;a class="headerlink" href="#in-pytest" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you use &lt;a class="reference external" href="https://docs.pytest.org/en/stable/"&gt;pytest&lt;/a&gt; to run your tests, the above pattern will work, but you may also wish to use its built-in &lt;a class="reference external" href="https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html#accessing-captured-output-from-a-test-function"&gt;&lt;code class="docutils literal"&gt;capsys&lt;/code&gt; fixture&lt;/a&gt;.
Within a unittest class, you can use it with a custom fixture in your test class that attaches the &lt;code class="docutils literal"&gt;capsys&lt;/code&gt; fixture to the &lt;code class="docutils literal"&gt;TestCase&lt;/code&gt; instance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pytest&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;print_silly&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PrintSillyTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capsys&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capsys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capsys&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;What a lovely day!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;captured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capsys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readouterr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;WhAt a lOvElY DaY!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Otherwise, within regular pytest tests, in functions or plain classes, you can use the &lt;code class="docutils literal"&gt;capsys&lt;/code&gt; fixture directly as a function argument:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;print_silly&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capsys&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;print_silly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;What a lovely day!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;captured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capsys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readouterr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;WhAt a lOvElY DaY!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="avoid-the-tools-in-test-support"&gt;
&lt;h2&gt;Avoid the tools in &lt;code class="docutils literal"&gt;test.support&lt;/code&gt;&lt;a class="headerlink" href="#avoid-the-tools-in-test-support" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you search Python’s documentation or other sources, you may find &lt;a class="reference external" href="https://docs.python.org/3/library/test.html#test.support.captured_stdout"&gt;&lt;code class="docutils literal"&gt;test.support.captured_stdout()&lt;/code&gt;&lt;/a&gt; and the similar &lt;code class="docutils literal"&gt;captured_stderr()&lt;/code&gt;.
These work like wrappers around &lt;code class="docutils literal"&gt;redirect_stdout()&lt;/code&gt; and &lt;code class="docutils literal"&gt;redirect_stderr()&lt;/code&gt;, creating the &lt;code class="docutils literal"&gt;StringIO&lt;/code&gt; buffers for you:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;test.support&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;captured_stdout&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;captured_stdout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello, World!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Hello, World!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;However, you should avoid using these tools in your own code.
The &lt;code class="docutils literal"&gt;test.support&lt;/code&gt; module is an internal part of Python’s own test suite, with no guarantees of stability or backwards compatibility.
Additionally, it may not be available in all Python distributions or implementations—for example, it’s not available in the Python distributions from &lt;a class="reference external" href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;May you capture all the output that you expect,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/><category term="unittest"/></entry><entry><title>Python: check a package version with importlib.metadata.version()</title><link href="https://adamj.eu/tech/2025/07/30/python-check-package-version-importlib-metadata-version/" rel="alternate"/><published>2025-07-30T18:33:00+01:00</published><updated>2025-07-30T18:33:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2025-07-30:/tech/2025/07/30/python-check-package-version-importlib-metadata-version/</id><summary type="html">&lt;p&gt;Sometimes, it’s useful to branch the behaviour of your code based on the version of a package that you have installed.
This may be to support an upgrade in your project, or for your own package to support different versions of a dependency.&lt;/p&gt;
&lt;p&gt;Some Python packages provide a top-level …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Sometimes, it’s useful to branch the behaviour of your code based on the version of a package that you have installed.
This may be to support an upgrade in your project, or for your own package to support different versions of a dependency.&lt;/p&gt;
&lt;p&gt;Some Python packages provide a top-level version attribute that gives the version number as a tuple.
This is often called &lt;code class="docutils literal"&gt;__version__&lt;/code&gt;, though in the case of Django, it is called &lt;code class="docutils literal"&gt;django.VERSION&lt;/code&gt;, which can be used like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;django&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERSION&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Use new features in Django 5.2&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Do some backport&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Such version attributes are not a standard though, so not all packages have them.
They require extra work from package maintainers to provide: for example, setuptools requires extra &lt;a class="reference external" href="https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html#dynamic-metadata"&gt;dynamic metadata configuration&lt;/a&gt;.
And in the worst cases, a &lt;code class="docutils literal"&gt;__version__&lt;/code&gt; attribute may have been forgotten about by new package maintainers, leaving it out of sync with the actual package version.&lt;/p&gt;
&lt;p&gt;Instead of using such attributes, you can instead use &lt;a class="reference external" href="https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.version"&gt;&lt;code class="docutils literal"&gt;importlib.metadata.version()&lt;/code&gt;&lt;/a&gt;, added in Python 3.8, to get a package version as a string:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;importlib.metadata&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;

&lt;span class="n"&gt;In&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;django&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;5.2.4&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is available for all installed packages as it reads the metadata files left by the package installer.&lt;/p&gt;
&lt;p&gt;For example, the previous example can be rewritten like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;importlib.metadata&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;django&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;5.2.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Use new features in Django 5.2&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Do some backport&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It can be a bit annoying to work with strings, though.
In this case, you can install &lt;a class="reference external" href="https://packaging.pypa.io/en/stable/"&gt;packaging&lt;/a&gt; and use its &lt;a class="reference external" href="https://packaging.pypa.io/en/stable/version.html"&gt;&lt;code class="docutils literal"&gt;packaging.version.Version&lt;/code&gt; class&lt;/a&gt; to parse the string into a &lt;code class="docutils literal"&gt;Version&lt;/code&gt; object:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;importlib.metadata&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;

&lt;span class="n"&gt;In&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;packaging.version&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;

&lt;span class="n"&gt;In&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;django&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;Out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;5.2.4&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;Version&lt;/code&gt; objects can be compared with each other, so you can use them like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;importlib.metadata&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;packaging.version&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;django&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;5.2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Use new features in Django 5.2&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Do some backport&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;May you never need package-version-branching, and when you do, may it be short-lived.&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/></entry><entry><title>Python: fix BrokenPipeError when piping output to other commands</title><link href="https://adamj.eu/tech/2025/07/20/python-fix-brokenpipeerror/" rel="alternate"/><published>2025-07-20T00:00:00+01:00</published><updated>2025-07-20T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2025-07-20:/tech/2025/07/20/python-fix-brokenpipeerror/</id><summary type="html">&lt;p&gt;If you’ve written a Python script that outputs a lot of data, then piped that output into another command that only reads part of it, you might have encountered a &lt;code class="docutils literal"&gt;BrokenPipeError&lt;/code&gt;.
For example, take this script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;This is line &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When piped …&lt;/p&gt;</summary><content type="html">&lt;p&gt;If you’ve written a Python script that outputs a lot of data, then piped that output into another command that only reads part of it, you might have encountered a &lt;code class="docutils literal"&gt;BrokenPipeError&lt;/code&gt;.
For example, take this script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;This is line &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When piped into &lt;code class="docutils literal"&gt;head &lt;span class="pre"&gt;-n3&lt;/span&gt;&lt;/code&gt;, which only reads the first 10 lines, it raises a &lt;code class="docutils literal"&gt;BrokenPipeError&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;example.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n3
&lt;span class="go"&gt;This is line 0&lt;/span&gt;
&lt;span class="go"&gt;This is line 1&lt;/span&gt;
&lt;span class="go"&gt;This is line 2&lt;/span&gt;
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  File &amp;quot;/.../example.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;    print(f&amp;quot;This is line {i} &amp;quot;)&lt;/span&gt;
&lt;span class="go"&gt;    ~~~~~^^^^^^^^^^^^^^^^^^^^^^&lt;/span&gt;
&lt;span class="go"&gt;BrokenPipeError: [Errno 32] Broken pipe&lt;/span&gt;
&lt;span class="go"&gt;Exception ignored on flushing sys.stdout:&lt;/span&gt;
&lt;span class="go"&gt;BrokenPipeError: [Errno 32] Broken pipe&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This happens because &lt;code class="docutils literal"&gt;head&lt;/code&gt; stops reading its input after the first 10 lines and closes its input stream, which is the pipe from Python.
The operating system sends a &lt;code class="docutils literal"&gt;SIGPIPE&lt;/code&gt; signal to the Python process, indicating that it can no longer write to the pipe because it’s closed.
Python translates this signal into a &lt;code class="docutils literal"&gt;BrokenPipeError&lt;/code&gt; when it tries to write the next line to the closed pipe.&lt;/p&gt;
&lt;p&gt;The exception occurs a second time as well, when Python shuts down, as it tries to flush the output stream.
That’s reported in the final two lines, where the exception recurred but is ignored because it occurs during deletion of the &lt;code class="docutils literal"&gt;sys.stdout&lt;/code&gt; object at shutdown.&lt;/p&gt;
&lt;p&gt;To fix these &lt;code class="docutils literal"&gt;BrokenPipeError&lt;/code&gt;s, the Python documentation &lt;a class="reference external" href="https://docs.python.org/3/library/signal.html#note-on-sigpipe"&gt;has a recommendation&lt;/a&gt;.
Applying this to our example, we get:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;This is line &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;BrokenPipeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Python flushes standard streams on exit; redirect remaining output&lt;/span&gt;
    &lt;span class="c1"&gt;# to devnull to avoid another BrokenPipeError at shutdown&lt;/span&gt;
    &lt;span class="n"&gt;devnull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;devnull&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devnull&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fileno&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here’s what’s new:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;The output loop is wrapped in a &lt;code class="docutils literal"&gt;try&lt;/code&gt; / &lt;code class="docutils literal"&gt;except PipeError&lt;/code&gt; block.&lt;/li&gt;
&lt;li&gt;The &lt;code class="docutils literal"&gt;try&lt;/code&gt; branch now flushes &lt;code class="docutils literal"&gt;sys.stdout&lt;/code&gt;, to trigger any &lt;code class="docutils literal"&gt;BrokenPipeError&lt;/code&gt;s that might occur during the loop.&lt;/li&gt;
&lt;li&gt;The &lt;code class="docutils literal"&gt;except&lt;/code&gt; branch handles the &lt;code class="docutils literal"&gt;BrokenPipeError&lt;/code&gt; by redirecting the remaining output to &lt;code class="docutils literal"&gt;/dev/null&lt;/code&gt;.
It does this by opening the &lt;code class="docutils literal"&gt;os.devnull&lt;/code&gt; special file (&lt;code class="docutils literal"&gt;/dev/null&lt;/code&gt; on Unix-like systems, or &lt;code class="docutils literal"&gt;NUL&lt;/code&gt; on Windows) for writing, and then using the arcane system call &lt;code class="docutils literal"&gt;os.dup2()&lt;/code&gt; to redirect the standard output stream to this file descriptor.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Python’s documentation also suggests &lt;code class="docutils literal"&gt;sys.exit(1)&lt;/code&gt; in the &lt;code class="docutils literal"&gt;except&lt;/code&gt; block, but I didn’t include that because I think it’s unnecessary.
Exiting with a non-zero exit code indicates an error, but there’s not really an error when stopping early.
Many programs use a zero exit code when finishing early (&lt;code class="docutils literal"&gt;cat&lt;/code&gt;, &lt;code class="docutils literal"&gt;grep&lt;/code&gt;, &lt;code class="docutils literal"&gt;rg&lt;/code&gt;) while others use exit code 141 (like &lt;code class="docutils literal"&gt;yes&lt;/code&gt; or &lt;code class="docutils literal"&gt;git log&lt;/code&gt;)
It doesn’t seem common to use exit code 1, though.&lt;/p&gt;
&lt;p&gt;After these changes, the example exits cleanly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;example.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n3
&lt;span class="go"&gt;This is line 0&lt;/span&gt;
&lt;span class="go"&gt;This is line 1&lt;/span&gt;
&lt;span class="go"&gt;This is line 2&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…and we can check the exit code using the &lt;code class="docutils literal"&gt;pipestatus&lt;/code&gt; variable in Bash/Zsh:&lt;/p&gt;
&lt;blockquote&gt;
$ echo $pipestatus
0 0&lt;/blockquote&gt;
&lt;p&gt;This variable lists the exit codes of all commands in the last one, and here they’re both 0.&lt;/p&gt;
&lt;div class="section" id="as-a-context-manager"&gt;
&lt;h2&gt;As a context manager&lt;a class="headerlink" href="#as-a-context-manager" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It’s a bit fiddly to wrap the output section of your script in such a &lt;code class="docutils literal"&gt;try&lt;/code&gt; / &lt;code class="docutils literal"&gt;except&lt;/code&gt; block.
Here‘s a version that bundles the logic into a context manager:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt;


&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_broken_pipe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Prevent BrokenPipeError when the output stream is closed early, such as&lt;/span&gt;
&lt;span class="sd"&gt;    when piping to head.&lt;/span&gt;

&lt;span class="sd"&gt;    https://adamj.eu/tech/2025/07/20/python-fix-brokenpipeerror/&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;BrokenPipeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Python flushes standard streams on exit; redirect remaining output&lt;/span&gt;
        &lt;span class="c1"&gt;# to devnull to avoid another BrokenPipeError at shutdown&lt;/span&gt;
        &lt;span class="n"&gt;devnull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;devnull&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devnull&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fileno&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Use it like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;handle_broken_pipe&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;This is line &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I like this approach because it separates the error handling into a neat package.
Also, if you put the context manager on your outer &lt;code class="docutils literal"&gt;main()&lt;/code&gt; function, you can scatter output statements throughout your code without worrying about &lt;code class="docutils literal"&gt;BrokenPipeError&lt;/code&gt;s.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-about-stderr"&gt;
&lt;h2&gt;What about &lt;code class="docutils literal"&gt;stderr&lt;/code&gt;?&lt;a class="headerlink" href="#what-about-stderr" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The above code only handles &lt;code class="docutils literal"&gt;stdout&lt;/code&gt; being closed.
It’s also possible, though less common, to pipe &lt;code class="docutils literal"&gt;stderr&lt;/code&gt; to a process that closes it early.
For example, we can modify our example to write to &lt;code class="docutils literal"&gt;stderr&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;This is line &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With some Zsh redirection, we can pipe &lt;code class="docutils literal"&gt;stderr&lt;/code&gt; to &lt;code class="docutils literal"&gt;head&lt;/code&gt; like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;example.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="o"&gt;(&lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n3&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;This is line 0&lt;/span&gt;
&lt;span class="go"&gt;This is line 1&lt;/span&gt;
&lt;span class="go"&gt;This is line 2&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This time, there’s no exception displayed because Python would print it to &lt;code class="docutils literal"&gt;stderr&lt;/code&gt;, but that stream has been closed early.
Still, we can see a failure exit code:&lt;/p&gt;
&lt;blockquote&gt;
$ echo $?
120&lt;/blockquote&gt;
&lt;p&gt;It’s possible to adapt the previous example to handle &lt;code class="docutils literal"&gt;stderr&lt;/code&gt; as well, but I am not sure if it’s a great idea.
&lt;code class="docutils literal"&gt;stderr&lt;/code&gt; being closed early is less of a “normal” case, as it prevents normal error reporting from working, so perhaps it’s better to exit with a non-zero status code (at least).&lt;/p&gt;
&lt;p&gt;(I’m not sure where the value 120 comes from.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;May your only broken pipes be virtual!&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/></entry><entry><title>Python: sharing common tests in unittest</title><link href="https://adamj.eu/tech/2025/05/30/python-unittest-common-tests/" rel="alternate"/><published>2025-05-30T00:00:00+01:00</published><updated>2025-05-30T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2025-05-30:/tech/2025/05/30/python-unittest-common-tests/</id><summary type="html">&lt;p&gt;A neat testing pattern is writing common tests in a base class and then applying them to multiple objects through subclassing.
Doing so can help you test smarter and cover more code with less boilerplate.&lt;/p&gt;
&lt;p&gt;unittest doesn’t have a built-in way to define a base class of tests that …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A neat testing pattern is writing common tests in a base class and then applying them to multiple objects through subclassing.
Doing so can help you test smarter and cover more code with less boilerplate.&lt;/p&gt;
&lt;p&gt;unittest doesn’t have a built-in way to define a base class of tests that should only be run when subclassed, but there a few ways to achieve it.
We’ll explore a couple of approaches here.&lt;/p&gt;
&lt;div class="section" id="example-code"&gt;
&lt;h2&gt;Example code&lt;a class="headerlink" href="#example-code" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In all the examples below, we’ll be testing these two classes which have a common interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Armadillo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Hrrr!&amp;quot;&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Okapi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Gronk!&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="with-a-deleted-base-class"&gt;
&lt;h2&gt;With a deleted base class&lt;a class="headerlink" href="#with-a-deleted-base-class" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This approach creates a base class, uses it, and then hides the base class with &lt;code class="docutils literal"&gt;del&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Armadillo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Okapi&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;animal_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;  &lt;span class="c1"&gt;# To be defined in subclasses&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;animal_class&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertIsInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertGreater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArmadilloTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;animal_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Armadillo&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OkapiTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;animal_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Okapi&lt;/span&gt;


&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;BaseAnimalTests&lt;/span&gt;  &lt;span class="c1"&gt;# Hide base class from test discovery&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class="docutils literal"&gt;del&lt;/code&gt; is needed to prevent unittest from collecting and running &lt;code class="docutils literal"&gt;BaseAnimalTests&lt;/code&gt; itself.
Without it, unittest would run it, and it would fail because it does not define the required &lt;code class="docutils literal"&gt;animal_class&lt;/code&gt; attribute.&lt;/p&gt;
&lt;p&gt;This approach is my preferred one.
It might be a little surprising for readers to find a &lt;code class="docutils literal"&gt;del&lt;/code&gt;, but I think the comment is explanation enough.&lt;/p&gt;
&lt;p&gt;Running the tests, we see just the two concrete test classes executing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;unittest&lt;span class="w"&gt; &lt;/span&gt;-v
&lt;span class="go"&gt;test_speak (tests.ArmadilloTests.test_speak) ... ok&lt;/span&gt;
&lt;span class="go"&gt;test_speak (tests.OkapiTests.test_speak) ... ok&lt;/span&gt;

&lt;span class="go"&gt;----------------------------------------------------------------------&lt;/span&gt;
&lt;span class="go"&gt;Ran 2 tests in 0.000s&lt;/span&gt;

&lt;span class="go"&gt;OK&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="with-a-test-class-mixin"&gt;
&lt;h2&gt;With a test class mixin&lt;a class="headerlink" href="#with-a-test-class-mixin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This approach puts the common tests in a mixin class that does not inherit from &lt;code class="docutils literal"&gt;unittest.TestCase&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Armadillo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Okapi&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;animal_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;  &lt;span class="c1"&gt;# To be defined in subclasses&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;animal_class&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertIsInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertGreater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArmadilloTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;animal_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Armadillo&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OkapiTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;animal_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Okapi&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;No &lt;code class="docutils literal"&gt;del&lt;/code&gt; is needed here: unittest will not collect &lt;code class="docutils literal"&gt;BaseAnimalTests&lt;/code&gt; because it does not inherit from &lt;code class="docutils literal"&gt;unittest.TestCase&lt;/code&gt;.
But this approach has at least a couple of drawbacks:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;If any base class forgets to inherit from both &lt;code class="docutils literal"&gt;BaseAnimalTests&lt;/code&gt; and &lt;code class="docutils literal"&gt;unittest.TestCase&lt;/code&gt;, it will not be collected by unittest, and its tests will not run.
This may be hard to notice.
Even a defence like enforcing 100% coverage on your tests, as &lt;a class="reference external" href="https://nedbatchelder.com/blog/202008/you_should_include_your_tests_in_coverage.html"&gt;Ned Batchelder implores&lt;/a&gt; we use, may not help, since subclasses may not contain any tests of their own.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Type checkers will raise errors about undefined methods in the base class, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;mypy&lt;span class="w"&gt; &lt;/span&gt;--check-untyped-defs&lt;span class="w"&gt; &lt;/span&gt;tests.py
&lt;span class="go"&gt;tests.py:11: error: &amp;quot;BaseAnimalTests&amp;quot; has no attribute &amp;quot;assertIsInstance&amp;quot;  [attr-defined]&lt;/span&gt;
&lt;span class="go"&gt;tests.py:12: error: &amp;quot;BaseAnimalTests&amp;quot; has no attribute &amp;quot;assertGreater&amp;quot;  [attr-defined]&lt;/span&gt;
&lt;span class="go"&gt;Found 2 errors in 1 file (checked 1 source file)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I &lt;a class="reference external" href="/tech/2025/05/01/python-type-hints-mixin-classes/"&gt;previously blogged&lt;/a&gt; about writing type-checked mixin classes, with the conclusion being that they should inherit from their intended base class.
That means returning to the first approach, where the base class is a subclass of &lt;code class="docutils literal"&gt;unittest.TestCase&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="with-pytest-using-the-test-attribute"&gt;
&lt;h2&gt;With pytest: using the &lt;code class="docutils literal"&gt;__test__&lt;/code&gt; attribute&lt;a class="headerlink" href="#with-pytest-using-the-test-attribute" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you use &lt;a class="reference external" href="https://docs.pytest.org/en/stable/"&gt;pytest&lt;/a&gt; to run your unittest classes, you can use a &lt;code class="docutils literal"&gt;__test__&lt;/code&gt; attribute to prevent collection of a specific class:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Armadillo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Okapi&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__test__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# Hide from test discovery&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init_subclass__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__init_subclass__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__test__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# Enable test discovery for subclasses&lt;/span&gt;

    &lt;span class="n"&gt;animal_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;  &lt;span class="c1"&gt;# To be defined in subclasses&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;animal_class&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertIsInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertGreater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArmadilloTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;animal_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Armadillo&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OkapiTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseAnimalTests&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;animal_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Okapi&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;__test__&lt;/code&gt; controls whether pytest should collect a class, or not.
pytest respects it as a &lt;a class="reference external" href="https://docs.pytest.org/en/stable/example/pythoncollection.html#:~:text=__test__"&gt;lightly-documented feature&lt;/a&gt;, originally copied from the historical nose runner.
The approach above hides the base class but exposes the subclasses, through some automatic configuration in &lt;a class="reference external" href="https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__"&gt;&lt;code class="docutils literal"&gt;__init_subclass__&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Pytest collects and runs only the subclasses, as expected:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;-v
&lt;span class="go"&gt;===== test session starts ======&lt;/span&gt;
&lt;span class="go"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;collected 2 items&lt;/span&gt;

&lt;span class="go"&gt;test_example.py::ArmadilloTests::test_speak PASSED                                                                                                                                     [ 50%]&lt;/span&gt;
&lt;span class="go"&gt;test_example.py::OkapiTests::test_speak PASSED                                                                                                                                         [100%]&lt;/span&gt;

&lt;span class="go"&gt;====== 2 passed in 0.00s =======&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This approach is a little bit more complex than the previous two, and it only works with pytest.
Still it is nice that pytest gives you that control.
If you have a lot of common test classes, the technique could be wrapped up into a class decorator.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I think it would be neat if unittest gained some functionality here, like a &lt;code class="docutils literal"&gt;TestCase&lt;/code&gt; decorator to prevent collection of a specific class but not its subclasses.
Until that hypothetical future, though, I think &lt;code class="docutils literal"&gt;del&lt;/code&gt; is the way to go.&lt;/p&gt;
&lt;p&gt;May your common tests work towards the common good,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/><category term="unittest"/></entry><entry><title>Python: a quick cProfile recipe with pstats</title><link href="https://adamj.eu/tech/2025/05/20/python-quick-cprofile-recipe-pstats/" rel="alternate"/><published>2025-05-20T00:00:00+01:00</published><updated>2025-05-20T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2025-05-20:/tech/2025/05/20/python-quick-cprofile-recipe-pstats/</id><summary type="html">&lt;p&gt;Python comes with &lt;a class="reference external" href="https://docs.python.org/3/library/profile.html"&gt;two built-in profilers&lt;/a&gt; for measuring the performance of your code: &lt;strong&gt;cProfile&lt;/strong&gt; and &lt;strong&gt;profile&lt;/strong&gt;.
They have the same API, but &lt;strong&gt;cProfile&lt;/strong&gt; is a C extension, while &lt;strong&gt;profile&lt;/strong&gt; is implemented in Python.
You nearly always want to use &lt;strong&gt;cProfile&lt;/strong&gt;, as it’s faster and doesn’t skew measurements as …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Python comes with &lt;a class="reference external" href="https://docs.python.org/3/library/profile.html"&gt;two built-in profilers&lt;/a&gt; for measuring the performance of your code: &lt;strong&gt;cProfile&lt;/strong&gt; and &lt;strong&gt;profile&lt;/strong&gt;.
They have the same API, but &lt;strong&gt;cProfile&lt;/strong&gt; is a C extension, while &lt;strong&gt;profile&lt;/strong&gt; is implemented in Python.
You nearly always want to use &lt;strong&gt;cProfile&lt;/strong&gt;, as it’s faster and doesn’t skew measurements as much.&lt;/p&gt;
&lt;p&gt;By default, cProfile’s CLI profiles a command and displays its profile statistics afterwards.
But that can be a bit limited, especially for reading large profiles or re-sorting the same data in different ways.&lt;/p&gt;
&lt;p&gt;For more flexibility, cProfile can instead save the profile data to a file, which you can then read with the &lt;strong&gt;pstats&lt;/strong&gt; module.
This is my preferred way of using it, and this post covers a recipe for doing so, with a worked example.&lt;/p&gt;
&lt;div class="section" id="the-recipe"&gt;
&lt;h2&gt;The recipe&lt;a class="headerlink" href="#the-recipe" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;, profile your script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;profile&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;script&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;args&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Replace &lt;code class="docutils literal"&gt;&amp;lt;script&amp;gt;&lt;/code&gt; with the path to your Python file, and &lt;code class="docutils literal"&gt;[args]&lt;/code&gt; with any arguments you want to pass to it.
cProfile will run your script under its profiling machinery, saving the results to a file called &lt;code class="docutils literal"&gt;profile&lt;/code&gt;, as specified by the &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;-o&lt;/span&gt;&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt;, view the profile file using pstats:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pstats&lt;span class="w"&gt; &lt;/span&gt;profile&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;$&amp;#39;sort cumtime\nstats 1000&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;less
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The pstats CLI provides a REPL for interacting with profile files, based on its &lt;a class="reference external" href="https://docs.python.org/3/library/profile.html#the-stats-class"&gt;&lt;code class="docutils literal"&gt;Stats&lt;/code&gt; class&lt;/a&gt;.
The CLI is oddly undocumented, but its &lt;code class="docutils literal"&gt;help&lt;/code&gt; command lists the available commands.&lt;/p&gt;
&lt;p&gt;The above command passes several commands to pstats in a string.
The string uses the &lt;code class="docutils literal"&gt;$&lt;/code&gt; syntax, a Bash feature for C-style strings, allowing &lt;code class="docutils literal"&gt;\n&lt;/code&gt; to represent a newline, passing two commands:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;code class="docutils literal"&gt;sort cumtime&lt;/code&gt;: Sort the output by cumulative time, largest first.
This means the time spent in a function and all its callees.&lt;/li&gt;
&lt;li&gt;&lt;code class="docutils literal"&gt;stats 1000&lt;/code&gt;: Show the first 1,000 lines of the profile.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The output is passed to &lt;code class="docutils literal"&gt;less&lt;/code&gt;, a common pager, allowing you to scroll through the results.
Press &lt;code class="docutils literal"&gt;q&lt;/code&gt; to quit when you’re done!&lt;/p&gt;
&lt;div class="section" id="profile-a-module"&gt;
&lt;h3&gt;Profile a module&lt;a class="headerlink" href="#profile-a-module" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you’re running a module instead of a script, add &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;-m&lt;/span&gt;&lt;/code&gt; like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;profile&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;module&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;args&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Replace &lt;code class="docutils literal"&gt;&amp;lt;module&amp;gt;&lt;/code&gt; with the name of the module you want to profile, and &lt;code class="docutils literal"&gt;[args]&lt;/code&gt; with any arguments you want to pass to it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="multiple-profiles"&gt;
&lt;h3&gt;Multiple profiles&lt;a class="headerlink" href="#multiple-profiles" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you’re profiling code before and after, consider using different profile file names instead of just &lt;code class="docutils literal"&gt;profile&lt;/code&gt;.
For example, for checking the results of some optimization, I often use the names &lt;code class="docutils literal"&gt;before.profile&lt;/code&gt; and &lt;code class="docutils literal"&gt;after.profile&lt;/code&gt;, like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;before.profile&lt;span class="w"&gt; &lt;/span&gt;example.py

&lt;span class="gp"&gt;$ &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;switch&lt;span class="w"&gt; &lt;/span&gt;optimize_all_the_things

&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;after.profile&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="alternative-sort-orders"&gt;
&lt;h3&gt;Alternative sort orders&lt;a class="headerlink" href="#alternative-sort-orders" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To sort by other metrics, swap &lt;code class="docutils literal"&gt;cumtime&lt;/code&gt; in &lt;code class="docutils literal"&gt;sort cumtime&lt;/code&gt; for one of these values, per &lt;a class="reference external" href="https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats"&gt;the &lt;code class="docutils literal"&gt;Stats.sort_stats()&lt;/code&gt; documentation&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;code class="docutils literal"&gt;time&lt;/code&gt;: internal time—the time spent in the function itself, excluding calls to other functions.&lt;/p&gt;
&lt;p&gt;This is useful for finding the slowest functions in your code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;code class="docutils literal"&gt;calls&lt;/code&gt;: number of calls to the function.&lt;/p&gt;
&lt;p&gt;This is useful for finding functions that are called many times and may be candidates for optimization, such as caching.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="a-djangoey-example"&gt;
&lt;h2&gt;A Djangoey example&lt;a class="headerlink" href="#a-djangoey-example" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here’s a worked example showing how to apply this recipe to a Django management command.
Say you are testing a database migration locally:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;./manage.py&lt;span class="w"&gt; &lt;/span&gt;migrate&lt;span class="w"&gt; &lt;/span&gt;example&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0002&lt;/span&gt;
&lt;span class="go"&gt;Operations to perform:&lt;/span&gt;
&lt;span class="go"&gt;  Target specific migration: 0002_complexito, from example&lt;/span&gt;
&lt;span class="go"&gt;Running migrations:&lt;/span&gt;
&lt;span class="go"&gt;  Applying example.0002_complexito... OK&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;While it did pass, it was unexpectedly slow.
To profile it, you would first reverse the migration to reset your test database:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;./manage.py&lt;span class="w"&gt; &lt;/span&gt;migrate&lt;span class="w"&gt; &lt;/span&gt;example&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0001&lt;/span&gt;
&lt;span class="go"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then you could apply the recipe to profile the migration.&lt;/p&gt;
&lt;p&gt;First, stick the &lt;code class="docutils literal"&gt;cProfile&lt;/code&gt; command in front of the migration command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;profile&lt;span class="w"&gt; &lt;/span&gt;./manage.py&lt;span class="w"&gt; &lt;/span&gt;migrate&lt;span class="w"&gt; &lt;/span&gt;example&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0002&lt;/span&gt;
&lt;span class="go"&gt;Operations to perform:&lt;/span&gt;
&lt;span class="go"&gt;  Target specific migration: 0002_complexito, from example&lt;/span&gt;
&lt;span class="go"&gt;Running migrations:&lt;/span&gt;
&lt;span class="go"&gt;  Applying example.0002_complexito... OK&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, run the second &lt;code class="docutils literal"&gt;pstats&lt;/code&gt; command to view the results:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pstats&lt;span class="w"&gt; &lt;/span&gt;profile&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;$&amp;#39;sort cumtime\nstats 1000&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;less
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This opens &lt;code class="docutils literal"&gt;less&lt;/code&gt; with a long table, starting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Welcome to the profile statistics browser.
profile% profile% Mon May 19 23:52:37 2025    profile

         213287 function calls (206021 primitive calls) in 1.150 seconds

   Ordered by: cumulative time
   List reduced from 3576 to 1000 due to restriction &amp;lt;1000&amp;gt;

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   425/1    0.001    0.000    1.150    1.150 {built-in method builtins.exec}
       1    0.000    0.000    1.150    1.150 ./manage.py:1(&amp;lt;module&amp;gt;)
       1    0.000    0.000    1.150    1.150 ./manage.py:7(main)
       1    0.000    0.000    1.109    1.109 /.../django/core/management/__init__.py:439(execute_from_command_line)
   ...
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The header tells us how many function calls were made, how many were primitive calls, and how long the code took to run.
Then there’s the table of all function calls, limited to 1,000 entries.&lt;/p&gt;
&lt;p&gt;Since we’re sorting by &lt;code class="docutils literal"&gt;cumtime&lt;/code&gt;, cumulative time spent in each function, the first line shows the total time spent in all functions.
That &lt;code class="docutils literal"&gt;exec&lt;/code&gt; is cProfile running your code, and the later lines represent the top-level wrappers from Django.&lt;/p&gt;
&lt;p&gt;Generally, it’s best to find the first listed function within your code base.
In this profile, you would search for &lt;code class="docutils literal"&gt;``example/&lt;/code&gt; and find this entry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ncalls  tottime  percall  cumtime  percall filename:lineno(function)
...
    1    0.000    0.000    1.005    1.005 /.../example/migrations/0002_complexito.py:4(forward)
...
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One call to the &lt;code class="docutils literal"&gt;forward()&lt;/code&gt; function in the migration file took 1.005 seconds, nearly all of the 1.150 seconds total runtime.
That’s a bit suspicious!&lt;/p&gt;
&lt;p&gt;Right above that entry, you might also spot the time spent running queries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ncalls  tottime  percall  cumtime  percall filename:lineno(function)
...
   13    0.000    0.000    1.007    0.077 /.../django/db/backends/utils.py:78(execute)
   13    0.000    0.000    1.007    0.077 /.../django/db/backends/utils.py:88(_execute_with_wrappers)
   13    0.000    0.000    1.007    0.077 /.../django/db/backends/utils.py:94(_execute)
   13    0.000    0.000    1.007    0.077 /.../django/db/backends/sqlite3/base.py:354(execute)
   13    1.006    0.077    1.006    0.077 {function SQLiteCursorWrapper.execute at 0x1054f7f60}
...
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This stack of functions all show 13 calls, with a cumulative time of 1.007 or 1.006 seconds.
They represent Django’s database backend wrappers, which eventually pass the query to Python’s &lt;code class="docutils literal"&gt;SQLiteCursorWrapper.execute()&lt;/code&gt;, which is displayed differently because it’s implemented in C.&lt;/p&gt;
&lt;p&gt;So, we can tell that the migration ran 13 queries in total, and at least one of them was slow and ran by &lt;code class="docutils literal"&gt;forward()&lt;/code&gt;.
At this point, you might look at the source of &lt;code class="docutils literal"&gt;forward()&lt;/code&gt; to see if you can find the slow query.
But first, you might want to re-display the profile to show only the &lt;code class="docutils literal"&gt;forward()&lt;/code&gt; function and its callees (the functions it called), which might shed some light on what it was doing.&lt;/p&gt;
&lt;p&gt;To show only &lt;code class="docutils literal"&gt;forward()&lt;/code&gt; and its callees, you can use the pstats &lt;code class="docutils literal"&gt;callees&lt;/code&gt; command.
This takes a regular expression to match the function names you want to show:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pstats&lt;span class="w"&gt; &lt;/span&gt;profile&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;$&amp;#39;sort cumtime\ncallees \\bforward\\b&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;less
&lt;span class="go"&gt;Welcome to the profile statistics browser.&lt;/span&gt;
&lt;span class="go"&gt;profile% profile%    Ordered by: cumulative time&lt;/span&gt;
&lt;span class="go"&gt;   List reduced from 3576 to 1 due to restriction &amp;lt;&amp;#39;\\bforward\\b&amp;#39;&amp;gt;&lt;/span&gt;

&lt;span class="go"&gt;Function&lt;/span&gt;
&lt;span class="go"&gt;called...&lt;/span&gt;
&lt;span class="go"&gt;ncalls  tottime  cumtime&lt;/span&gt;
&lt;span class="go"&gt;/.../example/migrations/0002_complexito.py:4(forward)&lt;/span&gt;
&lt;span class="go"&gt;  -&amp;gt;       1    0.000    0.000  /.../django/db/backends/utils.py:41(__enter__)&lt;/span&gt;
&lt;span class="go"&gt;1    0.000    0.000  /.../django/db/backends/utils.py:44(__exit__)&lt;/span&gt;
&lt;span class="go"&gt;1    0.000    1.005  /.../django/db/backends/utils.py:78(execute)&lt;/span&gt;
&lt;span class="go"&gt;1    0.000    0.000  /.../django/utils/asyncio.py:15(inner)&lt;/span&gt;
&lt;span class="go"&gt;1    0.000    0.000  {method &amp;#39;create_function&amp;#39; of &amp;#39;sqlite3.Connection&amp;#39; objects}&lt;/span&gt;

&lt;span class="go"&gt;profile%&lt;/span&gt;
&lt;span class="go"&gt;Goodbye.&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(Output wrapped.)&lt;/p&gt;
&lt;p&gt;This has revealed:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;code class="docutils literal"&gt;forward()&lt;/code&gt; only calls &lt;code class="docutils literal"&gt;execute()&lt;/code&gt; once, so there’s only one slow query.&lt;/li&gt;
&lt;li&gt;There’s also a call to SQLite’s &lt;code class="docutils literal"&gt;create_function()&lt;/code&gt;.
It’s fast, rounding down to 0.000 seconds, but perhaps may be something to do with the slow query.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Okay, time to look at the source:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_editor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;

    &lt;span class="n"&gt;schema_editor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;sleep&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;schema_editor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;SELECT sleep(1)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ah, it’s a deliberate pause that I added to show you this example.
Well, that solves that mystery.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;May you cook up some great profiles with this recipe!&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="python"/><category term="django"/></entry><entry><title>pre-commit: install with uv</title><link href="https://adamj.eu/tech/2025/05/07/pre-commit-install-uv/" rel="alternate"/><published>2025-05-07T00:00:00+01:00</published><updated>2025-05-07T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2025-05-07:/tech/2025/05/07/pre-commit-install-uv/</id><summary type="html">&lt;p&gt;&lt;a class="reference external" href="https://pre-commit.com/"&gt;pre-commit&lt;/a&gt; is my favourite Git-integrated “run things on commit” tool.
It acts as a kind of package manager, installing tools as necessary from their Git repositories.
This makes it fairly easy to set up: all you need to install is pre-commit itself, and it takes things from there.&lt;/p&gt;
&lt;p&gt;That said …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a class="reference external" href="https://pre-commit.com/"&gt;pre-commit&lt;/a&gt; is my favourite Git-integrated “run things on commit” tool.
It acts as a kind of package manager, installing tools as necessary from their Git repositories.
This makes it fairly easy to set up: all you need to install is pre-commit itself, and it takes things from there.&lt;/p&gt;
&lt;p&gt;That said, pre-commit’s &lt;a class="reference external" href="https://pre-commit.com/#installation"&gt;install guide&lt;/a&gt; is not the friendliest, particularly to developers who don’t use Python.
The guide only covers installation with Python’s default installer tool, Pip, and the rather unconventional &lt;a class="reference external" href="https://docs.python.org/3/library/zipapp.html"&gt;zipapp&lt;/a&gt; alternative.
Both of these methods are a bit annoying as they require a working Python installation and virtual environment, entailing manual upgrades of those tools too.&lt;/p&gt;
&lt;p&gt;Enter &lt;a class="reference external" href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt; (pronounced “you-vee”), a new Python environment manager.
Since its release last year, it has made the Pythonsphere go wild, seeing massive adoption.
That’s because it simplifies a lot of workflows, including managing development tools like pre-commit.
Once you have uv, it can manage Python versions and virtual environments for you, swiftly and smoothly.&lt;/p&gt;
&lt;p&gt;I now recommend you install pre-commit using uv’s &lt;a class="reference external" href="https://docs.astral.sh/uv/concepts/tools/"&gt;tool mechanism&lt;/a&gt;, using this command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;tool&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;pre-commit&lt;span class="w"&gt; &lt;/span&gt;--with&lt;span class="w"&gt; &lt;/span&gt;pre-commit-uv
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Running it, you’ll see output describing the installation process:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;tool&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;pre-commit&lt;span class="w"&gt; &lt;/span&gt;--with&lt;span class="w"&gt; &lt;/span&gt;pre-commit-uv
&lt;span class="go"&gt;Resolved 11 packages in 1ms&lt;/span&gt;
&lt;span class="go"&gt;Installed 11 packages in 8ms&lt;/span&gt;
&lt;span class="go"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;Installed 1 executable: pre-commit&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will put the &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;pre-commit&lt;/span&gt;&lt;/code&gt; executable in &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;~/.local/bin&lt;/span&gt;&lt;/code&gt; or similar (per &lt;a class="reference external" href="https://docs.astral.sh/uv/concepts/tools/#the-bin-directory"&gt;the documentation&lt;/a&gt;).
You should then be able to run it from anywhere:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pre-commit&lt;span class="w"&gt; &lt;/span&gt;--version
&lt;span class="go"&gt;pre-commit 4.2.0 (pre-commit-uv=4.1.4, uv=0.7.2)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The install command also adds &lt;a class="reference external" href="https://pypi.org/project/pre-commit-uv/"&gt;pre-commit-uv&lt;/a&gt;, a plugin that patches pre-commit to use uv to install Python-based tools.
This drastically speeds up using Python-based hooks, a common use case.
(Unfortunately, it seems pre-commit itself won’t be adding uv support.)&lt;/p&gt;
&lt;p&gt;With pre-commit installed globally, you can now install its Git hook in relevant repositories per usual:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;myrepo

&lt;span class="gp"&gt;$ &lt;/span&gt;pre-commit&lt;span class="w"&gt; &lt;/span&gt;install
&lt;span class="go"&gt;pre-commit installed at .git/hooks/pre-commit&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ pre-commit run --all-files
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
[INFO] Using pre-commit with uv 0.7.2 via pre-commit-uv 4.1.4
check for added large files..............................................&lt;span class=" -Color -Color-BGGreen"&gt;Passed&lt;/span&gt;
check for merge conflicts................................................&lt;span class=" -Color -Color-BGGreen"&gt;Passed&lt;/span&gt;
trim trailing whitespace.................................................&lt;span class=" -Color -Color-BGGreen"&gt;Passed&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="section" id="upgrade-pre-commit"&gt;
&lt;h2&gt;Upgrade pre-commit&lt;a class="headerlink" href="#upgrade-pre-commit" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To upgrade pre-commit installed this way, run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;tool&lt;span class="w"&gt; &lt;/span&gt;upgrade&lt;span class="w"&gt; &lt;/span&gt;pre-commit
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;tool&lt;span class="w"&gt; &lt;/span&gt;upgrade&lt;span class="w"&gt; &lt;/span&gt;pre-commit
&lt;span class="go"&gt;Updated pre-commit v4.1.0 -&amp;gt; v4.2.0&lt;/span&gt;
&lt;span class="go"&gt; - pre-commit==4.1.0&lt;/span&gt;
&lt;span class="go"&gt; + pre-commit==4.2.0&lt;/span&gt;
&lt;span class="go"&gt;Installed 1 executable: pre-commit&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This command upgrades pre-commit and all of its dependencies, in its managed environment.
For more information, see &lt;a class="reference external" href="https://docs.astral.sh/uv/concepts/tools/#upgrading-tools"&gt;the uv tool upgrade documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fin"&gt;
&lt;h2&gt;Fin&lt;a class="headerlink" href="#fin" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;May you commit early and often,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="posts"/><category term="pre-commit"/><category term="python"/></entry></feed>