<?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-08-16T00:00:00+01:00</updated><entry><title>Python: use re.prefixmatch() instead of re.match() from Python 3.15</title><link href="https://adamj.eu/tech/2026/08/16/python-prefer-prefixmatch-to-match/" rel="alternate"/><published>2026-08-16T00:00:00+01:00</published><updated>2026-08-16T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-08-16:/tech/2026/08/16/python-prefer-prefixmatch-to-match/</id><summary type="html">&lt;p&gt;Take this validation function:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;TRAIN_NUMBER_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d&lt;/span&gt;&lt;span class="si"&gt;{6}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# six digits&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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="nb"&gt;bool&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;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TRAIN_NUMBER_RE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It looks reasonable, and it works for the intended cases:&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;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;345071&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ABC123&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;False&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But, woah, it also accepts garbage suffixes:&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;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;345071-in-abbey-wood&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True …&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;Take this validation function:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;TRAIN_NUMBER_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d&lt;/span&gt;&lt;span class="si"&gt;{6}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# six digits&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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="nb"&gt;bool&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;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TRAIN_NUMBER_RE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It looks reasonable, and it works for the intended cases:&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;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;345071&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ABC123&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;False&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But, woah, it also accepts garbage suffixes:&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;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;345071-in-abbey-wood&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That is a bug, totally not what the author intended.
&lt;a class="reference external" href="https://docs.python.org/3/library/re.html#re.Pattern.match"&gt;&lt;code class="docutils literal"&gt;re.Pattern.match()&lt;/code&gt;&lt;/a&gt; (and its module-level shortcut, &lt;a class="reference external" href="https://docs.python.org/3/library/re.html#re.match"&gt;&lt;code class="docutils literal"&gt;re.match()&lt;/code&gt;&lt;/a&gt;) only anchors at the &lt;em&gt;start&lt;/em&gt; of the string.
It happily reports a match as soon as it finds &lt;code class="docutils literal"&gt;345071&lt;/code&gt; at the beginning, regardless of what comes after.&lt;/p&gt;
&lt;p&gt;If you want to check that the &lt;em&gt;entire&lt;/em&gt; string conforms to the pattern, you need &lt;a class="reference external" href="https://docs.python.org/3/library/re.html#re.Pattern.fullmatch"&gt;&lt;code class="docutils literal"&gt;fullmatch()&lt;/code&gt;&lt;/a&gt; instead:&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="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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="nb"&gt;bool&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;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TRAIN_NUMBER_RE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…then 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;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;345071-in-abbey-wood&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;False&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;is_valid_train_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;345071&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So, there’s a bit of name confusion with &lt;code class="docutils literal"&gt;match()&lt;/code&gt;.
In many other regex implementations, like &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match"&gt;JavaScript&lt;/a&gt;, &lt;a class="reference external" href="https://www.pcre.org/current/doc/html/pcre2api.html#SEC28"&gt;PCRE&lt;/a&gt;, and &lt;a class="reference external" href="https://docs.ruby-lang.org/en/3.4/Regexp.html#method-i-match"&gt;Ruby&lt;/a&gt;, the “match” method name means “match this pattern &lt;em&gt;somewhere&lt;/em&gt; in the string”.
Python has this behaviour as &lt;code class="docutils literal"&gt;search()&lt;/code&gt;, and instead its &lt;code class="docutils literal"&gt;match()&lt;/code&gt; means “match this pattern as a prefix of the string”, which programmers may not even realize until they hit a bug like the above.&lt;/p&gt;
&lt;div class="section" id="introducing-re-prefixmatch"&gt;
&lt;h2&gt;Introducing &lt;code class="docutils literal"&gt;re.prefixmatch()&lt;/code&gt;&lt;a class="headerlink" href="#introducing-re-prefixmatch" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Python 3.15 (expected October 2026) adds &lt;a class="reference external" href="https://docs.python.org/3.15/library/re.html#re.prefixmatch"&gt;&lt;code class="docutils literal"&gt;re.prefixmatch()&lt;/code&gt;&lt;/a&gt; and &lt;a class="reference external" href="https://docs.python.org/3.15/library/re.html#re.Pattern.prefixmatch"&gt;&lt;code class="docutils literal"&gt;re.Pattern.prefixmatch()&lt;/code&gt;&lt;/a&gt;, as exact synonyms for &lt;code class="docutils literal"&gt;re.match()&lt;/code&gt; and &lt;code class="docutils literal"&gt;re.Pattern.match()&lt;/code&gt; respectively.
The new name describes what the function actually does: match a prefix of the string, not the whole thing, and not “anywhere”.
Hopefully, this will clear up the name confusion.&lt;/p&gt;
&lt;p&gt;So now you could run:&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;TRAIN_NUMBER_RE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefixmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;345071-in-abbey-wood&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;re.Match object; span=(0, 6), match=&amp;#39;345071&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefixmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d&lt;/span&gt;&lt;span class="si"&gt;{6}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;345071-in-abbey-wood&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;re.Match object; span=(0, 6), match=&amp;#39;345071&amp;#39;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that the behaviour is unchanged and the garbage suffix is still accepted.
&lt;code class="docutils literal"&gt;prefixmatch()&lt;/code&gt; doesn’t fix the validation bug discussed above—&lt;code class="docutils literal"&gt;fullmatch()&lt;/code&gt; is still the solution.
The new name only makes the prefix behaviour obvious at the call site, so you can spot such bugs when reading the code, or authors can make clear they really do intend to match a prefix.&lt;/p&gt;
&lt;p&gt;Alongside the new name, Python’s documentation now describes &lt;code class="docutils literal"&gt;match()&lt;/code&gt; as “soft deprecated” in favour of &lt;code class="docutils literal"&gt;prefixmatch()&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
Soft deprecated since version 3.15: &lt;code class="docutils literal"&gt;match()&lt;/code&gt; has been soft deprecated in favor of the alternate &lt;code class="docutils literal"&gt;prefixmatch()&lt;/code&gt; name of this API which is more explicitly descriptive.
Use it to better express intent.
The norm in other languages and regular expression implementations is to use the term &lt;em&gt;match&lt;/em&gt; to refer to the behavior of what Python has always called &lt;code class="docutils literal"&gt;search()&lt;/code&gt;.&lt;/blockquote&gt;
&lt;p&gt;…along with a link to &lt;a class="reference external" href="https://docs.python.org/3.15/library/re.html#prefixmatch-vs-match"&gt;a longer discussion on why&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Soft deprecated&lt;/strong&gt; means that the old name, &lt;code class="docutils literal"&gt;match()&lt;/code&gt;, does not emit a warning, nor are there any plans to add a warning or remove the method.
Existing code will continue to work without issue, but you should prefer the new name &lt;code class="docutils literal"&gt;prefixmatch()&lt;/code&gt; going forwards, when your code supports Python 3.15+.&lt;/p&gt;
&lt;p&gt;Potentially, linters will grow to enforce the new &lt;code class="docutils literal"&gt;prefixmatch()&lt;/code&gt; name over &lt;code class="docutils literal"&gt;match()&lt;/code&gt;, to make the behaviour obvious, but it’s too early for that at time of writing, as Python 3.15 isn’t even out yet.&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 method names be precise and your regexes concise,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: fix SyntaxWarning: invalid octal escape sequence</title><link href="https://adamj.eu/tech/2026/08/14/why-does-python-log-a-warning-for-invalid-octal-escape-sequence/" rel="alternate"/><published>2026-08-14T00:00:00+01:00</published><updated>2026-08-14T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-08-14:/tech/2026/08/14/why-does-python-log-a-warning-for-invalid-octal-escape-sequence/</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="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;C:&lt;/span&gt;&lt;span class="se"&gt;\477&lt;/span&gt;&lt;span class="s2"&gt;_data&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we run this with Python 3.11+ in &lt;a class="reference external" href="https://docs.python.org/3/library/devmode.html"&gt;development mode&lt;/a&gt; (to enable deprecation warnings), we will 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.11&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;dev&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;/.../example.py:1: DeprecationWarning: invalid octal escape sequence &amp;#39;\477&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;  path = &amp;quot;C:\477_data&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On Python 3.12+, we instead get …&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="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;C:&lt;/span&gt;&lt;span class="se"&gt;\477&lt;/span&gt;&lt;span class="s2"&gt;_data&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we run this with Python 3.11+ in &lt;a class="reference external" href="https://docs.python.org/3/library/devmode.html"&gt;development mode&lt;/a&gt; (to enable deprecation warnings), we will 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.11&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;dev&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;/.../example.py:1: DeprecationWarning: invalid octal escape sequence &amp;#39;\477&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;  path = &amp;quot;C:\477_data&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On Python 3.12+, we instead get a &lt;code class="docutils literal"&gt;SyntaxWarning&lt;/code&gt; which happens at compile time, even without development mode.
We can see this by compiling the file with &lt;a class="reference external" href="https://docs.python.org/3/library/py_compile.html"&gt;&lt;code class="docutils literal"&gt;py_compile&lt;/code&gt;&lt;/a&gt; rather than running it:&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.12&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;py_compile&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;example.py:1: SyntaxWarning: invalid octal escape sequence &amp;#39;\477&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;  path = &amp;quot;C:\477_data&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So, what does this warning mean?&lt;/p&gt;
&lt;p&gt;Python strings support &lt;strong&gt;octal escape sequences&lt;/strong&gt;: a backslash followed by one to three &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Octal"&gt;octal&lt;/a&gt; digits (&lt;code class="docutils literal"&gt;0&lt;/code&gt;–&lt;code class="docutils literal"&gt;7&lt;/code&gt;), such as &lt;code class="docutils literal"&gt;\120&lt;/code&gt;, which represents the character with that codepoint.
Three octal digits can encode any value from &lt;code class="docutils literal"&gt;0&lt;/code&gt; to &lt;code class="docutils literal"&gt;0o777&lt;/code&gt; (511 in decimal), but octal escapes were only ever intended to cover single bytes, &lt;code class="docutils literal"&gt;0&lt;/code&gt; to &lt;code class="docutils literal"&gt;0o377&lt;/code&gt; (255 in decimal), the range used by earlier character sets like Latin-1.&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;\477&lt;/code&gt; is one such over-large escape: &lt;code class="docutils literal"&gt;0o477&lt;/code&gt; is &lt;code class="docutils literal"&gt;319&lt;/code&gt; in decimal, above that &lt;code class="docutils literal"&gt;0o377&lt;/code&gt; limit.
In a &lt;code class="docutils literal"&gt;str&lt;/code&gt;, Python doesn’t clamp or wrap the value, it uses it as a full Unicode codepoint, however large:&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="nb"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\477&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;319&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\477&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;Ŀ&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code class="docutils literal"&gt;Ŀ&lt;/code&gt; is &lt;code class="docutils literal"&gt;U+013F LATIN CAPITAL LETTER L WITH MIDDLE DOT&lt;/code&gt;, an obscure character.
It’s probably not what the developer intended, which is why Python introduced this warning.
Curiously, octal escape sequences in &lt;code class="docutils literal"&gt;bytes&lt;/code&gt; literals behave differently, since a byte can only hold values &lt;code class="docutils literal"&gt;0&lt;/code&gt;–&lt;code class="docutils literal"&gt;255&lt;/code&gt;: Python masks the value down to fit, so &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;b&amp;quot;C:\477_data&amp;quot;&lt;/span&gt;&lt;/code&gt; becomes &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;b&amp;quot;C:?_data&amp;quot;&lt;/span&gt;&lt;/code&gt; (&lt;code class="docutils literal"&gt;319 &amp;amp; 0xFF&lt;/code&gt; is &lt;code class="docutils literal"&gt;63&lt;/code&gt;, the code point for &lt;code class="docutils literal"&gt;?&lt;/code&gt;), an equally surprising, silent, result.&lt;/p&gt;
&lt;p&gt;This warning was added in Python 3.11.
From the &lt;a class="reference external" href="https://docs.python.org/3.11/whatsnew/3.11.html#deprecated:~:text=Octal%20escapes%20in%20string%20and%20bytes%20literals"&gt;release notes&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
Octal escapes in string and bytes literals with values larger than &lt;code class="docutils literal"&gt;0o377&lt;/code&gt; (255 in decimal) now produce a &lt;code class="docutils literal"&gt;DeprecationWarning&lt;/code&gt;.
In a future Python version, they will raise a &lt;code class="docutils literal"&gt;SyntaxWarning&lt;/code&gt; and eventually a &lt;code class="docutils literal"&gt;SyntaxError&lt;/code&gt;.
(Contributed by Serhiy Storchaka in &lt;a class="reference external" href="https://github.com/python/cpython/issues/81548"&gt;gh-81548&lt;/a&gt;.)&lt;/blockquote&gt;
&lt;p&gt;And, as promised, Python 3.12 upgraded it, from the &lt;a class="reference external" href="https://docs.python.org/3.12/whatsnew/3.12.html#other-language-changes:~:text=Octal%20escapes%20with%20value%20larger%20than%200o377"&gt;3.12 release notes&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
Octal escapes with value larger than &lt;code class="docutils literal"&gt;0o377&lt;/code&gt; (ex: &lt;code class="docutils literal"&gt;&amp;quot;\477&amp;quot;&lt;/code&gt;), deprecated in Python 3.11, now produce a &lt;code class="docutils literal"&gt;SyntaxWarning&lt;/code&gt;, instead of &lt;code class="docutils literal"&gt;DeprecationWarning&lt;/code&gt;.
In a future Python version they will be eventually a &lt;code class="docutils literal"&gt;SyntaxError&lt;/code&gt;.
(Contributed by Victor Stinner in &lt;a class="reference external" href="https://github.com/python/cpython/issues/98401"&gt;gh-98401&lt;/a&gt;.)&lt;/blockquote&gt;
&lt;p&gt;This is the same version-by-version escalation that “regular” invalid escape sequences went through, &lt;a class="reference external" href="/tech/2022/11/04/why-does-python-deprecationwarning-invalid-escape-sequence/"&gt;as covered in a previous post&lt;/a&gt;, one Python version later.
The eventual promotion to &lt;code class="docutils literal"&gt;SyntaxError&lt;/code&gt; hasn’t happened yet, even in Python 3.15 (due October 2026).&lt;/p&gt;
&lt;p&gt;To fix this warning, try to understand the author’s intention when writing it.
In most cases, the backslash wasn’t intended to start an octal escape sequence, for example when writing out a Windows path or a regular expression, so you can double the backslash to make it a literal backslash:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;477_data&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In rare cases, if a genuine octal escape was meant, you can substitute the rendered character.&lt;/p&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;There is no escape,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: fix TypeError: NamedTuple() got an unexpected keyword argument</title><link href="https://adamj.eu/tech/2026/08/13/python-fix-typeerror-namedtuple-got-an-unexpected-keyword-argument/" rel="alternate"/><published>2026-08-13T00:00:00+01:00</published><updated>2026-08-13T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-08-13:/tech/2026/08/13/python-fix-typeerror-namedtuple-got-an-unexpected-keyword-argument/</id><summary type="html">&lt;p&gt;Take this code, defining a small &lt;code class="docutils literal"&gt;NamedTuple&lt;/code&gt; with a keyword argument per field:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;

&lt;span class="n"&gt;Point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Point&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&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.13 or 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:3: DeprecationWarning: Creating NamedTuple classes using keyword …&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;Take this code, defining a small &lt;code class="docutils literal"&gt;NamedTuple&lt;/code&gt; with a keyword argument per field:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;

&lt;span class="n"&gt;Point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Point&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&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.13 or 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:3: DeprecationWarning: Creating NamedTuple classes using keyword arguments is deprecated and will be disallowed in Python 3.15. Use the class-based or functional syntax instead.&lt;/span&gt;
&lt;span class="go"&gt;  Point = NamedTuple(&amp;quot;Point&amp;quot;, x=int, y=int)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And on Python 3.15+, it’s broken:&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.15&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  ...&lt;/span&gt;
&lt;span class="go"&gt;TypeError: NamedTuple() got an unexpected keyword argument &amp;#39;x&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What’s up with that?&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;NamedTuple&lt;/code&gt; has always had two &lt;em&gt;documented&lt;/em&gt; ways to define fields: the class-based syntax, and a functional syntax taking a list of &lt;code class="docutils literal"&gt;(name, type)&lt;/code&gt; pairs.
The keyword-argument style above was never one of them, it worked only as a side effect of the old implementation happening to accept &lt;code class="docutils literal"&gt;**kwargs&lt;/code&gt;.
This form was therefore deprecated and removed in Python 3.15, with &lt;code class="docutils literal"&gt;NamedTuple&lt;/code&gt;’s signature now locked to positional-only.&lt;/p&gt;
&lt;p&gt;From the &lt;a class="reference external" href="https://docs.python.org/3.15/whatsnew/3.15.html#:~:text=The%20undocumented%20keyword%20argument%20syntax%20for%20creating%20NamedTuple%20classes"&gt;release notes&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
The undocumented keyword argument syntax for creating &lt;code class="docutils literal"&gt;NamedTuple&lt;/code&gt; classes (for example, &lt;code class="docutils literal"&gt;Point = &lt;span class="pre"&gt;NamedTuple(&amp;quot;Point&amp;quot;,&lt;/span&gt; x=int, y=int)&lt;/code&gt;) is no longer supported.
Use the class-based syntax or the functional syntax instead.
(Contributed by Bénédikt Tran in &lt;a class="reference external" href="https://github.com/python/cpython/issues/133817"&gt;gh-133817&lt;/a&gt;.)&lt;/blockquote&gt;
&lt;div class="section" id="the-fix"&gt;
&lt;h2&gt;The fix&lt;a class="headerlink" href="#the-fix" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Switch to the class-based syntax, which is the more readable option, and gives you a normal class body to add methods or docstrings to later:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or, if you’d rather keep a one-liner, use the documented functional syntax, a list of &lt;code class="docutils literal"&gt;(name, type)&lt;/code&gt; pairs, instead of keyword arguments:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;

&lt;span class="n"&gt;Point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Point&amp;quot;&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;x&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&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;y&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&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="auto-fix-this-problem-with-ruff"&gt;
&lt;h2&gt;Auto-fix this problem with Ruff&lt;a class="headerlink" href="#auto-fix-this-problem-with-ruff" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="https://docs.astral.sh/ruff/"&gt;Ruff&lt;/a&gt;’s pyupgrade-derived rule &lt;a class="reference external" href="https://docs.astral.sh/ruff/rules/convert-named-tuple-functional-to-class/"&gt;convert-named-tuple-functional-to-class (UP014)&lt;/a&gt; rewrites both functional forms, the list-of-tuples version and this keyword-argument version, to class syntax automatically.
As of &lt;a class="reference external" href="https://astral.sh/blog/ruff-v0.16.0"&gt;Ruff 0.16&lt;/a&gt;, released July 2026, &lt;code class="docutils literal"&gt;UP014&lt;/code&gt; is one of the hundreds of rules Ruff now enables by default, so there’s nothing to switch on, just 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;ruff&lt;span class="w"&gt; &lt;/span&gt;check&lt;span class="w"&gt; &lt;/span&gt;--fix&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;Found 2 errors (2 fixed, 0 remaining).&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(If you’re on an older Ruff, add &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--select&lt;/span&gt; UP014&lt;/code&gt;, or the broader &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--select&lt;/span&gt; UP&lt;/code&gt;, to opt in explicitly.)&lt;/p&gt;
&lt;p&gt;…turning our original example straight into:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;/pre&gt;&lt;/div&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 name your tuples well,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: introducing emojet, a fast emoji lookup library</title><link href="https://adamj.eu/tech/2026/08/12/python-introducing-emojet/" rel="alternate"/><published>2026-08-12T00:00:00+01:00</published><updated>2026-08-12T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-08-12:/tech/2026/08/12/python-introducing-emojet/</id><summary type="html">&lt;p&gt;New package just landed!&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://pypi.org/project/emojet/"&gt;&lt;strong&gt;emojet&lt;/strong&gt;&lt;/a&gt; is an emoji library for Python: it converts between emoji and their names, in both directions, plus the searching and lookup functions that go with that.
It covers the core API of the &lt;a class="reference external" href="https://github.com/carpedm20/emoji/"&gt;&lt;code class="docutils literal"&gt;emoji&lt;/code&gt; package&lt;/a&gt;, a library that been available for this job since 2014 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;New package just landed!&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://pypi.org/project/emojet/"&gt;&lt;strong&gt;emojet&lt;/strong&gt;&lt;/a&gt; is an emoji library for Python: it converts between emoji and their names, in both directions, plus the searching and lookup functions that go with that.
It covers the core API of the &lt;a class="reference external" href="https://github.com/carpedm20/emoji/"&gt;&lt;code class="docutils literal"&gt;emoji&lt;/code&gt; package&lt;/a&gt;, a library that been available for this job since 2014, using the same names and the same data.
The difference is that emojet does the work in Rust, running 3.5x faster for conversion, 70 times faster for  deconversion, and using about 40% less memory.&lt;/p&gt;
&lt;p&gt;Use &lt;code class="docutils literal"&gt;emojize()&lt;/code&gt; to convert names to emoji:&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;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;emojet&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;emojet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emojize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Python is fun :thumbs_up:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;Python is fun 👍&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…and &lt;code class="docutils literal"&gt;demojize()&lt;/code&gt; to deconvert emoji to their names:&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;emojet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;demojize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Python is fun 👍&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;Python is fun :thumbs_up:&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Names come in 14 languages, plus the English aliases that GitHub and Slack use:&lt;/p&gt;
&lt;!-- typos: off --&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;emojet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emojize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Python ist toll :daumen_hoch:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;de&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;Python ist toll 👍&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;emojet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emojize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Python is fun :thumbsup:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;alias&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;Python is fun 👍&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;!-- typos: on --&gt;
&lt;p&gt;See the &lt;a class="reference external" href="https://emojet.readthedocs.io/en/latest/api.html"&gt;documentation&lt;/a&gt; for the full API.&lt;/p&gt;
&lt;div class="section" id="the-motivation"&gt;
&lt;h2&gt;The motivation&lt;a class="headerlink" href="#the-motivation" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Part of my work for my client &lt;a class="reference external" href="https://www.rippling.com/"&gt;Rippling&lt;/a&gt; is speeding up their project’s startup time.
I use several techniques to achieve this, as covered in &lt;a class="reference external" href="/tech/2023/03/02/django-profile-and-improve-import-time/"&gt;my previous post on optimizing startup time&lt;/a&gt;.
I’ve spent a lot of time finding slow-to-import third-party packages and choosing what to do with them, like deferring their imports, or upgrading to a faster version.&lt;/p&gt;
&lt;p&gt;The &lt;code class="docutils literal"&gt;emoji&lt;/code&gt; package stood out from profiling data: it took 22ms to import while providing minimal functionality.
22ms is not the slowest package by far, but it is still noticeable on a trace.
Further profiling revealed that most of its import time was spent loading and parsing a 520 KB JSON file containing the emoji characters and names.&lt;/p&gt;
&lt;p&gt;Initially, I deferred the &lt;code class="docutils literal"&gt;emoji&lt;/code&gt; imports, like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gd"&gt;-from emoji import emojize&lt;/span&gt;

&lt;span class="w"&gt; &lt;/span&gt;def show_message(text):
&lt;span class="gi"&gt;+    from emoji import emojize&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    print(emojize(text))
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This pushes the 22ms cost out of most processes, at the cost of loading the package later, after initialization is complete.
But the idea of a faster rewrite smouldered away at the back of my mind, based on these motivations:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;The data rarely changes (on Unicode annual updates), perfect for baking into a binary format.&lt;/li&gt;
&lt;li&gt;The API is fairly simple.&lt;/li&gt;
&lt;li&gt;String manipulation is often much faster in a lower-level language.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Anyway, I have been trying out Claude more recently, thanks to a free subscription that Anthropic gifted me due to my open source contributions.
I decided to fire Claude at the task and whaddaya know, bot did good, and after a bunch of iteration and minor touchups, we have emojet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="benchmarkation"&gt;
&lt;h2&gt;Benchmarkation&lt;a class="headerlink" href="#benchmarkation" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The emojet repo bundles a benchmark script for comparison with the original emoji package.
Here are some of the results comparing emojet 1.0.0 against emoji 2.15.0, on Python 3.14 (macOS, ARM):&lt;/p&gt;
&lt;table border="1" class="docutils"&gt;
&lt;colgroup&gt;
&lt;col width="25%" /&gt;
&lt;col width="25%" /&gt;
&lt;col width="25%" /&gt;
&lt;col width="25%" /&gt;
&lt;/colgroup&gt;
&lt;thead valign="bottom"&gt;
&lt;tr&gt;&lt;th class="head"&gt;&lt;strong&gt;Benchmark&lt;/strong&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;strong&gt;emoji&lt;/strong&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;strong&gt;emojet&lt;/strong&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;strong&gt;Speedup&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td&gt;&lt;code class="docutils literal"&gt;import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20.57 ms&lt;/td&gt;
&lt;td&gt;869.94 µs&lt;/td&gt;
&lt;td&gt;23.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code class="docutils literal"&gt;import&lt;/code&gt; + first &lt;code class="docutils literal"&gt;demojize()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;24.45 ms&lt;/td&gt;
&lt;td&gt;911.13 µs&lt;/td&gt;
&lt;td&gt;26.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code class="docutils literal"&gt;demojize()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;345.12 µs&lt;/td&gt;
&lt;td&gt;4.94 µs&lt;/td&gt;
&lt;td&gt;69.9x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code class="docutils literal"&gt;emojize()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;29.23 µs&lt;/td&gt;
&lt;td&gt;8.33 µs&lt;/td&gt;
&lt;td&gt;3.5x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Memory use is also lower: importing and calling &lt;code class="docutils literal"&gt;demojize()&lt;/code&gt; once uses about 17.4 MB of memory with emojet versus 29.2 MB with the &lt;code class="docutils literal"&gt;emoji&lt;/code&gt; package.&lt;/p&gt;
&lt;p&gt;You can check these numbers yourself.
The benchmark script uses &lt;a class="reference external" href="https://peps.python.org/pep-0723/"&gt;PEP 723&lt;/a&gt; inline script metadata, so &lt;a class="reference external" href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt; will install the latest releases of both packages into a temporary environment for you:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;scripts/benchmark.py
&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;emojet is a Rust extension module, built with &lt;a class="reference external" href="https://pyo3.rs/"&gt;PyO3&lt;/a&gt; and &lt;a class="reference external" href="https://www.maturin.rs/"&gt;maturin&lt;/a&gt;, with all of the emoji data compiled into static tables:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;A &lt;a class="reference external" href="https://docs.rs/phf/"&gt;perfect hash function&lt;/a&gt; over the 5,316 English names and aliases, so &lt;code class="docutils literal"&gt;emojize()&lt;/code&gt; gets a collision-free, single-probe lookup.&lt;/li&gt;
&lt;li&gt;A static trie over the code points of all 5,225 emoji sequences, which drives the scanner behind &lt;code class="docutils literal"&gt;demojize()&lt;/code&gt; and friends.&lt;/li&gt;
&lt;li&gt;Per-language name tables, indexed directly, with no loading step.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nothing is parsed at import time.
The tables sit in the read-only data section of the extension module, so the operating system pages them in on demand, and they’re shared between processes.
That’s where the import-time win comes from: the remaining ~900 µs is mostly the dynamic loader doing its job.&lt;/p&gt;
&lt;p&gt;The scanners work on UTF-8 bytes end to end, writing each result into a single buffer rather than joining a list of pieces.
They also skip runs of ASCII text with a byte-per-character test, since almost no emoji starts with an ASCII character—and most text is mostly ASCII.
That’s why &lt;code class="docutils literal"&gt;demojize()&lt;/code&gt; on a mixed string sees a bigger speedup than &lt;code class="docutils literal"&gt;emojize()&lt;/code&gt;, which has to hunt for delimiters.&lt;/p&gt;
&lt;p&gt;The data itself is generated by &lt;code class="docutils literal"&gt;scripts/generate_data.py&lt;/code&gt;, which follows the &lt;code class="docutils literal"&gt;emoji&lt;/code&gt; package’s own pipeline: Unicode’s &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;emoji-test.txt&lt;/span&gt;&lt;/code&gt; and &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;emoji-variation-sequences.txt&lt;/span&gt;&lt;/code&gt;, &lt;a class="reference external" href="https://cldr.unicode.org/"&gt;CLDR&lt;/a&gt; annotations for translated names, and GitHub’s &lt;a class="reference external" href="https://github.com/github/gemoji"&gt;gemoji&lt;/a&gt; database for aliases.
The generated file is checked in, so installing or building emojet downloads nothing.
1.0.0 ships Unicode 17.0.0 data, matching &lt;code class="docutils literal"&gt;emoji&lt;/code&gt; 2.15.0.&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 your project has emoji conversion needs, please give emojet a try and let me know how it goes.&lt;/p&gt;
&lt;p&gt;One emojo at a time,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: tprof 1.3.0: now with less overhead</title><link href="https://adamj.eu/tech/2026/08/08/python-tprof-performance/" rel="alternate"/><published>2026-08-08T00:00:00+01:00</published><updated>2026-08-08T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-08-08:/tech/2026/08/08/python-tprof-performance/</id><summary type="html">&lt;p&gt;Back in January, &lt;a class="reference external" href="/tech/2026/01/14/python-introducing-tprof/"&gt;I introduced tprof&lt;/a&gt;, a targeting profiler for Python 3.12+ that measures the time spent in specific functions, rather than your whole program.
As a reminder, here’s the basic usage, specifying a target function with &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;-t&lt;/span&gt;&lt;/code&gt; and a script to 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;tprof&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;lib:maths&lt;span class="w"&gt; &lt;/span&gt;./example …&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;Back in January, &lt;a class="reference external" href="/tech/2026/01/14/python-introducing-tprof/"&gt;I introduced tprof&lt;/a&gt;, a targeting profiler for Python 3.12+ that measures the time spent in specific functions, rather than your whole program.
As a reminder, here’s the basic usage, specifying a target function with &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;-t&lt;/span&gt;&lt;/code&gt; and a script to 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;tprof&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;lib:maths&lt;span class="w"&gt; &lt;/span&gt;./example.py
&lt;span class="go"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;🎯 tprof results:&lt;/span&gt;
&lt;span class="go"&gt; function    calls total  median ± σ     min … max&lt;/span&gt;
&lt;span class="go"&gt; lib:maths()     2 610ms 305ms ± 2ms 304ms … 307ms&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Today, it’s my pleasure to announce &lt;a class="reference external" href="https://github.com/adamchainz/tprof/blob/main/CHANGELOG.rst"&gt;tprof 1.3.0&lt;/a&gt;, a big release with the following changes.&lt;/p&gt;
&lt;div class="section" id="way-less-overhead"&gt;
&lt;h2&gt;Way less overhead&lt;a class="headerlink" href="#way-less-overhead" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;tprof previously stored each recorded call as a Python &lt;code class="docutils literal"&gt;int&lt;/code&gt; inside a &lt;code class="docutils literal"&gt;dict&lt;/code&gt; of &lt;code class="docutils literal"&gt;list&lt;/code&gt;s, then used the &lt;a class="reference external" href="https://docs.python.org/3/library/statistics.html"&gt;&lt;code class="docutils literal"&gt;statistics&lt;/code&gt;&lt;/a&gt; module to crunch the numbers at report time.
These data structures were easy to write in an initial version and get the project going, but it added meaningful overhead to &lt;em&gt;every&lt;/em&gt; monitored call.&lt;/p&gt;
&lt;p&gt;The new release replaces all of that with per-thread C structures: a stack of entry times and a flat array of durations per target, storing raw nanosecond &lt;code class="docutils literal"&gt;int64_t&lt;/code&gt; values instead of boxed Python ints.
On Python 3.13+, it also reads timestamps with the new &lt;a class="reference external" href="https://docs.python.org/3/c-api/time.html#c.PyTime_PerfCounterRaw"&gt;&lt;code class="docutils literal"&gt;PyTime_PerfCounterRaw()&lt;/code&gt;&lt;/a&gt; instead of calling &lt;a class="reference external" href="https://docs.python.org/3/library/time.html#time.perf_counter_ns"&gt;&lt;code class="docutils literal"&gt;time.perf_counter_ns()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Most importantly, &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; callbacks are now disabled entirely for code that isn’t a target, so non-target functions run at full speed.&lt;/p&gt;
&lt;p&gt;The results, benchmarking a trivial function on Python 3.13 (overhead versus an unprofiled call in parentheses):&lt;/p&gt;
&lt;table border="1" class="docutils"&gt;
&lt;colgroup&gt;
&lt;col width="33%" /&gt;
&lt;col width="33%" /&gt;
&lt;col width="33%" /&gt;
&lt;/colgroup&gt;
&lt;thead valign="bottom"&gt;
&lt;tr&gt;&lt;th class="head"&gt;&amp;nbsp;&lt;/th&gt;
&lt;th class="head"&gt;Before&lt;/th&gt;
&lt;th class="head"&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td&gt;Target call&lt;/td&gt;
&lt;td&gt;469ns (+439)&lt;/td&gt;
&lt;td&gt;170ns (+141)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Non-target call&lt;/td&gt;
&lt;td&gt;440ns (+410)&lt;/td&gt;
&lt;td&gt;28ns (+0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Memory per 1M calls&lt;/td&gt;
&lt;td&gt;34.8 MiB&lt;/td&gt;
&lt;td&gt;8.0 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Report per 1M calls&lt;/td&gt;
&lt;td&gt;~430ms&lt;/td&gt;
&lt;td&gt;~3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In short:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Target functions have about a &lt;strong&gt;3× reduction in overhead&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Non-target functions have &lt;strong&gt;none at all&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Memory use is &lt;strong&gt;4× lower&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Generating the final report is roughly &lt;strong&gt;100× faster&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="comparing-against-a-saved-baseline"&gt;
&lt;h2&gt;Comparing against a saved baseline&lt;a class="headerlink" href="#comparing-against-a-saved-baseline" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;tprof’s existing comparison mode (&lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;-x&lt;/span&gt;&lt;/code&gt;/&lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--compare&lt;/span&gt;&lt;/code&gt;) is great for comparing two functions side-by-side in one run, such as “before” and “after” versions while you’re mid-refactor.
But sometimes the “before” version only exists on another Git branch.
For that, this release adds &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--json&lt;/span&gt;&lt;/code&gt; and &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--baseline&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Run with &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--json&lt;/span&gt; &amp;lt;path&amp;gt;&lt;/code&gt; to write the statistics to a file (or &lt;code class="docutils literal"&gt;-&lt;/code&gt; for stdout) as well as printing the usual report:&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;git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;main
&lt;span class="gp"&gt;$ &lt;/span&gt;tprof&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;lib:maths&lt;span class="w"&gt; &lt;/span&gt;--json&lt;span class="w"&gt; &lt;/span&gt;before.json&lt;span class="w"&gt; &lt;/span&gt;./example.py
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, after checking out your change, pass that file to &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--baseline&lt;/span&gt;&lt;/code&gt; to get a “delta” column comparing each function’s median against the saved 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;git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;my-optimization
&lt;span class="gp"&gt;$ &lt;/span&gt;tprof&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;lib:maths&lt;span class="w"&gt; &lt;/span&gt;--baseline&lt;span class="w"&gt; &lt;/span&gt;before.json&lt;span class="w"&gt; &lt;/span&gt;./example.py
&lt;span class="go"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;🎯 tprof results:&lt;/span&gt;
&lt;span class="go"&gt; function    calls total  median ± σ     min … max     delta&lt;/span&gt;
&lt;span class="go"&gt; lib:maths()     2 592ms 296ms ± 2ms  294ms … 297ms -3.11%&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="median-not-mean"&gt;
&lt;h2&gt;Median, not mean&lt;a class="headerlink" href="#median-not-mean" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The headline statistic in tprof’s report is now the &lt;strong&gt;median&lt;/strong&gt; rather than the mean, since it’s more robust to the outliers that profiling data tends to contain, like garbage collection pauses and cold caches.
Comparison mode deltas, and the new baseline deltas above, are computed from medians too.
The median is computed exactly, in C, via &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Quickselect"&gt;quickselect&lt;/a&gt; over a scratch buffer at report time, so this change doesn’t add any recording overhead—only a small amount of extra work when the report is generated.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="programmatic-access-to-results"&gt;
&lt;h2&gt;Programmatic access to results&lt;a class="headerlink" href="#programmatic-access-to-results" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;code class="docutils literal"&gt;tprof()&lt;/code&gt; context manager / decorator now yields a list of &lt;code class="docutils literal"&gt;FunctionStats&lt;/code&gt; objects, populated once the profiled block ends, so you can use the results in code rather than only reading the printed report:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;lib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;maths&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;tprof&lt;/span&gt;&lt;span class="w"&gt; &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;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;maths&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;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;maths&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;function_stats&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;  &lt;span class="c1"&gt;# unpack the single result for maths()&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;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;function_stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; took &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;function_stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;median_ns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ns&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each &lt;code class="docutils literal"&gt;FunctionStats&lt;/code&gt; has &lt;code class="docutils literal"&gt;name&lt;/code&gt;, &lt;code class="docutils literal"&gt;calls&lt;/code&gt;, &lt;code class="docutils literal"&gt;total_ns&lt;/code&gt;, &lt;code class="docutils literal"&gt;min_ns&lt;/code&gt;, &lt;code class="docutils literal"&gt;max_ns&lt;/code&gt;, &lt;code class="docutils literal"&gt;median_ns&lt;/code&gt;, and &lt;code class="docutils literal"&gt;stdev_ns&lt;/code&gt;, matching the fields in the &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--json&lt;/span&gt;&lt;/code&gt; output.&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 you’re already using tprof, this release should make it easier to get correct results.
And if you haven’t tried it yet, &lt;a class="reference external" href="https://pypi.org/project/tprof/"&gt;install it from PyPI&lt;/a&gt; and give it a go.&lt;/p&gt;
&lt;p&gt;More tea, prof?&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: how time-machine is O(1) where freezegun is O(n)</title><link href="https://adamj.eu/tech/2026/08/03/python-time-machine-o1-freezegun-on/" rel="alternate"/><published>2026-08-03T00:00:00+01:00</published><updated>2026-08-03T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-08-03:/tech/2026/08/03/python-time-machine-o1-freezegun-on/</id><summary type="html">&lt;p&gt;&lt;a class="reference external" href="https://time-machine.readthedocs.io/en/latest/"&gt;time-machine&lt;/a&gt; is my library for mocking the current date and time in Python tests.
Its headline advantage over &lt;a class="reference external" href="https://github.com/spulec/freezegun"&gt;freezegun&lt;/a&gt;, the library that inspired it, is speed.&lt;/p&gt;
&lt;p&gt;Back in 2021, I &lt;a class="reference external" href="/tech/2021/02/19/freezegun-versus-time-machine/"&gt;benchmarked the two libraries&lt;/a&gt; at two project sizes, and found time-machine 100 to 200 times faster.
That post asserted that …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a class="reference external" href="https://time-machine.readthedocs.io/en/latest/"&gt;time-machine&lt;/a&gt; is my library for mocking the current date and time in Python tests.
Its headline advantage over &lt;a class="reference external" href="https://github.com/spulec/freezegun"&gt;freezegun&lt;/a&gt;, the library that inspired it, is speed.&lt;/p&gt;
&lt;p&gt;Back in 2021, I &lt;a class="reference external" href="/tech/2021/02/19/freezegun-versus-time-machine/"&gt;benchmarked the two libraries&lt;/a&gt; at two project sizes, and found time-machine 100 to 200 times faster.
That post asserted that freezegun’s work grows with the number of imported modules, whilst time-machine’s does not.&lt;/p&gt;
&lt;p&gt;Five years on, following many time-machine optimizations, including in Friday’s &lt;a class="reference external" href="/tech/2026/07/31/python-time-machine-3.3.0/"&gt;time-machine 3.3.0&lt;/a&gt;, let’s re-benchmark the two libraries to empirically measure their runtime complexity and examine their code to explain why.&lt;/p&gt;
&lt;div class="section" id="the-benchmark"&gt;
&lt;h2&gt;The benchmark&lt;a class="headerlink" href="#the-benchmark" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I wrote a benchmark script to compare the two libraries, listed in full at the end of this section.
The variable here is the number of attributes across all imported modules.&lt;/p&gt;
&lt;p&gt;To roughly simulate production code, the benchmark generates modules as bare &lt;a class="reference external" href="https://docs.python.org/3/library/types.html#types.ModuleType"&gt;&lt;code class="docutils literal"&gt;types.ModuleType&lt;/code&gt;&lt;/a&gt; instances and stuffs them straight into &lt;code class="docutils literal"&gt;sys.modules&lt;/code&gt;.
Each module object has 25 attributes, and one in ten of them has module-level references to the date and time functions, as if written with &lt;code class="docutils literal"&gt;from datetime import date, datetime&lt;/code&gt; and &lt;code class="docutils literal"&gt;from time import time&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_modules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&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="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;generated_module_&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="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModuleType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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;j&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="n"&gt;ATTRIBUTES_PER_MODULE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nb"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&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;attribute_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&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="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;value_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&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="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;10&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# As if: from datetime import date, datetime&lt;/span&gt;
            &lt;span class="c1"&gt;#        from time import time&lt;/span&gt;
            &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;
            &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;
            &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;
        &lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;modules&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The benchmark then measures the time taken to mock and unmock time with each library, with these test skeletons:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2020&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;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;freezegun_test&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;freezegun&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freeze_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tick&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;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;time_machine_test&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;time_machine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;travel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A sweep then walks the module counts, installing the generated modules, timing both functions with &lt;a class="reference external" href="https://docs.python.org/3/library/timeit.html"&gt;&lt;code class="docutils literal"&gt;timeit&lt;/code&gt;&lt;/a&gt;, and cleaning up:&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;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;COUNTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;generated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_modules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&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;modules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated&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="n"&gt;total&lt;/span&gt; &lt;span class="o"&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;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;freezegun_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;freezegun_test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time_machine_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_machine_test&lt;/span&gt;&lt;span class="p"&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;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&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;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freezegun_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_machine_time&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;measure()&lt;/code&gt; uses &lt;code class="docutils literal"&gt;timeit.Timer.autorange()&lt;/code&gt; to pick a loop count, repeats five times, and takes the fastest per-call time.
The whole sweep then runs five times over, keeping the fastest time seen at each size.
Keeping the fastest times removes noise from our results, as nearly all slowdowns are due to external reasons.&lt;/p&gt;
&lt;p&gt;Here’s the whole thing, with &lt;a class="reference external" href="https://packaging.python.org/en/latest/specifications/inline-script-metadata/"&gt;inline script metadata&lt;/a&gt; so that &lt;code class="docutils literal"&gt;uv run benchmark.py&lt;/code&gt; is all you need to run it:&lt;/p&gt;
&lt;details&gt;
  &lt;summary&gt;&lt;strong&gt;&lt;code&gt;benchmark.py&lt;/code&gt; source&lt;/strong&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Benchmark freezegun against time-machine as imported modules grow.&lt;/span&gt;

&lt;span class="sd"&gt;Generates module objects, installs them in sys.modules, and times a&lt;/span&gt;
&lt;span class="sd"&gt;mock-and-unmock cycle for each library. One in ten generated modules has&lt;/span&gt;
&lt;span class="sd"&gt;module-level references to the date and time functions, as if written with&lt;/span&gt;
&lt;span class="sd"&gt;``from datetime import date, datetime`` and ``from time import time``.&lt;/span&gt;
&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# /// script&lt;/span&gt;
&lt;span class="c1"&gt;# requires-python = &amp;quot;&amp;gt;=3.11&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# dependencies = [&amp;quot;freezegun==1.5.5&amp;quot;, &amp;quot;time-machine==3.3.0&amp;quot;]&lt;/span&gt;
&lt;span class="c1"&gt;# ///&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;__future__&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;timeit&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;types&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;collections.abc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;freezegun&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time_machine&lt;/span&gt;

&lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2020&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;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;COUNTS&lt;/span&gt; &lt;span class="o"&gt;=&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="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ATTRIBUTES_PER_MODULE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;span class="n"&gt;ROUNDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_modules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;dict&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="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModuleType&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&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="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;generated_module_&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="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModuleType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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;j&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="n"&gt;ATTRIBUTES_PER_MODULE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nb"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&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;attribute_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&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="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;value_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&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="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;10&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# As if: from datetime import date, datetime&lt;/span&gt;
            &lt;span class="c1"&gt;#        from time import time&lt;/span&gt;
            &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;
            &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;
            &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;
        &lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;modules&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;freezegun_test&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;freezegun&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freeze_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tick&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;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;time_machine_test&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;time_machine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;travel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&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="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;float&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;Seconds per call, taking the fastest of several timed batches.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autorange&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sweep&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;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&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="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&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;Time both libraries at each module count, once each.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&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;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;COUNTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;generated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_modules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&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;modules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated&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="n"&gt;total&lt;/span&gt; &lt;span class="o"&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;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;freezegun_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;freezegun_test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;time_machine_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_machine_test&lt;/span&gt;&lt;span class="p"&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;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;del&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;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freezegun_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_machine_time&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;results&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&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;# Sweep several times over, keeping the fastest time seen for each size.&lt;/span&gt;
    &lt;span class="c1"&gt;# A single pass takes a couple of minutes, over which a machine can drift&lt;/span&gt;
    &lt;span class="c1"&gt;# in and out of being busy, which would otherwise bend the results.&lt;/span&gt;
    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sweep&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="n"&gt;ROUNDS&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&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;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freezegun_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_machine_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sweep&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_freezegun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_time_machine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best_freezegun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freezegun_time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best_time_machine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_machine_time&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="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;generated&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;total&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;freezegun&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;13&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;time-machine&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;13&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freezegun_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_machine_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&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;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;10,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;10,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;freezegun_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1e6&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;10,.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; µs &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time_machine_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1e6&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;10,.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; µs&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;br&gt;&lt;p&gt;Here are the results, recorded on Python 3.15 on my M1 MacBook with freezegun 1.5.5 and time-machine 3.3.0.
Every total includes the 259 modules that are there before any are generated: those imported by the benchmark itself, plus the ones that freezegun imports, such as &lt;code class="docutils literal"&gt;asyncio&lt;/code&gt;, the first time it freezes.&lt;/p&gt;
&lt;table border="1" class="docutils"&gt;
&lt;colgroup&gt;
&lt;col width="25%" /&gt;
&lt;col width="25%" /&gt;
&lt;col width="25%" /&gt;
&lt;col width="25%" /&gt;
&lt;/colgroup&gt;
&lt;thead valign="bottom"&gt;
&lt;tr&gt;&lt;th class="head"&gt;&lt;strong&gt;Generated&lt;/strong&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;strong&gt;Total modules&lt;/strong&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;strong&gt;freezegun&lt;/strong&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;strong&gt;time-machine&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;259&lt;/td&gt;
&lt;td&gt;1,406.7 µs&lt;/td&gt;
&lt;td&gt;1.5 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;759&lt;/td&gt;
&lt;td&gt;2,484.3 µs&lt;/td&gt;
&lt;td&gt;1.5 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1,259&lt;/td&gt;
&lt;td&gt;3,736.1 µs&lt;/td&gt;
&lt;td&gt;1.5 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;2,259&lt;/td&gt;
&lt;td&gt;5,757.4 µs&lt;/td&gt;
&lt;td&gt;1.5 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;4,259&lt;/td&gt;
&lt;td&gt;10,428.1 µs&lt;/td&gt;
&lt;td&gt;1.5 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;8,000&lt;/td&gt;
&lt;td&gt;8,259&lt;/td&gt;
&lt;td&gt;20,429.3 µs&lt;/td&gt;
&lt;td&gt;1.5 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;16,000&lt;/td&gt;
&lt;td&gt;16,259&lt;/td&gt;
&lt;td&gt;40,971.3 µs&lt;/td&gt;
&lt;td&gt;1.5 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- generation script for chart --&gt;
&lt;!-- #!/usr/bin/env -S uv run - -script --&gt;
&lt;!-- """Draw the freezegun versus time-machine benchmark chart. --&gt;
&lt;!--  --&gt;
&lt;!-- Plots the numbers measured for the post “Python: how time-machine is O(1) --&gt;
&lt;!-- where freezegun is O(n)”, and writes an SVG styled to match the site --&gt;
&lt;!-- """ --&gt;
&lt;!--  --&gt;
&lt;!-- # /// script --&gt;
&lt;!-- # requires-python = "&gt;=3.11" --&gt;
&lt;!-- # dependencies = ["xy==0.0.5"] --&gt;
&lt;!-- # /// --&gt;
&lt;!--  --&gt;
&lt;!-- from __future__ import annotations --&gt;
&lt;!--  --&gt;
&lt;!-- from pathlib import Path --&gt;
&lt;!--  --&gt;
&lt;!-- import xy  # type: ignore[import-not-found] --&gt;
&lt;!--  --&gt;
&lt;!-- MODULE_DIR = Path(__file__).parent.resolve() --&gt;
&lt;!-- OUTPUT_PATH = MODULE_DIR / "static" / "tech" / "assets" / "2026-08-01-benchmark.svg" --&gt;
&lt;!--  --&gt;
&lt;!-- # Modules in sys.modules, and microseconds per mock-and-unmock cycle. --&gt;
&lt;!-- MODULES = [259, 759, 1259, 2259, 4259, 8259, 16259] --&gt;
&lt;!-- FREEZEGUN = [1406.7, 2484.3, 3736.1, 5757.4, 10428.1, 20429.3, 40971.3] --&gt;
&lt;!-- TIME_MACHINE = [1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5] --&gt;
&lt;!--  --&gt;
&lt;!-- # Site styles, from theme/static/css/main.css. --&gt;
&lt;!-- SPARKLING_GRAPE = "#8d4384" --&gt;
&lt;!-- ULTRA_VIOLET = "#71589b" --&gt;
&lt;!-- BACKGROUND = "#ffffff" --&gt;
&lt;!-- TEXT = "#515151" --&gt;
&lt;!-- RULE = "#e5e5e5" --&gt;
&lt;!-- FONT = "'PT Sans', Helvetica, Arial, sans-serif" --&gt;
&lt;!--  --&gt;
&lt;!-- # …and its dark mode counterparts. The series keep their colours, as the site’s --&gt;
&lt;!-- # links keep theirs. --&gt;
&lt;!-- DARK_BACKGROUND = "#000000" --&gt;
&lt;!-- DARK_TEXT = "#eee8d5" --&gt;
&lt;!-- DARK_RULE = "#333333" --&gt;
&lt;!--  --&gt;
&lt;!-- # xy hard-codes this stack on the root &lt;svg&gt; element, where it applies to the --&gt;
&lt;!-- # tick labels and legend. There’s no API to set it, so swap it out afterwards. --&gt;
&lt;!-- XY_FONT = "system-ui, -apple-system, 'Segoe UI', sans-serif" --&gt;
&lt;!--  --&gt;
&lt;!-- # xy writes colours as presentation attributes, which any stylesheet rule beats, --&gt;
&lt;!-- # so the SVG can recolour itself for whichever scheme the reader is using. It’s --&gt;
&lt;!-- # embedded with &lt;object&gt;, so it’s a document of its own and gets to do that. --&gt;
&lt;!-- DARK_MODE_STYLE = f"""\ --&gt;
&lt;!-- &lt;style&gt; --&gt;
&lt;!-- @media (prefers-color-scheme: dark) {{ --&gt;
&lt;!-- [fill="{BACKGROUND}"] {{ fill: {DARK_BACKGROUND}; }} --&gt;
&lt;!-- [fill="{TEXT}"] {{ fill: {DARK_TEXT}; }} --&gt;
&lt;!-- [stroke="{RULE}"] {{ stroke: {DARK_RULE}; }} --&gt;
&lt;!-- }} --&gt;
&lt;!-- &lt;/style&gt;""" --&gt;
&lt;!--  --&gt;
&lt;!--  --&gt;
&lt;!-- def restyle(svg: str) -&gt; str: --&gt;
&lt;!-- """Swap in the site font stack and add the dark mode rules.""" --&gt;
&lt;!-- styled = svg.replace(f'font-family="{XY_FONT}"', f'font-family="{FONT}"', 1) --&gt;
&lt;!-- if styled == svg: --&gt;
&lt;!-- raise SystemExit("xy’s default font stack not found, check the SVG output") --&gt;
&lt;!-- start_tag, separator, rest = styled.partition("&gt;") --&gt;
&lt;!-- if not separator: --&gt;
&lt;!-- raise SystemExit("no root &lt;svg&gt; element found, check the SVG output") --&gt;
&lt;!-- return start_tag + separator + DARK_MODE_STYLE + rest --&gt;
&lt;!--  --&gt;
&lt;!--  --&gt;
&lt;!-- def main() -&gt; None: --&gt;
&lt;!-- axis_style = { --&gt;
&lt;!-- "label_font_family": FONT, --&gt;
&lt;!-- "label_color": TEXT, --&gt;
&lt;!-- "label_size": 16, --&gt;
&lt;!-- "tick_label_size": 14, --&gt;
&lt;!-- } --&gt;
&lt;!-- chart = xy.line_chart( --&gt;
&lt;!-- xy.line(MODULES, FREEZEGUN, name="freezegun", color=SPARKLING_GRAPE, width=2.5), --&gt;
&lt;!-- xy.scatter(MODULES, FREEZEGUN, color=SPARKLING_GRAPE, size=5), --&gt;
&lt;!-- xy.line( --&gt;
&lt;!-- MODULES, --&gt;
&lt;!-- TIME_MACHINE, --&gt;
&lt;!-- name="time-machine", --&gt;
&lt;!-- color=ULTRA_VIOLET, --&gt;
&lt;!-- width=2.5, --&gt;
&lt;!-- ), --&gt;
&lt;!-- xy.scatter(MODULES, TIME_MACHINE, color=ULTRA_VIOLET, size=5), --&gt;
&lt;!-- xy.x_axis(label="Modules in sys.modules", format=",.0f", style=axis_style), --&gt;
&lt;!-- xy.y_axis(label="Microseconds per call", format=",.0f", style=axis_style), --&gt;
&lt;!-- xy.legend(loc="upper left", style={"font-size": 14}), --&gt;
&lt;!-- xy.theme( --&gt;
&lt;!-- background=BACKGROUND, --&gt;
&lt;!-- plot_background=BACKGROUND, --&gt;
&lt;!-- grid_color=RULE, --&gt;
&lt;!-- axis_color=RULE, --&gt;
&lt;!-- text_color=TEXT, --&gt;
&lt;!-- ), --&gt;
&lt;!-- ) --&gt;
&lt;!--  --&gt;
&lt;!-- svg = chart.to_svg(width=600, height=340) --&gt;
&lt;!-- OUTPUT_PATH.write_text(restyle(svg) + "\n") --&gt;
&lt;!-- print(f"Wrote {OUTPUT_PATH}") --&gt;
&lt;!--  --&gt;
&lt;!--  --&gt;
&lt;!-- if __name__ == "__main__": --&gt;
&lt;!-- main() --&gt;
&lt;img src=/tech/assets/2026-08-03-benchmark.svg
     width=1000
     alt="Line chart of time per call against modules in sys.modules. freezegun climbs up whilst time-machine stays flat."&gt;&lt;p&gt;freezegun’s times climb in a straight line: about 1.4 ms of fixed cost plus 2.5 µs per module.
time-machine stays at 1.5 µs whatever you throw at it, with no trend as the modules pile up.&lt;/p&gt;
&lt;p&gt;So the ratio is not a fixed “N times faster”—it’s a function of your project’s size:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;259 modules, a bare interpreter: 940 times faster.&lt;/li&gt;
&lt;li&gt;1,259 modules, about the size of the small Django project from the 2021 post: 2,490 times faster.&lt;/li&gt;
&lt;li&gt;16,259 modules, a big project with a lot of dependencies: 27,300 times faster.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Per start/stop cycle, freezegun at 16,000 generated modules costs 41 ms.
A suite with 2,000 time-mocking tests spends 82 seconds doing nothing but find-and-replace.
The same suite with time-machine spends 3 milliseconds.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="why-freezegun-is-o-n-the-sweep"&gt;
&lt;h2&gt;Why freezegun is O(n): the sweep&lt;a class="headerlink" href="#why-freezegun-is-o-n-the-sweep" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;freezegun replaces the date and time functions with fakes, then goes looking for every copy of the originals that other modules made when they imported them.
That search is a loop over &lt;code class="docutils literal"&gt;sys.modules&lt;/code&gt;, in &lt;code class="docutils literal"&gt;freeze_time.start()&lt;/code&gt; (&lt;a class="reference external" href="https://github.com/spulec/freezegun/blob/92d61b3f5c31942a1039713574487bdcfcdbbfff/freezegun/api.py#L819-L832"&gt;source&lt;/a&gt;):&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;mod_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="ow"&gt;in&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;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&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;mod_name&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;mod_name&lt;/span&gt; &lt;span class="o"&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;continue&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;mod_name&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="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;mod_name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.six.moves&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__name__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;datetime&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;time&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="n"&gt;module_attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_cached_module_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&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;attribute_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attribute_value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;module_attrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;fake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fakes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_value&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;fake&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attribute_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;add_change&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attribute_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attribute_value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Every time you freeze time, every loaded module gets visited.
&lt;code class="docutils literal"&gt;fakes&lt;/code&gt; is a dict keyed by &lt;code class="docutils literal"&gt;id()&lt;/code&gt; of the real objects, so a module that imported &lt;code class="docutils literal"&gt;datetime.datetime&lt;/code&gt; has that name rebound to &lt;code class="docutils literal"&gt;FakeDatetime&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;_get_cached_module_attributes()&lt;/code&gt; gathers the attributes to check, with a little caching to speed things up: it remembers which of a module’s names held date and time objects last time, and reuses that list if the module’s contents hash the same.
Even on a hit, though, computing that hash means listing and hashing every attribute name of every module, every time.
Therefore, the work per freeze stays proportional to the number of module-level attributes across all loaded modules.&lt;/p&gt;
&lt;p&gt;THus we can say freezegun has &lt;strong&gt;O(n)&lt;/strong&gt; runtime complexity, where &lt;strong&gt;n&lt;/strong&gt; is the number of module-level attributes across all loaded modules.
The larger your project, the longer it takes, proportionally.&lt;/p&gt;
&lt;p&gt;This slow process is also leaky for (at least) these two reasons:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;It doesn’t discover some kinds of references, like class attributes, default arguments, closures, and C extensions that hold references to the original functions.
These continue to yield the true time, even while most calls to the same function name are mocked.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;The fake objects have observably different types:&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;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;freezegun&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;
&lt;span class="go"&gt;(&amp;#39;datetime&amp;#39;, &amp;#39;time&amp;#39;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;freezegun&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freeze_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2020-01-01&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&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;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;FakeDatetime fake_time&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This difference can subtly break code that checks or records types.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="why-time-machine-is-o-1-the-swap"&gt;
&lt;h2&gt;Why time-machine is O(1): the swap&lt;a class="headerlink" href="#why-time-machine-is-o-1-the-swap" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;time-machine takes a different approach: it swaps what the date and time functions do, a bit like &lt;a class="reference external" href="https://docs.python.org/3/library/unittest.mock.html"&gt;&lt;code class="docutils literal"&gt;unittest.mock&lt;/code&gt;&lt;/a&gt; but for built-in functions.&lt;/p&gt;
&lt;p&gt;In CPython, a built-in like &lt;code class="docutils literal"&gt;time.time&lt;/code&gt; is a &lt;a class="reference external" href="https://github.com/python/cpython/blob/5ffefdb108094a44e84345561ab1f1b57ffda23f/Include/cpython/methodobject.h#L7-L14"&gt;&lt;code class="docutils literal"&gt;PyCFunctionObject&lt;/code&gt;&lt;/a&gt;.
Its &lt;code class="docutils literal"&gt;m_ml&lt;/code&gt; member points at a &lt;a class="reference external" href="https://docs.python.org/3/c-api/structures.html#c.PyMethodDef"&gt;&lt;code class="docutils literal"&gt;PyMethodDef&lt;/code&gt;&lt;/a&gt; struct, and that struct’s &lt;code class="docutils literal"&gt;ml_meth&lt;/code&gt; member is the C function pointer that actually gets called.&lt;/p&gt;
&lt;p&gt;That pointer is writable, so time-machine’s &lt;code class="docutils literal"&gt;patch()&lt;/code&gt; overwrites it, saving the original first (&lt;a class="reference external" href="https://github.com/adamchainz/time-machine/blob/206d32e062f8e2c78dfa25f0d3c02fe3a5112092/src/_time_machine.c#L812-L815"&gt;source&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;time_time&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m_ml&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ml_meth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_time_machine_time&lt;/span&gt;&lt;span class="p"&gt;)&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;&lt;span class="n"&gt;original_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;time_time&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m_ml&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ml_meth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;time_time&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m_ml&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ml_meth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_time_machine_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s the whole technique, repeated for the ten functions that read the clock, such as &lt;code class="docutils literal"&gt;datetime.now()&lt;/code&gt; and &lt;code class="docutils literal"&gt;time.time_ns()&lt;/code&gt;.
Ten pointer writes, no matter how big your project is.&lt;/p&gt;
&lt;p&gt;Thus we can say time-machine has &lt;strong&gt;O(1)&lt;/strong&gt; runtime complexity: its runtime is constant, no matter how many modules are loaded or how many attributes they have.&lt;/p&gt;
&lt;p&gt;This process also fixes the two sources of leakiness in freezegun discussed above:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;The swap is done at the C layer, so every reference to the function object, no matter where it lives, sees the new behaviour.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;The types of date and time functions are unchanged:&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;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time_machine&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;
&lt;span class="go"&gt;(&amp;#39;datetime&amp;#39;, &amp;#39;time&amp;#39;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;time_machine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;travel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2020-01-01&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&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;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&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;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&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;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;datetime time 1577836800.0&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="migrate-today-with-time-machines-migration-cli"&gt;
&lt;h2&gt;Migrate today  with time-machine’s migration CLI&lt;a class="headerlink" href="#migrate-today-with-time-machines-migration-cli" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If the numbers above look temptign to shave seconds or minutes from your test suite, time-machine makes it easier to migrate with its &lt;a class="reference external" href="https://time-machine.readthedocs.io/en/latest/migration.html#migration-cli"&gt;migration CLI&lt;/a&gt; that does some of the work for you.
Run it with &lt;a class="reference external" href="https://docs.astral.sh/uv/concepts/tools/"&gt;&lt;code class="docutils literal"&gt;uvx&lt;/code&gt;&lt;/a&gt;, pointed at your test files:&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;--from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;time-machine[cli]&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;time_machine&lt;span class="w"&gt; &lt;/span&gt;migrate&lt;span class="w"&gt; &lt;/span&gt;tests/test_delorean.py
&lt;span class="go"&gt;Rewriting tests/test_delorean.py&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Given this file:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dt&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;freezegun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;freeze_time&lt;/span&gt;


&lt;span class="nd"&gt;@freeze_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1955-11-05 01:22&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_delorean&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;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&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;1955-11-05&amp;quot;&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_clock_tower&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;freeze_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1955-11-12 22:04&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;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…the tool rewrites the import, the decorator, and the context manager:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;import datetime as dt

&lt;span class="gd"&gt;-from freezegun import freeze_time&lt;/span&gt;
&lt;span class="gi"&gt;+import time_machine&lt;/span&gt;


&lt;span class="gd"&gt;-@freeze_time(&amp;quot;1955-11-05 01:22&amp;quot;)&lt;/span&gt;
&lt;span class="gi"&gt;+@time_machine.travel(&amp;quot;1955-11-05 01:22&amp;quot;, tick=False)&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;def test_delorean():
&lt;span class="w"&gt; &lt;/span&gt;    assert dt.date.today().isoformat() == &amp;quot;1955-11-05&amp;quot;


&lt;span class="w"&gt; &lt;/span&gt;def test_clock_tower():
&lt;span class="gd"&gt;-    with freeze_time(&amp;quot;1955-11-12 22:04&amp;quot;):&lt;/span&gt;
&lt;span class="gi"&gt;+    with time_machine.travel(&amp;quot;1955-11-12 22:04&amp;quot;, tick=False):&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;        assert dt.datetime.now().hour == 22
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note &lt;code class="docutils literal"&gt;tick=False&lt;/code&gt; appearing where freezegun didn’t pass &lt;code class="docutils literal"&gt;tick&lt;/code&gt;.
freezegun freezes time by default, whilst time-machine lets it tick, so the tool spells out the old behaviour to keep your tests passing.
Once migrated, dropping &lt;code class="docutils literal"&gt;tick=False&lt;/code&gt; where you can is worthwhile—time that advances is more realistic, although it means writing assertions against ranges rather than exact values.&lt;/p&gt;
&lt;p&gt;The CLI does partial replacements, so it can leave a file in a broken state, such as an unused &lt;code class="docutils literal"&gt;freezegun&lt;/code&gt; import next to a call it couldn’t rewrite.
Run it from a clean commit and lean on your linters to find the leftovers.
&lt;a class="reference external" href="https://docs.astral.sh/ruff/"&gt;Ruff&lt;/a&gt;’s &lt;code class="docutils literal"&gt;F401&lt;/code&gt; and &lt;code class="docutils literal"&gt;F821&lt;/code&gt; rules catch most of them.&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;O(1) is where the heart is,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: time-machine 3.3.0 lets your tests time-travel even faster</title><link href="https://adamj.eu/tech/2026/07/31/python-time-machine-3.3.0/" rel="alternate"/><published>2026-07-31T00:00:00+01:00</published><updated>2026-07-31T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-07-31:/tech/2026/07/31/python-time-machine-3.3.0/</id><summary type="html">&lt;p&gt;&lt;a class="reference external" href="https://time-machine.readthedocs.io/en/latest/"&gt;time-machine&lt;/a&gt; is my library for mocking the current date and time in Python tests.
I’ve just released version 3.3.0 with a decent number of changes, so here’s a lovely summary for you.&lt;/p&gt;
&lt;p&gt;Despite doing one simple-to-describe task, time-machine has ended up being quite a big library …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a class="reference external" href="https://time-machine.readthedocs.io/en/latest/"&gt;time-machine&lt;/a&gt; is my library for mocking the current date and time in Python tests.
I’ve just released version 3.3.0 with a decent number of changes, so here’s a lovely summary for you.&lt;/p&gt;
&lt;p&gt;Despite doing one simple-to-describe task, time-machine has ended up being quite a big library, thanks to the various complexities of reading time and API changes and optimizations in Python.
I’m grateful for all the help I’ve received through open source contributions, it really makes it more manageable.&lt;/p&gt;
&lt;div class="section" id="python-3-15-support"&gt;
&lt;h2&gt;Python 3.15 support&lt;a class="headerlink" href="#python-3-15-support" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Python 3.15 is currently in beta, due in October 2026, but time-machine 3.3.0 already supports it (wheels pending the release candidate).
I try to get time-machine ready for new Python versions as soon as possible because it is becoming more widely used, and projects like Fedora want to test all the packages they support against the new Python version early.&lt;/p&gt;
&lt;p&gt;This release required a bit more work than usual.
Historically, time-machine never needed to mock &lt;a class="reference external" href="https://docs.python.org/3/library/datetime.html#datetime.date.today"&gt;&lt;code class="docutils literal"&gt;datetime.date.today()&lt;/code&gt;&lt;/a&gt;, because CPython implemented it by calling &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;cls.fromtimestamp(time.time())&lt;/span&gt;&lt;/code&gt;, which time-machine already patched.
Python 3.15 added a fast path that reads the system clock directly (&lt;a class="reference external" href="https://github.com/python/cpython/pull/130980"&gt;CPython PR #130980&lt;/a&gt;), so time travel stopped affecting it.
time-machine now mocks &lt;code class="docutils literal"&gt;date.today()&lt;/code&gt; directly to restore the expected behaviour.&lt;/p&gt;
&lt;p&gt;Thanks to Miro Hrončok and Karolina Surma for the report in &lt;a class="reference external" href="https://github.com/adamchainz/time-machine/issues/610"&gt;Issue #610&lt;/a&gt;, Lumír ‘Frenzy’ Balhar for the fix in &lt;a class="reference external" href="https://github.com/adamchainz/time-machine/pull/618"&gt;PR #618&lt;/a&gt;, and Maurycy Pawłowski-Wieroński for review.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="faster-patched-functions"&gt;
&lt;h2&gt;Faster patched functions&lt;a class="headerlink" href="#faster-patched-functions" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When I first wrote time-machine, I knew I needed to write a C extension, but I wanted to avoid writing too much of it because I found it &lt;em&gt;pwetty scawy&lt;/em&gt;.
This meant that during time travel, the patched-in C functions would actually call into Python to compute the datetime value to return.
This behaviour made things easy to implement, but it meant extra overhead on basic calls like &lt;code class="docutils literal"&gt;time.time()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now, with the power of LLM assistance and a rigorously built-up test suite, I can bravely read and write more C code, so I was able to convert those functions from Python to C, in &lt;a class="reference external" href="https://github.com/adamchainz/time-machine/pull/643"&gt;PR #643&lt;/a&gt;.
The results are much more verbose, but faster.&lt;/p&gt;
&lt;p&gt;As an example, here’s how the intermediary function calculating a result for &lt;code class="docutils literal"&gt;datetime.datetime.now()&lt;/code&gt; looked in Python:&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="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&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;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&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;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…and in C:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="cm"&gt;/* datetime.datetime.now() */&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="nf"&gt;_time_machine_now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PyTypeObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Py_ssize_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;kwnames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Py_None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Py_ssize_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nkwargs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kwnames&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PyTuple_GET_SIZE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kwnames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Py_ssize_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nkwargs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&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;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PyTuple_GET_ITEM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kwnames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PyUnicode_CompareWithASCIIString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;tz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&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;&lt;span class="n"&gt;PyErr_Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;PyExc_TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;now() got an unexpected keyword argument &amp;#39;%U&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&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;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nargs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&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;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nargs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nkwargs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&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;&lt;span class="n"&gt;PyErr_Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;PyExc_TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;now() takes at most 1 argument (%zd given)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nargs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nkwargs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&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;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nargs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&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;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;args&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="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// cls.fromtimestamp(traveller_time, tz)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_time_machine_traveller_time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&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;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&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;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stack&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PyObject_VectorcallMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;str_fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PY_VECTORCALL_ARGUMENTS_OFFSET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Py_DECREF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&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;p&gt;Yeah… it takes a lot of extra work in C to even verify the arguments before even calling &lt;code class="docutils literal"&gt;cls.fromtimestamp()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At least the result is worth it, with a ~30% speedup, as demonstrated with this benchmark using &lt;a class="reference external" href="https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-timeit"&gt;IPython’s &lt;code class="docutils literal"&gt;%timeit&lt;/code&gt; magic&lt;/a&gt;:&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="o"&gt;%%timeit&lt;/span&gt;
   &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time_machine&lt;/span&gt;
   &lt;span class="o"&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;time_machine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;travel&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="o"&gt;...&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;1_000_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="o"&gt;...&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;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Result before:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;418 ms ± 4.49 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Result after:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;299 ms ± 9.38 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This change also unblocked a couple of bug fixes:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;The &lt;code class="docutils literal"&gt;DeprecationWarning&lt;/code&gt; from &lt;a class="reference external" href="https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow"&gt;&lt;code class="docutils literal"&gt;datetime.utcnow()&lt;/code&gt;&lt;/a&gt; on Python 3.12+ is no longer hidden while time travelling.
time-machine now emits a copy of the warning itself from its patched &lt;code class="docutils literal"&gt;utcnow()&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;Calling &lt;code class="docutils literal"&gt;now()&lt;/code&gt; or &lt;code class="docutils literal"&gt;utcnow()&lt;/code&gt; on a &lt;code class="docutils literal"&gt;datetime&lt;/code&gt; subclass now returns an instance of that subclass, rather than a plain &lt;code class="docutils literal"&gt;datetime.datetime&lt;/code&gt; instance.
This was hard to fix with the Python-level intermediary function, because it did not receive the calling type as a parameter, but the C-level intermediary function does.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="subinterpreter-support"&gt;
&lt;h2&gt;Subinterpreter support&lt;a class="headerlink" href="#subinterpreter-support" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Python 3.14 shipped &lt;a class="reference external" href="https://docs.python.org/3.14/library/concurrent.interpreters.html"&gt;&lt;code class="docutils literal"&gt;concurrent.interpreters&lt;/code&gt;&lt;/a&gt;, which allows multiple Python interpreters to run in one process, allowing a new model of parallelization.
Back in 2021, for version 2.2.0, I moved time-machine’s C module to multi-phase initialization, which was “future-proofing” for the day CPython supported subinterpreters.
However, it was not sufficient, and time-machine would error in an isolated subinterpreter when another interpreter was time-travelling.&lt;/p&gt;
&lt;p&gt;The fixes in this version fix those errors, enabling each interpreter to run its own time travel independently of the others.
Hopefully this change unblocks experimentation with using subinterpreters to run tests in parallel.&lt;/p&gt;
&lt;p&gt;There are probably still some edge cases to fix, but at least the basic functionality is there and tested.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="migration-cli"&gt;
&lt;h2&gt;Migration CLI&lt;a class="headerlink" href="#migration-cli" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I built time-machine as an optimized successor to &lt;a class="reference external" href="https://github.com/spulec/freezegun"&gt;freezegun&lt;/a&gt; (it’s &lt;a class="reference external" href="/tech/2021/02/19/freezegun-versus-time-machine/"&gt;way faster&lt;/a&gt;).
To help you migrate test suites from freezegun to time-machine (a current client task!), time-machine ships with a &lt;a class="reference external" href="https://time-machine.readthedocs.io/en/latest/migration.html#migration-cli"&gt;Migration CLI&lt;/a&gt; that rewrites your code as best it can.&lt;/p&gt;
&lt;p&gt;The CLI gained some new capabilities in this release.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First,&lt;/strong&gt; it now handles calls that pass &lt;code class="docutils literal"&gt;tick&lt;/code&gt;, which it previously left alone:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gd"&gt;-@freeze_time(&amp;quot;2023-01-01&amp;quot;, tick=True)&lt;/span&gt;
&lt;span class="gi"&gt;+@time_machine.travel(&amp;quot;2023-01-01&amp;quot;, tick=True)&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;def test_ticking():
&lt;span class="w"&gt; &lt;/span&gt;    ...
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(Where &lt;code class="docutils literal"&gt;tick&lt;/code&gt; isn’t passed, the tool adds &lt;code class="docutils literal"&gt;tick=False&lt;/code&gt;, matching freezegun’s frozen-by-default behaviour.)&lt;/p&gt;
&lt;p&gt;Thanks to George-Cristian Birzan for the report in &lt;a class="reference external" href="https://github.com/adamchainz/time-machine/issues/609"&gt;Issue #609&lt;/a&gt; and tanren for the fix in &lt;a class="reference external" href="https://github.com/adamchainz/time-machine/pull/636"&gt;PR #636&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second,&lt;/strong&gt; it also rewrites decorators on async functions, another case it used to skip:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gd"&gt;-@freeze_time(&amp;quot;2023-01-01&amp;quot;)&lt;/span&gt;
&lt;span class="gi"&gt;+@time_machine.travel(&amp;quot;2023-01-01&amp;quot;, tick=False)&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;async def test_async():
&lt;span class="w"&gt; &lt;/span&gt;    ...
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Thanks to George-Cristian Birzan for the report in &lt;a class="reference external" href="https://github.com/adamchainz/time-machine/issues/608"&gt;Issue #608&lt;/a&gt; and Sanjay Santhanam for the fix in &lt;a class="reference external" href="https://github.com/adamchainz/time-machine/pull/640"&gt;PR #640&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third,&lt;/strong&gt; from-imports that pull in &lt;code class="docutils literal"&gt;freeze_time&lt;/code&gt; alongside other names now get split, keeping those other names importing from freezegun:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gd"&gt;-from freezegun import freeze_time, FakeDate&lt;/span&gt;
&lt;span class="gi"&gt;+import time_machine&lt;/span&gt;
&lt;span class="gi"&gt;+from freezegun import FakeDate&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Fourth,&lt;/strong&gt; a bug fix: relative imports to modules that happen to be called &lt;code class="docutils literal"&gt;freezegun&lt;/code&gt; name are now left alone.
Previously, a line like this would get rewritten to &lt;code class="docutils literal"&gt;import time_machine&lt;/code&gt;, even though it imports from your a local 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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;.freezegun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;freeze_time&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This might happen if you have a wrapper module, which probably needs its own consideration for migration, so the CLI should leave it alone.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="wheels"&gt;
&lt;h2&gt;Wheels&lt;a class="headerlink" href="#wheels" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Two changes to what ships on PyPI:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;No more wheels for 32-bit Linux and Windows.
I realized these platforms are vanishingly rare at this point, and many larger projects have dropped their 32-bit support, so I’m following suit.&lt;/li&gt;
&lt;li&gt;No more wheels for free-threaded Python 3.13, since &lt;a class="reference external" href="https://iscinumpy.dev/post/cibuildwheel-4-0-0/"&gt;cibuildwheel 4.0.0 dropped support for building them&lt;/a&gt;.
Free-threaded Python 3.14+ is unaffected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Wheels are also now built with frame pointers enabled, per the &lt;a class="reference external" href="https://docs.python.org/dev/whatsnew/3.15.html#whatsnew315-frame-pointers"&gt;PEP 831 change in Python 3.15&lt;/a&gt;.
&lt;strong&gt;Frame pointers&lt;/strong&gt; are a debugging feature for low-level languages like C, which allow debuggers to reconstruct the call stack of a program.
Python has decided to enable frame pointers by default and encourage the ecosystem to do so as well, to make debugging easier for us all, so time-machine is joining the party.&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 upgrade today and let me know how it goes!&lt;/p&gt;
&lt;p&gt;Are your tests &lt;em&gt;still&lt;/em&gt; running?&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: fix ValueError: day of month directive '%d' may not be used without a year directive</title><link href="https://adamj.eu/tech/2026/07/29/python-fix-valueerror-day-of-month-directive-may-not-be-used-without-a-year-directive/" rel="alternate"/><published>2026-07-29T00:00:00+01:00</published><updated>2026-07-29T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-07-29:/tech/2026/07/29/python-fix-valueerror-day-of-month-directive-may-not-be-used-without-a-year-directive/</id><summary type="html">&lt;p&gt;Take this code, parsing the date of an annually recurring event that only has a month and day, such as a work anniversary:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;parse_anniversary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_str&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="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;/%m&amp;quot;&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;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With Python 3.13 or 3.14 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Take this code, parsing the date of an annually recurring event that only has a month and day, such as a work anniversary:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;parse_anniversary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_str&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="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;/%m&amp;quot;&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;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With Python 3.13 or 3.14, run it on January 1st, and it will log a warning, but still parse the date:&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;parse_anniversary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;01/01&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;/.../example.py:7: DeprecationWarning: Parsing dates involving a day of month without a year specified is ambiguous&lt;/span&gt;
&lt;span class="go"&gt;and fails to parse leap day. The default behavior will change in Python 3.15&lt;/span&gt;
&lt;span class="go"&gt;to either always raise an exception or to use a different default year (TBD).&lt;/span&gt;
&lt;span class="go"&gt;To avoid trouble, add a specific year to the input &amp;amp; format.&lt;/span&gt;
&lt;span class="go"&gt;See https://github.com/python/cpython/issues/70647.&lt;/span&gt;
&lt;span class="go"&gt;  parsed = datetime.strptime(date_str, &amp;quot;%d/%m&amp;quot;)&lt;/span&gt;
&lt;span class="go"&gt;(1, 1)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note the returned value’s year is 1900.&lt;/p&gt;
&lt;p&gt;But, parse February 29th, and the warning is followed by a &lt;code class="docutils literal"&gt;ValueError&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;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;parse_anniversary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;29/02&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;/.../example.py:7: DeprecationWarning: Parsing dates involving a day of month without a year specified is ambiguous&lt;/span&gt;
&lt;span class="go"&gt;and fails to parse leap day...&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;ValueError&lt;/span&gt;: &lt;span class="n"&gt;day is out of range for month&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code class="docutils literal"&gt;ValueError&lt;/code&gt; isn’t new: without a year, &lt;code class="docutils literal"&gt;strptime()&lt;/code&gt; has always filled in 1900 behind the scenes, which isn’t a leap year, so parsing “29 February” has always failed, on every Python version.
The new part is the warning, telling us this whole approach is on its way out.&lt;/p&gt;
&lt;p&gt;On Python 3.15+, any call to &lt;code class="docutils literal"&gt;strptime()&lt;/code&gt; with a &lt;code class="docutils literal"&gt;%d&lt;/code&gt; in the format string, without a year, will raise a &lt;code class="docutils literal"&gt;ValueError&lt;/code&gt; immediately, no matter the input:&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;parse_anniversary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;01/01&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;ValueError&lt;/span&gt;: &lt;span class="n"&gt;Day of month directive &amp;#39;%d&amp;#39; may not be used without a year directive. Parsing dates involving a day of month without a year is ambiguous and fails to parse leap day. Add a year to the input and format. See https://github.com/python/cpython/issues/70647.&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;From the &lt;a class="reference external" href="https://docs.python.org/3.15/whatsnew/3.15.html#:~:text=strptime%28%29%20now%20raises%20ValueError"&gt;release notes&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;code class="docutils literal"&gt;strptime()&lt;/code&gt; now raises &lt;code class="docutils literal"&gt;ValueError&lt;/code&gt; when the format string contains &lt;code class="docutils literal"&gt;%d&lt;/code&gt; (day of month) without a year directive.
This has been deprecated since Python 3.13.
(Contributed by Stan Ulbrych and Gregory P. Smith in &lt;a class="reference external" href="https://github.com/python/cpython/issues/70647"&gt;gh-70647&lt;/a&gt;.)&lt;/blockquote&gt;
&lt;p&gt;It seems like a fair trade-off to break this usage completely, rather than let it continue to hide around in code bases and only fail in the rare case that it is handed February 29th.
But it does mean any such call sites need fixing…&lt;/p&gt;
&lt;div class="section" id="the-fix"&gt;
&lt;h2&gt;The fix&lt;a class="headerlink" href="#the-fix" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Add a year to both the format string and input.
If you do have a specific year, use it, otherwise add a leap year (2000 works) and ignore the year in the returned value:&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;parse_anniversary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_str&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="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;date_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;date_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/2000&amp;quot;&lt;/span&gt;  &lt;span class="c1"&gt;# Add leap year to input&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;/%m/%Y&amp;quot;&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;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will work fine on all Python versions for all dates, including February 29th:&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;parse_anniversary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;01/01&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(1, 1)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;parse_anniversary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;29/02&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(29, 2)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&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 all of your directives be unambiguous,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: fix TypeError: NotImplemented should not be used in a boolean context</title><link href="https://adamj.eu/tech/2026/07/28/python-fix-typeerror-notimplemented-boolean-context/" rel="alternate"/><published>2026-07-28T00:00:00+01:00</published><updated>2026-07-28T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-07-28:/tech/2026/07/28/python-fix-typeerror-notimplemented-boolean-context/</id><summary type="html">&lt;p&gt;Take this class, which implements &lt;code class="docutils literal"&gt;__eq__()&lt;/code&gt; and derives &lt;code class="docutils literal"&gt;__ne__()&lt;/code&gt; from it:&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="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Lemming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&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;name&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__eq__&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;other&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Lemming&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;NotImplemented&lt;/span&gt;
        &lt;span class="k"&gt;return&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;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__ne__&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;other&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__eq__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This technique was common on …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Take this class, which implements &lt;code class="docutils literal"&gt;__eq__()&lt;/code&gt; and derives &lt;code class="docutils literal"&gt;__ne__()&lt;/code&gt; from it:&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="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Lemming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&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;name&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__eq__&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;other&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Lemming&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;NotImplemented&lt;/span&gt;
        &lt;span class="k"&gt;return&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;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__ne__&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;other&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__eq__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This technique was common on Python 2, before Python 3.0 started deriving &lt;code class="docutils literal"&gt;__ne__()&lt;/code&gt; automatically from &lt;code class="docutils literal"&gt;__eq__()&lt;/code&gt; (&lt;a class="reference external" href="https://docs.python.org/3/whatsnew/3.0.html#operators-and-special-methods:~:text=%21%3D%20now%20returns%20the%20opposite%20of%20%3D%3D%2C"&gt;release note&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Now compare a &lt;code class="docutils literal"&gt;Lemming&lt;/code&gt; with something that isn’t one:&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Lemming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Lenny&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;p&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Lenny&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;False&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ah, woops, that’s wrong.
A &lt;code class="docutils literal"&gt;Lemming&lt;/code&gt; is not equal to the plain string &lt;code class="docutils literal"&gt;&amp;quot;Lenny&amp;quot;&lt;/code&gt;, so &lt;code class="docutils literal"&gt;!=&lt;/code&gt; should return &lt;code class="docutils literal"&gt;True&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If we run the same code on Python 3.9–3.13 with &lt;a class="reference external" href="https://docs.python.org/3/using/cmdline.html#cmdoption-W"&gt;warnings enabled&lt;/a&gt;, we can see why:&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;-W&lt;span class="w"&gt; &lt;/span&gt;default&lt;span class="w"&gt; &lt;/span&gt;example.py
&lt;span class="go"&gt;/.../example.py:12: DeprecationWarning: NotImplemented should not be used in a boolean context&lt;/span&gt;
&lt;span class="go"&gt;  return not self.__eq__(other)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And on Python 3.14+, it’s no longer just a warning:&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;p&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Lenny&amp;quot;&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;NotImplemented should not be used in a boolean context&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What’s going on?&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;other&lt;/code&gt; is a string, not a &lt;code class="docutils literal"&gt;Lemming&lt;/code&gt;, so &lt;code class="docutils literal"&gt;self.__eq__(other)&lt;/code&gt; hits the &lt;code class="docutils literal"&gt;isinstance()&lt;/code&gt; check and returns the special singleton &lt;code class="docutils literal"&gt;NotImplemented&lt;/code&gt;, rather than &lt;code class="docutils literal"&gt;True&lt;/code&gt; or &lt;code class="docutils literal"&gt;False&lt;/code&gt;.
&lt;code class="docutils literal"&gt;__ne__()&lt;/code&gt; then does &lt;code class="docutils literal"&gt;not NotImplemented&lt;/code&gt;, feeding that singleton into a boolean context.&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;NotImplemented&lt;/code&gt; isn’t &lt;code class="docutils literal"&gt;False&lt;/code&gt;, &lt;code class="docutils literal"&gt;None&lt;/code&gt;, or empty, so like most objects, it’s truthy by default.
That means &lt;code class="docutils literal"&gt;not NotImplemented&lt;/code&gt; evaluates to &lt;code class="docutils literal"&gt;False&lt;/code&gt;, and that &lt;code class="docutils literal"&gt;False&lt;/code&gt; becomes the (wrong) result of our &lt;code class="docutils literal"&gt;!=&lt;/code&gt; comparison.
This “worked” only by accident: &lt;code class="docutils literal"&gt;NotImplemented&lt;/code&gt; is meant to be returned &lt;em&gt;from&lt;/em&gt; a comparison method, to tell Python “try something else”, not to be evaluated as a &lt;code class="docutils literal"&gt;bool&lt;/code&gt; itself.&lt;/p&gt;
&lt;p&gt;The warning was added in Python 3.9, per the &lt;a class="reference external" href="https://docs.python.org/3.9/whatsnew/3.9.html#deprecated:~:text=Using%20NotImplemented"&gt;release note&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
Using &lt;code class="docutils literal"&gt;NotImplemented&lt;/code&gt; in a boolean context has been deprecated, as it is almost exclusively the result of incorrect rich comparator implementations.
It will be made a &lt;code class="docutils literal"&gt;TypeError&lt;/code&gt; in a future version of Python.
(Contributed by Josh Rosenberg in &lt;a class="reference external" href="https://bugs.python.org/issue35712"&gt;bpo-35712&lt;/a&gt;.)&lt;/blockquote&gt;
&lt;p&gt;Five years later, Python 3.14 delivered on that promise and turned the warning into a &lt;code class="docutils literal"&gt;TypeError&lt;/code&gt;, per the &lt;a class="reference external" href="https://docs.python.org/3.14/whatsnew/3.14.html#removed:~:text=Using%20NotImplemented"&gt;release note&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
Using &lt;code class="docutils literal"&gt;NotImplemented&lt;/code&gt; in a boolean context will now raise a &lt;code class="docutils literal"&gt;TypeError&lt;/code&gt;.
This has raised a &lt;code class="docutils literal"&gt;DeprecationWarning&lt;/code&gt; since Python 3.9.
(Contributed by Jelle Zijlstra in &lt;a class="reference external" href="https://github.com/python/cpython/issues/118767"&gt;gh-118767&lt;/a&gt;.)&lt;/blockquote&gt;
&lt;p&gt;So rather than silently giving the wrong answer, our &lt;code class="docutils literal"&gt;Lemming&lt;/code&gt; example now fails loudly, right at the point where the mistake happens.&lt;/p&gt;
&lt;div class="section" id="the-fix"&gt;
&lt;h2&gt;The fix&lt;a class="headerlink" href="#the-fix" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The fix for the above &lt;code class="docutils literal"&gt;Lemming&lt;/code&gt; class is to remove the &lt;code class="docutils literal"&gt;__ne__()&lt;/code&gt; method entirely:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gd"&gt;-    def __ne__(self, other):&lt;/span&gt;
&lt;span class="gd"&gt;-        return not self.__eq__(other)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then we’ll see:&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Lemming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Lenny&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;p&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Lenny&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Since Python 3, &lt;code class="docutils literal"&gt;object&lt;/code&gt; provides a default &lt;code class="docutils literal"&gt;__ne__()&lt;/code&gt; that calls &lt;code class="docutils literal"&gt;__eq__()&lt;/code&gt; and correctly inverts its result, whilst still propagating &lt;code class="docutils literal"&gt;NotImplemented&lt;/code&gt; onwards so Python can try the other object’s reflected method, or fall back to identity comparison.
There is rarely a need to write a &lt;code class="docutils literal"&gt;__ne__()&lt;/code&gt; method by hand anymore, unless you have specific custom behaviour that isn’t the inverse of &lt;code class="docutils literal"&gt;__eq__()&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="another-case"&gt;
&lt;h2&gt;Another case&lt;a class="headerlink" href="#another-case" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This bug isn’t confined to hand-written classes.
For years, a “clever” one-liner for dropping &lt;code class="docutils literal"&gt;None&lt;/code&gt; values from a list was:&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;values&lt;/span&gt; &lt;span class="o"&gt;=&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="kc"&gt;None&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="kc"&gt;None&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="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="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__ne__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;[1, 2, 3]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This looks like it filters out &lt;code class="docutils literal"&gt;None&lt;/code&gt;, and on Python 3.8 and earlier, it does, but only because two “wrongs” made a right:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;code class="docutils literal"&gt;None.__ne__(other)&lt;/code&gt; returns &lt;code class="docutils literal"&gt;NotImplemented&lt;/code&gt; for any &lt;code class="docutils literal"&gt;other&lt;/code&gt; value that isn’t &lt;code class="docutils literal"&gt;None&lt;/code&gt; itself.&lt;/li&gt;
&lt;li&gt;&lt;code class="docutils literal"&gt;filter()&lt;/code&gt; then checks the truthiness of that return value directly, and &lt;code class="docutils literal"&gt;NotImplemented&lt;/code&gt; is truthy, so non-&lt;code class="docutils literal"&gt;None&lt;/code&gt; items pass through.
For &lt;code class="docutils literal"&gt;None&lt;/code&gt; itself, &lt;code class="docutils literal"&gt;None.__ne__(None)&lt;/code&gt; correctly returns &lt;code class="docutils literal"&gt;False&lt;/code&gt;, so it’s filtered out.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As above, this technique triggers a warning from Python 3.9, and raises &lt;code class="docutils literal"&gt;TypeError&lt;/code&gt; from Python 3.14.&lt;/p&gt;
&lt;p&gt;The fix is to write the check explicitly in a generator expression or list comprehension, as appropriate:&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="go"&gt;[1, 2, 3]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This technique is also about 15 times faster than the &lt;code class="docutils literal"&gt;filter()&lt;/code&gt; version, since it avoids the overhead of calling a method (&lt;code class="docutils literal"&gt;None.__ne__()&lt;/code&gt;) for every item in the list.&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 code be modernized and swift,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/></entry><entry><title>Python: inspect interleaved unittest.mock calls with attached mocks</title><link href="https://adamj.eu/tech/2026/07/27/python-mock-attach-mock/" rel="alternate"/><published>2026-07-27T00:00:00+01:00</published><updated>2026-07-27T00:00:00+01:00</updated><author><name>Adam Johnson</name></author><id>tag:adamj.eu,2026-07-27:/tech/2026/07/27/python-mock-attach-mock/</id><summary type="html">&lt;p&gt;When you use a &lt;code class="docutils literal"&gt;unittest.mock&lt;/code&gt; mock, you often make assertions on the calls it received, such as through the &lt;code class="docutils literal"&gt;mock_calls&lt;/code&gt; list.
But sometimes you want to assert on the order of calls across &lt;em&gt;multiple&lt;/em&gt; mocked functions, for example to check that steps in a process happen in the right …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When you use a &lt;code class="docutils literal"&gt;unittest.mock&lt;/code&gt; mock, you often make assertions on the calls it received, such as through the &lt;code class="docutils literal"&gt;mock_calls&lt;/code&gt; list.
But sometimes you want to assert on the order of calls across &lt;em&gt;multiple&lt;/em&gt; mocked functions, for example to check that steps in a process happen in the right sequence.&lt;/p&gt;
&lt;p&gt;To do this, use &lt;strong&gt;attached mocks&lt;/strong&gt;, which let you collect calls from multiple mocks into a single timeline.
A parent mock has multiple child mocks attached to it, and its &lt;code class="docutils literal"&gt;mock_calls&lt;/code&gt; list records calls from all of its children together, interleaved in the order they actually happened.
Sometimes that attachment happens automatically, and sometimes you have to set it up yourself.&lt;/p&gt;
&lt;p&gt;Say we have a &lt;code class="docutils literal"&gt;Kettle&lt;/code&gt; class, and a function that fills one by opening the lid, adding water, and closing the lid again:&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="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Kettle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;open&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="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;add_water&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;litres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;close&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="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kettle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Kettle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;litres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;kettle&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;kettle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_water&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;litres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;kettle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To test that &lt;code class="docutils literal"&gt;fill()&lt;/code&gt; calls those three methods in order, pass in a mock &lt;code class="docutils literal"&gt;Kettle&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;unittest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&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;mock&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Kettle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fill&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FillTests&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="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_fill_order&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;kettle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_autospec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Kettle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&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="n"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kettle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&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;kettle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mock_calls&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&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;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_water&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&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;p&gt;&lt;a class="reference external" href="https://docs.python.org/3/library/unittest.mock.html#unittest.mock.create_autospec"&gt;&lt;code class="docutils literal"&gt;mock.create_autospec()&lt;/code&gt;&lt;/a&gt; builds a mock that matches &lt;code class="docutils literal"&gt;Kettle&lt;/code&gt;’s shape, checking arguments against the real method signatures.
&lt;code class="docutils literal"&gt;instance=True&lt;/code&gt; makes it stand in for an instance of the class, rather than the class itself.&lt;/p&gt;
&lt;p&gt;The method mocks, like &lt;code class="docutils literal"&gt;open()&lt;/code&gt;, are created on access and automatically attached to the parent mock, &lt;code class="docutils literal"&gt;kettle&lt;/code&gt;.
That’s how &lt;code class="docutils literal"&gt;kettle.mock_calls&lt;/code&gt; contains all three calls in order, even though we never explicitly attached them, and a single assertion can check the whole sequence.&lt;/p&gt;
&lt;p&gt;This all works the same if you use &lt;strong&gt;spies&lt;/strong&gt; rather than plain mocks, by adding the &lt;code class="docutils literal"&gt;wraps&lt;/code&gt; argument, as covered in my &lt;a class="reference external" href="/tech/2026/07/25/python-spy-unittest-mock-wraps/"&gt;previous post&lt;/a&gt;.
That often makes a better test, since the test would then exercise the real behaviour of the wrapped object as well as recording the calls.&lt;/p&gt;
&lt;div class="section" id="separate-functions-need-attach-mock"&gt;
&lt;h2&gt;Separate functions need &lt;code class="docutils literal"&gt;attach_mock()&lt;/code&gt;&lt;a class="headerlink" href="#separate-functions-need-attach-mock" title="Permalink to this headline"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Automatic attachment only helps when the calls you care about are already methods on one object.
Say &lt;code class="docutils literal"&gt;fill_kettle()&lt;/code&gt; instead calls three separate module-level functions:&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="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;open_lid&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="o"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;fill_water&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;litres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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="o"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;close_lid&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="o"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;fill_kettle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;litres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;open_lid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fill_water&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;litres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;close_lid&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;open_lid&lt;/code&gt;, &lt;code class="docutils literal"&gt;fill_water&lt;/code&gt;, and &lt;code class="docutils literal"&gt;close_lid&lt;/code&gt; are entirely separate functions, so mocking them separately gives you three separate mocks, each with its own independent &lt;code class="docutils literal"&gt;mock_calls&lt;/code&gt;, and no shared parent to collect them together.
This is where &lt;a class="reference external" href="https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.attach_mock"&gt;&lt;code class="docutils literal"&gt;Mock.attach_mock()&lt;/code&gt;&lt;/a&gt; comes in: use it to attach each mock to a bare parent mock.&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="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;unittest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&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;mock&lt;/span&gt;

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


&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FillKettleTests&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="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_fill_kettle_order&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="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;open_lid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autospec&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;as&lt;/span&gt; &lt;span class="n"&gt;open_lid_mock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;fill_water&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autospec&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;as&lt;/span&gt; &lt;span class="n"&gt;fill_water_mock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;close_lid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autospec&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;as&lt;/span&gt; &lt;span class="n"&gt;close_lid_mock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach_mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open_lid_mock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;open_lid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach_mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fill_water_mock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;fill_water&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach_mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close_lid_mock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;close_lid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;fill_kettle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5&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;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mock_calls&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open_lid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fill_water&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close_lid&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;p&gt;&lt;code class="docutils literal"&gt;parent&lt;/code&gt; doesn’t mock anything itself, and so is never called.
It exists only to collect calls from its attached children.
Once attached, calls to any of the three mocks also appear on &lt;code class="docutils literal"&gt;parent.mock_calls&lt;/code&gt;, prefixed with the name each was attached under, interleaved in the order they actually happened.&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 not get too attached to your mocks,&lt;/p&gt;
&lt;p&gt;—Adam&lt;/p&gt;
&lt;/div&gt;
</content><category term="python"/><category term="unittest"/></entry></feed>