Posts whose title or text contains one of these words are left out of every view.
Whole words only: ai catches "AI-generated" but not "openai". Add a star for word beginnings: llm* catches llm and llms.
99 words is the limit
https://adamj.eu/tech
41 posts
Tech
Subscribe via RSS
Django projects end up slotting into a bunch of software release cycles: Django itself, Python, database servers, cache servers, and more. It can be hard for us developers to keep track of all of them, with their different support policies and end-of-life dates, and running an unsupported version means missing out on important security or bug fixes. norwegianblue is a handy command-line tool for checking software support dates. It fetches data from endoflife.date, a community-maintained…
When contributing to Django, you’ll want to run its test suite to check your changes. The official contributing docs cover setup in Running the unit tests, using a virtual environment and pip. Here’s an alternative guide using uv. Setup First, clone Django and change into the repository: $ git clone https://github.com/django/django $ cd django (You can fork it later if you want to make a pull request.) Then, create a virtual environment: $ uv venv Using CPython 3.14.6 Creating virtual…
Do you ever do this? You carefully type out a command, like this pytest one: $ pytest tests/test_checkout.py -k discount --reuse-db -x …then, just before pressing Enter, you remember that something will stop it from succeeding, like being on the wrong Git branch. What now? Maybe you run the command anyway, to put it in your history, letting it fail or cancelling it with Ctrl-C. Then you can run your setup command. Or maybe you press Ctrl-A to jump to the start, and add the setup command first,…
I’ve written a few posts on Zsh’s glob qualifiers recently, covering: (om[1]) to select the most recently modified file. (m0) to select files modified today. (e) and + to select files with arbitrary code. (OL) to find the largest files. (n) to sort in natural order. These single-character codes are powerful, but dense and forgettable. Luckily, you don’t need to memorize them at all exactly, because Zsh has a built-in cheat sheet: tab completion within the qualifier parentheses. Type a glob, an…
If your site has companion Apple or Android apps, you probably want links to your site to open in those apps, when installed. Both platforms support this, with Apple calling the feature Universal Links and Android calling it App Links. But an app can’t just claim to handle your links, or any malicious app could hijack them. Instead, both platforms require two-way association: the app declares which domains it handles, and each domain confirms which apps may handle its links. The domain’s side…
When a security researcher finds a vulnerability in your site, they need a way to tell you about it. Without a clear contact, they may resort to guessing at addresses like security@<yourdomain>, messaging random folks on social media, or give up. And of course, in the worst case, they might just publish the details, leaving you to find out when attackers do. security.txt is a web standard to fix this problem. It’s a small text file, served at the reserved path /.well-known/security.txt, that…
Globs sort filenames character by character, so numbered files often come out in a surprising order, with 10 before 2. Zsh has a neat fix for this: the n glob qualifier, which applies a “natural” sort, treating any run of digits as a number rather than a string of characters. I found this feature useful when building a PDF from multiple files.n I exported my multi-page document from Inkscape to one PNG per page, named Page 1.png, Page 2.png, and so on. I then stitched them into a single PDF…
I previously announced my Python optimization workshop in Lisbon, on 10th October (which still has a few places). I am now pleased to add a Python/Django meetup on the Thursday before. Here are the details: What: Python/Django meetup When: Thursday 8th October 2026, 18:00–20:00 Where: Terrace Restaurant, Praça Príncipe Perfeito, Lisbon Cost: Free Register on Luma Join us for an informal evening discussing Python, Django, and related topics (AI will surely come up). This meetup is hosted by Adam…
Got a directory that you want to tidy up bloat from? Whether you’re trying to find bad build artifacts in your work repository or clear out old cat videos from your home directory, Zsh’s (OL) glob qualifier can help you find the largest files. Using the Django source repository as an example, here’s how we can show the size of the five largest files: $ du -h **/*(.OL[1,5]) 696K tests/gis_tests/data/rasters/raster.numpy.txt 492K…
If you’re starting a long job on your Apple Macintosh Computer and wandering off for a coffee, the machine may go to sleep before it finishes due to inactivity. To prevent that from happening, also give your computer a boost with the caffeinate command, a macOS built-in since 2012 (OS X Mountain Lion). Basic usage Pass your command to caffeinate as arguments: $ caffeinate ./migrate.sh In this wrapper form, caffeinate will run your command and keep the machine awake for as long as it runs, then…
I’m running a workshop in Lisbon next month, and you’re invited! It’s a small, in-person, hands-on session on optimizing Python code, with a Django flavour. Here are the details: What: Python optimization workshop When: Saturday 10th October 2026, 10:00–13:00 Where: Martinhal Lisbon Oriente, Parque das Nações, Lisbon Cost: Free, but places are limited and registration is required Size: Up to 10 people Led by: Myself, Adam Johnson Organised by: Mafalda Marques Register on Luma What we’ll do In…
When a password manager detects that a user’s password has been leaked or reused, it can prompt them to change it, but the password change URL varies by site. The web’s answer to such discovery problems is the reserved /.well-known/ URL namespace (RFC 8615), home to machine-readable files and endpoints like security.txt. A Well-Known URL for Changing Passwords is the web specification that uses this namespace to fix password page discovery. It reserves the URL path /.well-known/change-password…
I made another package! Say hello to django-mcpz, for building Model Context Protocol (MCP) servers in your Django project. The package tagline is easy peasy MCP servers in Django. MCP, the place to be? MCP specifies a way for LLMs to interact with external data sources. MCP servers can expose a set of tools, which are essentially functions that an LLM can call on behalf of a user. So, for example, a user can ask “where’s my pizza at?” and the LLM can call one or more tools on your pizza shop…
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…
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…
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…
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…
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: $…
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…
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…
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…
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…
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…
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…
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…
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,…
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…
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…
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…
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