Adam Johnson

https://adamj.eu/tech

28 posts

Tech

Subscribe via RSS

  1. Zsh: manipulate filenames with modifiers like :r (root) and :e (extension)

    Zsh modifiers are a family of suffixes that transform words as they’re expanded. Several of them are dedicated to slicing up filenames, replacing fiddly calls to dirname, basename, and friends. Let’s take a look at them now! The filename modifiers Four modifiers cover most filename manipulation. Take this variable assignment to a path: $ f=talks/robot.png These modifiers each return a different part of the path: :h (“head”) keeps the directory part, like dirname: $ print ${f:h} talks Like…

    0
  2. Django: django-upgrade 1.32.0 out now, with 44 AI-assisted bug fixes

    django-upgrade is my tool for automatically upgrading your project code for new Django versions. It rewrites your Python files to fix deprecations and adopt some new features, taking a chunk of the monotony out of upgrading between Django versions. Yesterday, I released version 1.32.0, which fixes 44 bugs. Some are big, some are small, and all of them were found by Claude Fable, with this simple prompt: Find and fix bugs Yup, that’s it. Across two rounds of self-directed bug discovery, Claude…

    0
  3. Python: use re.prefixmatch() instead of re.match() from Python 3.15

    Take this validation function: import re TRAIN_NUMBER_RE = re.compile(r"\d{6}") # six digits def is_valid_train_number(value: str) -> bool: return bool(TRAIN_NUMBER_RE.match(value)) It looks reasonable, and it works for the intended cases: >>> is_valid_train_number("345071") True >>> is_valid_train_number("ABC123") False But, woah, it also accepts garbage suffixes: >>> is_valid_train_number("345071-in-abbey-wood") True That is a bug, totally not what the author intended. re.Pattern.match() (and…

    0
  4. Python: fix SyntaxWarning: invalid octal escape sequence

    Take this code: path = "C:\477_data" If we run this with Python 3.11+ in development mode (to enable deprecation warnings), we will see: $ python3.11 -X dev example.py /.../example.py:1: DeprecationWarning: invalid octal escape sequence '\477' path = "C:\477_data" On Python 3.12+, we instead get a SyntaxWarning which happens at compile time, even without development mode. We can see this by compiling the file with py_compile rather than running it: $ python3.12 -m py_compile example.py…

    0
  5. Python: fix TypeError: NamedTuple() got an unexpected keyword argument

    Take this code, defining a small NamedTuple with a keyword argument per field: from typing import NamedTuple Point = NamedTuple("Point", x=int, y=int) Run it on Python 3.13 or 3.14, and you’ll see: $ python3.14 example.py /.../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. Point = NamedTuple("Point", x=int, y=int) And on Python 3.15+, it’s broken: $…

    0
  6. Python: introducing emojet, a fast emoji lookup library

    New package just landed! emojet 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 emoji package, 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. Use emojize() to…

    0
  7. Python: tprof 1.3.0: now with less overhead

    Back in January, I introduced tprof, 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 -t and a script to run: $ tprof -t lib:maths ./example.py ... 🎯 tprof results: function calls total median ± σ min … max lib:maths() 2 610ms 305ms ± 2ms 304ms … 307ms Today, it’s my pleasure to announce tprof 1.3.0, a big release with the following changes. Way less…

    0
  8. Django: introducing django-msgspec

    It’s another day, another new package day here. Say hello to django-msgspec, a package of drop-in replacements for Django and Django REST Framework (DRF) components backed by msgspec. msgspec is a C-based serialization library covering JSON, MessagePack, YAML, and TOML, with optional schema validation through typed Struct classes. Its JSON encoder and decoder are several times faster than the standard library’s, which makes django-msgspec a cheap performance win in the parts of Django that…

    0
  9. Git: my git stash -p optimization in Git 2.55

    I got another commit merged in Git 2.55 (2026-06-29), yay! The release note reads: "git stash -p" has been optimized by reusing cached index entries in its temporary index, avoiding unnecessary lstat() calls on unchanged files. Here is the story of this change. Patch mode slowness The -p (--patch) option makes git stash interactive. Instead of stashing everything, Git walks you through your uncommitted changes hunk by hunk, asking whether to stash each one, using the same interactive machinery…

    0
  10. Zsh: select files with arbitrary code in (e) or + glob qualifiers

    Zsh glob qualifiers can select files by many built-in attributes: type, size, modification time, and more. But when no built-in qualifier fits, you can bring in the e or + qualifiers to run arbitrary code, making globs a fully programmable file filter. With great power comes great responsibility! The e syntax looks like this: extend your glob, like *, with (e:'CODE':), replacing CODE with your Zsh code to run. : is an arbitrary delimiter, and the quoting is needed to stop Zsh from expanding…

    0
  11. Git: list files at a given commit with ls-tree

    To list all files in your repository as they were at a given commit, use git ls-tree with its -r and --name-only options: $ git ls-tree --name-only -r <commit> Replace <commit> with any commit reference: a SHA, a branch name, a tag like above, a relative reference like @~ (the commit before last), and so on. For example, in a repository documenting puppy breeds: $ git ls-tree --name-only -r v1.0 README.md hound/beagle.md hound/dachshund.md pastoral/corgi.md sporting/golden-retriever.md…

    0
  12. Git: exclude commits by an author in git log with --perl-regexp

    git log has an --author option to limit the output to commits from matching authors: $ git log --author=<pattern> But there’s no option to invert the filter and show commits from all authors except those matching a pattern. Instead, you can build the negation into the pattern itself, using a regular expression with a negative lookahead, enabled with the --perl-regexp option: $ git log --author='^((?!<pattern>).)*$' --perl-regexp A breakdown of the regular expression: ^ matches the start of the…

    0
  13. Zsh: list recently modified files with (m0) glob qualifiers

    My downloads directory is a junk pile that I just never get around to fully clearing. So if I’ve just downloaded a bunch of files, I normally want to work with them and ignore the rest of the files. To do this, I’ve found it convenient to have Zsh select recently modified files with glob qualifiers, the extra syntax you can add to filename globs. The m qualifier selects files by modification time, measured in whole days by default. So m0 matches files modified zero whole days ago, that is,…

    0
  14. Python: how time-machine is O(1) where freezegun is O(n)

    time-machine is my library for mocking the current date and time in Python tests. Its headline advantage over freezegun, the library that inspired it, is speed. Back in 2021, I benchmarked the two libraries 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. Five years on, following many time-machine optimizations, including in Friday’s time-machine 3.3.0, let’s…

    0
  15. Python: time-machine 3.3.0 lets your tests time-travel even faster

    time-machine 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. 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. Python…

    0
  16. Python: fix ValueError: day of month directive '%d' may not be used without a year directive

    Take this code, parsing the date of an annually recurring event that only has a month and day, such as a work anniversary: from datetime import datetime def parse_anniversary(date_str: str) -> datetime: parsed = datetime.strptime(date_str, "%d/%m") return parsed.day, parsed.month With Python 3.13 or 3.14, run it on January 1st, and it will log a warning, but still parse the date: >>> parse_anniversary("01/01") /.../example.py:7: DeprecationWarning: Parsing dates involving a day of month without…

    0
  17. Python: fix TypeError: NotImplemented should not be used in a boolean context

    Take this class, which implements __eq__() and derives __ne__() from it: class Lemming: def __init__(self, name): self.name = name def __eq__(self, other): if not isinstance(other, Lemming): return NotImplemented return self.name == other.name def __ne__(self, other): return not self.__eq__(other) This technique was common on Python 2, before Python 3.0 started deriving __ne__() automatically from __eq__() (release note). Now compare a Lemming

    0
  18. Python: inspect unittest.mock calls with attached mocks

    When you use a unittest.mock mock, you often make assertions on the calls it received, such as through the mock_calls list. But sometimes you want to assert on the order of calls across multiple mocked functions, for example to check that steps in a process happen in the right sequence. To do this, use attached mocks, which let you collect calls from multiple mocks into a single timeline. A parent mock has multiple child mocks attached to it, and its mock_calls list records calls from all of…

    0
  19. Python: spy on function calls with unittest.mock’s wraps

    Testing terminology distinguishes between different kinds of test doubles, including: mocks, which replace the real behaviour of a function. spies, which wrap a function, recording calls for later assertions. Despite its name, unittest.mock is not just for mocks: it can also create spies, through the wraps argument of its Mock classes. Calls to such a Mock pass through to the wrapped object and return its real results, while the mock records the calls for later assertions. For example, say we…

    0
  20. Django: release code words up to 6.1

    Did you know that each Django release has a “code word” associated with it? It’s hidden in plain sight, in the announcement blog post describing the list of features coming in the next version. I think this is a lovely little tradition. I last covered the list back in 2021, for Django 3.2 (post). This post expands the table up until Django 6.1, which is expected next month (the first release candidate came out earlier this week). Each code word links to its Wiktionary entry so you can see the…

    0
  21. Django: introducing django-crawl

    I recently migrated one of my client projects from the legacy django-csp package to Django 6.0’s built-in Content Security Policy (CSP) support (release note). This security header is a powerful tool for preventing unwanted content from being loaded on your site, so configuration correctness is paramount. The migration was fairly straightforward, but a few pages had complicated overrides, so I wanted to be sure that no CSP headers had been changed by my swapping of CSP implementations. I had…

    0
  22. Django: introducing django-orjson

    Just as cars painted red are known to be faster, libraries implemented in Rust are also known to be faster. Today’s example is orjson, a Rusty replacement for Python’s built-in json module, boasting 10x faster serialization and 2x faster deserialization. Such a library is great, but adopting it isn’t easy, especially when your framework uses json in many different parts. To help Django developers adopt orjson, I have created django-orjson, which provides a whole bunch of drop-in replacements…

    0
  23. Python: find all instances of a class with gc.get_objects()

    Here’s a lovely hack that I’ve used when machete-mode debugging. Say you have a class, and you want to find its live instances, perhaps to check how many there are or what value a certain attribute has across them. Normally you'd have to alter the class initialization to store its instances in a collection, like a WeakSet, but making such edits slows down debugging and isn’t always possible. The approach in this post uses the garbage collector module, gc, to leverage its already-tracked…

    0
  24. Python: store extra data for objects in a WeakKeyDictionary

    In several programs, I’ve wanted to solve the problem of associating extra data with an object. For example, in django-upgrade, the individual “fixer” functions often want to store extra data per visited ast.Module object. A common pattern in Python is to store the data in an extra attribute directly on the object, like module._all_used_names = .... However, this approach has some downsides: The object may not allow arbitrary attributes, such as for built-in types like dict or slotted classes.…

    0
  25. Django: introducing django-integrity-policy

    Back in January, Firefox’s Security & Privacy Newsletter for 2025 Q4 piqued my interest with this mention: Integrity-Policy: Firefox 145 has added support for the Integrity-Policy response header. The header allows websites to ensure that only scripts with an integrity attribute will load. A new security header! That’s right up my street: I’ve cared about getting security headers right since 2018, when I created django-permissions-policy to set the Permissions-Policy header. (At the time, it…

    0
  26. Django: fixing a memory “leak” from Python 3.14’s incremental garbage collection

    Back in February, I encountered an out-of-memory error while migrating a client project to Python 3.14. The issue occurred when running Django’s database migration command (migrate) on a limited-resource server, and seemed to be caused by the new incremental garbage collection algorithm in Python 3.14. At the time, I wrote a workaround and started on this blog post, but other tasks took priority and I never got around to finishing it. But four days ago, Hugo van Kemenade, the Python 3.14…

    0
  27. Python: introducing profiling-explorer

    I’ve made another package! Like icu4py, which I made in February, it was sponsored by my client Rippling. And like tprof, which I made in January, it’s a profiling tool! profiling-explorer is a tool for exploring profiling data from Python’s built-in profilers, which are stored in pstats …

    0
  28. Python: introducing icu4py, bindings to the Unicode ICU library

    I made a new package! Thank you to my client Rippling for inspiring and sponsoring its development. ICU (International Components for Unicode) is Unicode’s official library for Unicode and Globalization tools. It’s a de-facto standard for handling text in a locale-aware way, used by many major projects, including …

    0