The Curious Engineer

All blogs / Python

alexwlchan

alexwlchan.net

Alex Chan’s personal website

PythonDev tools / editors / IDEsTesting / QARustEssays / personal

Posts
727 from 10 Jan 2013 to 26 Oct 2026
Posting
weekly (101 in the last year)
Kind
personal blog
Feed
https://alexwlchan.net/atom.xml

2026 86

  1. Positive alerts in Prometheus
  2. Organising my cross-stitch patterns
  3. Parametrised tests in Rust with named parameters
  4. Abusing ID3 chapters to turn videos into glanceable podcasts
  5. What is WS11 1DB?
  6. Blocking referrers with Caddy
  7. How to type a Spanish question mark (¿) on a Mac
  8. Non-overlapping type comparisons and Python type checkers
  9. Why does t.Setenv panic after t.Parallel?
  10. Why can’t you combine <code>.tar.gz</code> files with <code>cat</code>?
  11. Use Path.glob() and Path.rglob() for typed versions of glob.glob()
  12. How Tailscale tracked down a 16-year-old SQLite bug &rarr;
  13. How Tailscale tracked down a 16-year-old SQLite bugWe had repeated database corruption until we fixed a rare SQLite corruption bug. What does it take to find such an error?SQLite · 13 Aug 2026
  14. Curious clocks and colourful eyes
  15. Track which templates are used by Jinja2
  16. Archeologists distinguish between “sherds” and “shards”
  17. A single command to test all my changed Go packages
  18. Preventing line breaks in <code>&lt;code&gt;</code> elements
  19. Fixing a bug with byte order marks
  20. Disable the new message animations in WhatsApp
  21. A Git hook to prevent committing directly to main
  22. Finding high-churn folders that bother Backblaze
  23. Describing all my photos
  24. Always-on SSH agent forwarding with my Git pushes
  25. I don’t want to repeat repeat myself
  26. Rebuilding the computer room
  27. Managing the caption of a photo with AppleScript (but not PhotoKit)
  28. What can wonky APIs tell us about the web?
  29. Goodhart’s and Campbell’s Law are different
  30. Using the Screen Capture API to record a browser window
  31. Notes from The Cornishman No. 176 (Spring 2026)
  32. Using Pytester to test my Playwright fixtures
  33. Rendering a chat thread in CSS and JavaScript
  34. GitUp can’t diff text files larger than 8MB
  35. Format “human-readable” JSON with FracturedJSON
  36. Waiting for website changes in the browser
  37. Watching for file changes on macOS
  38. Testing the width of a page on a mobile device using Playwright
  39. Using Playwright to test my static sites
  40. Disable AirPods charging notifications
  41. Start a Caddy server in a subprocess during a Python session
  42. Building a basic cache with SQLite
  43. Filter a list of JSON object based on a list of tags
  44. HTTP GET requests with the Python standard library
  45. Auditing my local Python packages
  46. Quietly quantum-resistant blogging
  47. The FileExistsError exception exposes a filename attribute
  48. HOME_GET_ME_HOME is a Citymapper Shortcuts action
  49. Why can’t Python connect to example.com?
  50. The red-lined bubble snail
  51. Useful type hints for Python
  52. Creating a personalised bin calendar
  53. Monki Gras 2026 “Prepping Craft”
  54. How to truncate the middle of long command output
  55. The selfish case for public libraries
  56. What’s the main prefix in SQLite queries?
  57. AirPlay Receiver can interfere with Flask apps
  58. My randline project is tested by Crater
  59. The file(1) command can read SQLite databases
  60. Dreaming of a ten-year computerModern computers are more powerful than I know what to do with, but the norm is still to upgrade every few years. I want my current computer to last a decade, and I don’t think that’s unreasonable.Computers and code · 12 Mar 2026
  61. Drawing an image with Liquid Glass using SwiftUI Previews
  62. Gumdrop, a silly app for messing with my webcam
  63. Road signs in the Soviet union don’t have circular heads
  64. Get a map of IP addresses for devices in my tailnet
  65. Create a file atomically in Go
  66. Setting up golink in my personal tailnet
  67. Testing date formatting with date-fns-tz and different timezones
  68. Use SQL triggers to prevent overwriting a value
  69. The SQLite command line shell will count your unclosed parentheses
  70. The bare minimum for syncing Git repos
  71. Place with the same name, but different etymology
  72. The “strangler” pattern is named after a tree, not an act of violence
  73. Creating Caddyfiles with Cog
  74. Use systemctl is-active to determine if a service is running
  75. Removing a self-hosted runner from GitHub Actions
  76. My Git config
  77. Introducing Mosaic, my Python static site generator
  78. Python f-strings cheat sheet
  79. Who is Bufo the frog?
  80. Parody posters for made-up movies
  81. The Good, the Bad, and the GuttersA step-by-step guide to a movie poster grid that uses CSS Grid, text-wrap balanced titles, and dynamic hover states.CSS · 14 Jan 2026
  82. Using perceptual distance to create better headers
  83. The caret anchor (^) matches differently in Python and Ruby
  84. Where I store my multi-factor recovery codes
  85. The passwords I actually memorise
  86. Quick-and-dirty print debugging in Go

2025 136

  1. My favourite books from 2025Sapphics and spies, butlers and basketball, prophecy and purpose – what I enjoyed reading this year.Books I’ve read · 31 Dec 2025
  2. Ignore AI upscaled YouTube videos with yt-dlp
  3. Drawing Truchet tiles in SVG
  4. Adding a README to S3 buckets with Terraform
  5. The palm tree that led to Palmyra
  6. Meeting my younger selfI reviewed 150,000 fragments of my online life, and I was reminded of the friends I found, the mistakes I made, and the growth I gained.Preserving social media · 12 Dec 2025
  7. Hard problems in social media archivingPreserving social media is easier said than done. What makes it so difficult for institutions to back up the Internet?Preserving social media · 10 Dec 2025
  8. The Internet forgets, but I don’t want toI don’t trust platforms to preserve my memories, so I built my own scrapbook of social media.Preserving social media · 8 Dec 2025
  9. When square pixels aren’t square
  10. Collapsing whitespace in a Liquid template
  11. The LastModified date of an S3 multipart upload is when the upload started, not when it finished
  12. Can you take an ox to Oxford?
  13. Cleaning up messy dates in JSON
  14. Detecting AV1-encoded videos with Python
  15. Python has a builtin tool to do rot13 (among other things)
  16. Why can’t my iPhone play that video?
  17. Doing my own syntax highlighting (finally)
  18. How to find the Pygments token type for a short name
  19. Remove the microsecond precision from a datetime in Python
  20. Creating a personal wrapper around yt-dlp
  21. Don’t show Dock icons from apps on another device
  22. Look at the __annotations__ to learn about the definition of a TypedDict
  23. Get the avatar URL for an Instagram page
  24. Using the Tumblr v1 API doesn’t require auth
  25. How to expire a Tailscale node key faster than the min expiry
  26. The “MCP” in Archivematica stands for “Master Control Program”
  27. Use the -v flag to get verbose output from Go tests
  28. Opening all the files that have been modified in a Git branch
  29. Seeing the public node key of a Tailscale node
  30. Get a list of values in a JSON object with jq
  31. Linking to text fragments with a bookmarklet
  32. Don’t nest a <Script> in a <Head> with Next.js
  33. Using Go to write to a file, but only if it doesn’t exist yet
  34. Resizing images in Rust, now with EXIF orientation support
  35. The person who runs the Cambridge NTP servers has an excellent email address
  36. Using vcrpy to test HTTP interactions in PythonHow I record HTTP requests to get fast, reliable, and consistent tests, and the patterns I use in a production codebase.Python · 28 Aug 2025
  37. Discard a variable in a JavaScript object spread by assigning it to _
  38. The @ symbol was added to Morse code in 2004
  39. How to play with SQLite functions without real data
  40. Repeatedly run flaky Go tests with stress
  41. How to list the tests that will be run by a go test command
  42. My preferred options for SmartyPants in Python
  43. Get the avatar URL for a Bluesky user
  44. Looking up posts in the Bluesky API
  45. Create space-saving clones on macOS with Python
  46. Slipstitch, Queer Craft, and community spaces
  47. Today was my last day at the Flickr Foundation
  48. Minifying HTML on my Jekyll website
  49. Using zipstream to stream new zip files to object storage with boto3
  50. Using Linode object storage and boto3
  51. Adding a string to a tarfile in Python
  52. Disable HTTP Basic Auth for certain pages in Caddy
  53. Collecting pytest markers from failing tests
  54. Moving my Glitch apps to my own web server
  55. You can change the size of tabs on web pages with the tab-size property
  56. Combine arrows in Mermaid by using an invisible node
  57. Show a list of checkboxes in a WTForms form
  58. Group nodes in a Mermaid flowchart by putting them in a subgraph
  59. Redacting sensitive information from gunicorn access logs
  60. Some countries have “poison centers” as part of their healthcare service
  61. Listen for the popstate event to see when the user clicks the “back” button
  62. Python’s f-strings support = for self-documenting expressions
  63. Comparing two files in a bash script
  64. Use typing.getargs() to get a list of typing.Literal[…] values
  65. Ruby’s range can iterate over more than just numbers
  66. The error “No aggregated item, sequence was empty” comes from Jinja2
  67. Get a string representation of a Python traceback with traceback.format_exc()
  68. How to run a task on a schedule on macOS
  69. Go’s compiler is smart enough to spot division by zero errors
  70. Recreating the bird animation from Swift.orgThe Swift.org website got a redesign, including an animation of a swooping bird, and I read the source code to understand how it works.Drawing things · 11 Jun 2025
  71. Parsing JSON in Go with a required field
  72. Print a comma-separated number in Python with {num:,}
  73. Get the duration of a video file with mediainfo
  74. Use text=true with subprocess functions to get stdout/stderr as str, not bytes
  75. What I learnt about making websites by reading two thousand web pagesHow to write thoughtful HTML, new-to-me features of CSS, and some quirks and relics I found while building my personal web archive.Web archiving · 26 May 2025
  76. Building a personal archive of the web, the slow wayHow I built a web archive by hand, the tradeoffs between manual and automated archiving, and what I learnt about preserving the web.Web archiving · 19 May 2025
  77. The Panamanian Golden Frog communicates by semaphore
  78. Web archiving
  79. What does the word “gubernatorial” mean?
  80. How to stream lines from stdout with subprocess
  81. You can set/update the total of a progress bar tqdm after it starts
  82. Creating a static website for all my bookmarksTo help me keep track of interesting links, I created a static website that keeps all my data locally. Why do I care about bookmarks, and how does my new site work?Web archiving · 12 May 2025
  83. How do I find photos of a person on Flickr?
  84. Handling JSON objects with duplicate names in Python
  85. How does Flickr’s getLicenseHistory handle photos with no license changes?
  86. Where does AirDrop save files on macOS?
  87. Python 3.14 includes a pi-related Easter egg
  88. A faster way to copy SQLite databases between computers
  89. How to check if Tailscale is running
  90. To filter the results of a SQLite tally for values with a certain frequency, use a HAVING instead of a WHERE clause
  91. When fixing mojibake, use ftfy.fix_and_explain() to understand how it’s fixing a piece of text
  92. Why isn’t delete_where() deleting rows in sqlite-utils?
  93. A flash of light in the darkness
  94. console.log() holds a reference to an object, not a copy of it
  95. Beyond None: actionable error messages for keyring.get_password()
  96. Localising the <time> with JavaScript
  97. My favourite websites from my bookmark collection
  98. Always runningWhen the fire starts, I am already running for the exit. I run because somebody must show the way.Art and creativity · 6 Apr 2025
  99. You can set an output mode for SQLite
  100. How do you write ratios in the aspect-ratio property?
  101. Python 3.13 throws a ResourceWarning for SQLite databases that aren’t explicitly closed
  102. How to install exiftool in GitHub Actions
  103. What I’ve Learned by Building to LastThere are patterns in what lasts; people skills matter more than technical skills; long-lasting systems cannot grow forever.Digital preservation · 28 Mar 2025
  104. Whose code am I running in GitHub Actions?
  105. Fast and random sampling in SQLite
  106. We all lose when art is anonymised
  107. An unexpected lesson in CSS stacking contexts
  108. Creating static map images with OpenStreetMap, Web Mercator, and Pillow
  109. How to get a macOS file/folder icon in Swift
  110. It’s cool to careCaring deeply – about art, my friends, and shared experiences – has brought me so much joy.Personal thoughts · 21 Feb 2025
  111. The British Forces Post Office (BFPO) and the Loamshire Regiment
  112. Cosmetic updates to this site
  113. How I create static websites for tiny archivesStart with a simple HTML file, then add features like templates, sorting, and filtering as they become useful.Tiny archives · 19 Feb 2025
  114. Create an animated placeholder box with CSS
  115. Disable JavaScript in an <iframe> by setting the sandbox attribute
  116. Get video dimensions on macOS with built-in tools
  117. Get the dimensions of a video file with mediainfo
  118. Downloading avatars from Tumblr
  119. Good embedded toots
  120. Unexpected errors in the BagIt areaExploring the many ways a BagIt bag can be invalid, and how you might want to build a process for validating bags to ensure the quality of your digital archive.Digital preservation · 8 Feb 2025
  121. What does Flickr return in the flickr.photo.getSizes API for videos?
  122. A basic socket server in Ruby
  123. Bitly will delete your account if you don’t use it for three years
  124. Making inventory spreadsheets for my LEGO sets
  125. Adding auto-generated cover images to EPUBs downloaded from AO3
  126. How to find the content.opf in an EPUB file
  127. Getting and setting the cover image from an EPUB file
  128. Google will delete your account if you don’t use it for two years
  129. How to find platform-specific directories
  130. Looking at images in a spreadsheet
  131. Restricting routes to pre-approved IP addresses in Caddy
  132. randline: get a random selection of lines in a file using reservoir sampling
  133. Use shutil.copyfileobj and xb to avoid overwriting files when copying in Python
  134. How to tally the attributes of the top N rows in a SQLite table
  135. How I test Rust command-line apps with assert_cmd
  136. How I use the notes field in my password manager

2024 136

  1. My favourite books from 2024Fires and fighters, heroes and horrors, suicide and surgery – what I enjoyed reading this year.Books I’ve read · 31 Dec 2024
  2. I deleted all my tweets
  3. Not all blog posts are created equalSplitting my site into “articles” and “today I learned” helped me overcome writer’s block and improve my writing.Blogging about blogging · 26 Dec 2024
  4. How to get the IP address of a device in my Tailnet
  5. There are limits on the styles you can apply with :visited
  6. How to find the biggest files backed up by Backblaze
  7. Editing a filename in Finder will convert it to NFD
  8. How can I work out what program is keeping a disk open?
  9. You can reset the start of a regex in Ruby
  10. How to get the expiry date of an HTTPS certificate with Ruby
  11. How to install an asset from a GitHub release
  12. Debugging some confusing behaviour with find and xargs
  13. Creating a reverse proxy to a multi-site server with Caddy
  14. How to find all of Apple’s system icons
  15. How I name my computersWhat do flaming birds, man-eating giants, and obscure deities have in common?Computers and code · 14 Nov 2024
  16. The Apollo 11 astronauts were promised another attempt if they had to abort
  17. Drawing a diagonal banner over the corner of an image
  18. Find the shortest prefix to identify a string in Ruby
  19. How to get the target of an HTTP redirect with curl
  20. What is the author_name in the list of tags on a Flickr photo?
  21. How to get a user’s email address with the Flickr API
  22. Making an “under construction” element in pure CSS
  23. HTML strings may not be equivalent if you minify them
  24. What errors can you get from hyperlink.parse?
  25. Using static websites for tiny archivesI’ve been creating small, hand-written websites to organise my files. It’s a lightweight, flexible approach that I hope will last a long time.Tiny archives · 16 Oct 2024
  26. Making alt text more visible
  27. How do the ispublic, isfriend and isfamily flags work in the Flickr API?
  28. Moving my website from Netlify to Caddy
  29. A script to verify my Netlify redirects
  30. Two examples of hover styles on images
  31. Use std::io::IsTerminal to detect if you’re running in the terminal in Rust
  32. Drawing a better bandwidth graph for Netlify
  33. Going between Finder and the Terminal
  34. Digital declutteringI’m resisting my temptation towards digital hoarding and “save everything”, and trying to be more selective about the data I’m keeping.Personal thoughts · 13 Sep 2024
  35. Use $_ to get the last argument to the previous bash command
  36. With Flask-Login, you want current_user == None, not current_user is None
  37. How to get the expiry date of an HTTPS certificate with Python
  38. Mountaintop momentsThe amazing events that let us temporarily escape from our lives, and how to deal with the emotional crash that comes afterward.Personal thoughts · 26 Aug 2024
  39. create_thumbnail: create smaller versions of imagesI’ve made a new tool that allows me to reuse my thumbnailing code across all my projects.Images and videos · 20 Aug 2024
  40. Plates and statesOn a recent trip to the US, I learnt a surprising amount from watching the license plates on passing cars.The world around us · 14 Aug 2024
  41. Write to the middle of a file with Python
  42. Get an image from a video with ffmpeg
  43. Create solid-colour image placeholders to show before an image loads
  44. Get the embedded artwork from an MP3 file
  45. Convert an animated GIF to an MP4 with ffmpeg
  46. Build a URL with query string parameters with curl
  47. Using the Wikimedia Commons API to tell if a file has been deleted
  48. Doodling with the Mac’s command iconRotating a looped hook around a central point creates a surprising variety of shapes.Generative art · 4 Jul 2024
  49. How to get the selected item in Finder using AppleScript
  50. emptydir: look for (nearly) empty directories and delete themI’ve been writing tools in Rust again.Computers and code · 24 Jun 2024
  51. Google is showing outdated results from the UK’s election
  52. Removing letterboxing from a video screenshot with ImageMagick
  53. How to check the quality of a network connection
  54. The surprising utility of a Flickr URL parser
  55. How to highlight Python console sessions in Jekyll
  56. Drawing repetitive radial artworksUsing polar coordinates to draw leaf- and flower-like patterns that repeat in circles around a point.Generative art · 1 Jun 2024
  57. Not all coal is the same
  58. Documenting my DNS records
  59. Preserving pixels in Paris
  60. How to simulate an [Errno 54] Connection reset by peer when using pytest
  61. What does “insomnolent” mean?
  62. Open a Safari webarchive from Twitter/X without being redirected
  63. Creating a Safari webarchive from the command line
  64. Writing a file in Swift, but only if it doesn’t already exist
  65. What comes after AWS?
  66. What’s inside a Safari webarchive?
  67. Get a Palette colour as a command-line argument with Clap
  68. There’s a musical that tells you the number of minutes in a year
  69. How to get the filename/size without downloading a file in curl
  70. How to get a tally of tally counts in SQLite
  71. What is psephology?
  72. Taking regular screenshots of my website
  73. How to get a list of captures from the Wayback Machine
  74. Getting faster Jekyll builds with caching in plugins
  75. How to take a screenshot of a page in the Wayback Machine
  76. The Star-Spangled Ballad
  77. How to count how many Discord messages were sent on a given day
  78. How to see the HTTP requests being made by pywikibot
  79. flapi.sh: a tiny command-line tool for exploring the Flickr API
  80. How to embed an inline SVG in a CSS rule
  81. How to shuffle an array in a Jekyll template
  82. The new Flickr Commons Explorer
  83. Checking if a URL has changed when you fetch it over HTTP
  84. The open command can ask questions
  85. Why is Pillow rotating my image when I save it?
  86. Getting a boto3 Session for an IAM role using Python
  87. How to change the name of an internal link in an Obsidian table
  88. My config for running youtube-dl
  89. How much will Mastodon instances try to re-send messages?
  90. Step… Step… Step…
  91. Get my Netlify bandwidth usage from the API
  92. How to restrict a page to specific IP addresses
  93. Use the -n/-i flags to avoid overwriting files with cp and mv
  94. How to parse URLs in JXA
  95. How to check when an HTTPS certificate expires
  96. Using errexit and arithmetic expressions in bash
  97. What happens when you replace a photo on Flickr?
  98. The COUNT(X) function only counts non-null values
  99. Moving my YouTube Likes from one account to another
  100. Exclude files from Time Machine with tmutil addexclusion
  101. You need to call resp.close() to close the file opened by send_file()
  102. Making a PDF that’s larger than GermanyWe’re gonna need a bigger printer.Code crimes · 31 Jan 2024
  103. Add the -v flag to see what rm is deleting
  104. Custom error pages in Flask
  105. What characters are allowed in titles on Wikimedia Commons?
  106. Use the {% raw %} tag to describe Liquid in Liquid
  107. Beware of using test -n with command expansion
  108. Run a script on macOS on a schedule using a LaunchAgent
  109. The Collected Works of Ian Flemingo
  110. How to do offline geo-lookups of IP addresses
  111. How to move files when you need sudo on the remote server
  112. How to tally combinations of values across multiple columns
  113. How to gracefully restart a gunicorn app
  114. How to find the longest common suffix in a list of strings in Python
  115. How to simulate shell pipes with the subprocess module
  116. Manage MP3 metadata from iTunes with eyed3
  117. WordPress URLs that get hammered by spammers
  118. How to create a footer that’s always at the bottom of the page
  119. Use the {% capture %} tag to assign complex strings to variables
  120. Sort a list of DOM elements by sorting and calling appendChild()
  121. How to create flag emojis for countries in Python
  122. Use the IMAGE function to insert an image into a spreadsheet
  123. Create a directory before you cp or mv a file to it
  124. Pausing the animation of <svg> elements can affect child <svg> elements differently in different browsers
  125. How to style a <details> element differently depending on whether it’s open or closed
  126. Running the Netlify CLI in GitHub Actions
  127. Use shlex.split() to parse log files quickly
  128. Animate an attribute of an element with <animate>
  129. SVGs are only rendered on GitHub if you use an <img> that points to another file
  130. Get and manipulate the contents of a page in Safari with "do JavaScript"
  131. Use the regex library to get Unicode property escapes in Python
  132. Getting a tally of SQLite values as a CSV
  133. Run a randomly selected subset of tests with pytest
  134. What mammal is that?
  135. Python’s sqlite3 context manager doesn’t close connections
  136. Using sqlite-utils to convert a CSV into a SQLite database

2023 55

  1. My favourite books from 2023Storms and schools, escapes and exorcisms, murders and magic – what I enjoyed reading this year.Books I’ve read · 30 Dec 2023
  2. Getting the path to the note I have open in Obsidian
  3. How to delete albums
  4. Live Text is aware of how hyphenation works (kinda)
  5. Setting up Fish to make virtualenv easier
  6. Go between M-IDs and filenames on Wikimedia Commons
  7. Telling mechanize how to find local issuer certificates
  8. Why I prefer XML to JSON in the Wikimedia Commons APIs
  9. Making the fish shell more forgetful
  10. Hyperfocus and hobbiesThe power of ADHD-associated hyperfocus and how I’ve become wary of feeding it too often.Personal thoughts · 20 Oct 2023
  11. How to do resumable downloads with curl
  12. Find files that use a particular SDC field
  13. Finding the biggest items in my Photos Library
  14. Why the term “snak” keeps appearing in the Wikidata API
  15. The acronym “woe” in the Flickr API stands for “Where On Earth”
  16. Spotting spam in our CloudFront logs
  17. Adding locations to my photos from my Apple Watch workouts
  18. Fare-Wellcome Collection
  19. How I set up my Obsidian vaults
  20. Use Unicode property escapes to detect emoji in JavaScript
  21. Finding the original page for a post on Mastodon
  22. Finding a mystery IAM access key
  23. A blue plaque for HesterMaking a small memorial to a forgotten secretary who helped pull off one of the most daring deceptions of WWII.Entertainment · 19 Aug 2023
  24. Tag your infrastructure-as-code resources with a link to their definitions
  25. Parsing CloudFront logs with Python
  26. My Python snippet for walking a file tree
  27. My custom <picture> plugin for Jekyll
  28. Spy for Spy
  29. Turning off ECS tasks overnight using an EventBridge Schedule
  30. Preserving Dates during JSON serialisation with vanilla JS
  31. Writing a Mac app to review my photosDipping my toes into SwiftUI to make a tool for reviewing my photos with just the keyboard.Photo management · 18 Jun 2023
  32. Have a single definition of “now”
  33. Use concurrency gates to prevent concurrent deployments
  34. Starting Docker just before I need it
  35. Snippets to manage albums in Photos.app
  36. How to profile Swift code
  37. s3tree: viewing a tree of objects in S3 in my terminal
  38. Redecorating my bedroom
  39. Getting alerts about flaky ECS tasks in Slack
  40. Using templates in Terraform to document a deployment
  41. Publishing lots and lots of messages to SNS
  42. Filtering AWS CLI output by tags using jq
  43. Cats, cross-stitch, and copyrightIn celebration of my favourite apex predator.Art and creativity · 13 Mar 2023
  44. Filtering out bogus requests from Netlify Analytics
  45. Splitting a class into balanced groups
  46. Creating a Python dictionary with multiple, equivalent keys
  47. Testing JavaScript without a (third-party) framework
  48. CSS formatting in the console
  49. Going through my old school papers
  50. A Python function to iterate through an S3 Bucket Inventory
  51. How moving to the cloud took Wellcome’s digital collections to new heights
  52. A bookmarklet to show which responsive image was chosen
  53. Changing the bulb in a Meridian Lighting CIR100B ceiling light
  54. Beware of transparent backgrounds when using AVIF with ImageMagick 6
  55. Upward assignment in RubyA deep dive into the internals of Ruby and metaprogramming techniques, in a quest for a cursed operator.Code crimes · 13 Jan 2023

2022 46

  1. A script to get Live Text from images
  2. My favourite books from 2022Romance and rust, gender and grief, horror and the House – what I enjoyed reading this year.Books I’ve read · 31 Dec 2022
  3. Getting an Important Internet Checkmark to follow your cursor
  4. How we do bulk analysis of our Prismic content
  5. Launching a rocket in the worst possible wayTaking the humble <marquee> tag where no HTML tag has gone before.Code crimes · 16 Dec 2022
  6. Getting the base directory of an sbt project
  7. Redacting sensitive query parameters with koa and koa-logger
  8. A day out at the Bure Valley RailwayMy photos from a delightful day on a steam railway with smol trains.The world around us · 28 Nov 2022
  9. Changing the macOS accent colour without System Preferences
  10. Ten years of blogging
  11. Saving your alt text from Twitter
  12. A simple gallery plugin for Obsidian
  13. Agile and iterative project management
  14. Finding books in nearby library branches
  15. Some experiments with circle-based art
  16. Finding a tricky bug in Elasticsearch 8.4.2Gradually deleting more and more data helped me get a reliable repro for an elusive bug.Computers and code · 5 Oct 2022
  17. My (tiny) contribution to Rust 1.64
  18. “Fixing” the rules of divisionIf we want to redefine how division works, Ruby is happy to oblige.Code crimes · 23 Sep 2022
  19. The maths cross-stitch that hangs behind meWhen I’m on video calls, my backdrop has some maths-related art that I helped to make.Art and creativity · 22 Sep 2022
  20. Generating art from lattice graphsRandomly selecting a subset of edge from a graph can make pretty pictures.Generative art · 13 Sep 2022
  21. Finding redundant data in our Next.js props
  22. How to use hex colours with the palette crate
  23. I always want StrictUndefined in Jinja
  24. An Egyptian ‘mixtape’ of embroidered material
  25. Drawing a circular arc in an SVG
  26. How to customise the title of Buildkite builds triggered from GitHub deployments
  27. Our Place in Space
  28. Cut the cutesy errors
  29. A surprise smattering of stardom
  30. You should take more screenshotsIf you do any sort of creative digital work, screenshots and screen recordings are a great way to remember it.Personal thoughts · 23 Jul 2022
  31. A Martian plaque for a made-up plot
  32. One small stitch for yarn, one giant leap for yarn-kind
  33. Creating an Alfred Workflow to open GitHub repos
  34. Fictional phone numbers in For All Mankind
  35. Experimenting with jq as a tool for filtering JSON
  36. Find the dominant colours in an image in your web browser
  37. Running a Rust binary in Glitch
  38. Illustrating the cipher wheels of a Lorenz machine
  39. Creating a “simple” three-up image layout in CSS
  40. Checking lots of URLs with curl
  41. Beware delays in SQS metric delivery
  42. A tale of two Twitter cards
  43. Closing lots of Safari tabs with JXA
  44. Why is Amazon Route 53 named that way?
  45. Creating animated GIFs from fruit and veg
  46. Creating coloured bookshelf graphics in Rust

2021 37

  1. My favourite books from 2021Aliens and aces, railroads and racism, dating and disasters – what I enjoyed reading this year.Books I’ve read · 31 Dec 2021
  2. Calley-ope (calliope) Syndrome is pronouncing a word wrong because you’ve only ever read it on the page
  3. Why is os.sep insufficient for path operations?
  4. The ever-improving error messages of Rust
  5. A tale of two path separators
  6. dominant_colours, a CLI tool for finding dominant colours in an imageA new tool for playing with images.Colours · 30 Nov 2021
  7. Programatically finding the original filename of a photo in the macOS Photos Library
  8. The Oboe of Optozorax, and Other Objects
  9. Prevent accidentally copying the prompt character in console code snippets
  10. READMEs for Open Science
  11. Beware of incomplete PDF redactions
  12. How do you work with non-engineers?
  13. SeptembRSE: Missing narratives in discussions around diversity and inclusion
  14. An editing toolbar for alexwlchan.net
  15. Getting a monthly cloud costs report in Slack
  16. Operations on strings don’t always commute
  17. When is my EventBridge cron expression going to run next?
  18. Picking perfect planks with Python
  19. How to ignore lots of folders in Spotlight
  20. Markdown’s gentle encouragement towards accessible images
  21. Finding misconfigured or dangling CloudWatch Alarms
  22. A few useful GitHub searches
  23. Listing deleted secrets in AWS Secrets Manager with boto3 and the AWS CLI
  24. Visualising how often I write in my journal
  25. Downloading objects from/uploading files to S3 with progress bars in Python
  26. Drawing coloured squares/text in my terminal with Python
  27. Using AppleScript to detect if a Safari window uses Private Browsing
  28. Finding the app/process that’s using Secure Input
  29. Getting a list of URLs from my Safari history
  30. Building Wellcome Collection’s new unified catalogue search
  31. Drawing inner/outer strokes in SVG (clips and masks)Showing how clips and masks work, and how they can be used to draw inner and outer strokes of a shape.Drawing things · 12 Mar 2021
  32. Creating pairs of interlocking rainbow hearts 🌈
  33. An AppleScript to toggle Voice Control
  34. Screaming in the Cloud: Using the Cloud to Preserve the Future
  35. How KempisBot worksHow I reincarnated a fifteenth-century monk and taught him to use Twitter.Fun stuff · 31 Jan 2021
  36. Wellcome Collection’s approach to digital verificationHow we ensure the safety, security and integrity of the files in Wellcome’s digital collections.Digital preservation · 18 Jan 2021
  37. What year is it? (A tale of ISO week dates)

2020 60

  1. Creating short-lived, temporary roles for experimenting with AWS IAM policy documents
  2. “Non-technical users”
  3. A script to copy images from Docker Hub to Amazon ECR
  4. How I read non-fiction books
  5. Maths is about facing ambiguity, not avoiding it
  6. Remembering if a <details> element was opened
  7. A Python function to ignore a path with .git/info/exclude
  8. Use git check-ignore to debug your .gitignore
  9. How do I use my iPhone cameras?
  10. A new README for docstore, my tool for organising scanned paperwork
  11. The danger of bad error messages
  12. Replicating Wellcome Collection’s digital archive to Azure Blob Storage
  13. Using qlmanage to create thumbnails on macOS
  14. Two Python functions for getting CloudTrail events
  15. S3 prefixes are not directories
  16. S3 keys are not file paths
  17. Using fuzzy string matching to find duplicate tags
  18. Getting a Markdown link to a window in Safari
  19. Why do programming languages have a main() function?
  20. Running concurrent Try functions in Scala
  21. What does \d match in a regex?
  22. How to do parallel downloads with youtube-dl
  23. Changing the accent colour of ICNS icons
  24. Using AppleScript to open a URL in Private Browsing in Safari
  25. Fat shaming in The Good Place
  26. Large things living in cold placesUsing cold storage tiers to reduce the cost of storing Wellcome’s digital collections in the cloud.Digital preservation · 17 Jun 2020
  27. Archive monocultures considered harmful
  28. Finding the months between two dates in Python
  29. Getting every item from a DynamoDB table with Python
  30. Showing human-friendly dates in JavaScript
  31. The Sachsenhausen concentration camp
  32. Moving messages between SQS queues
  33. Downloading the AO3 fics that I’ve saved in Pinboard
  34. Social media as a growth culture for opinions
  35. Taking tuple unpacking to terrible places
  36. Make it safe to admit mistakes
  37. Give your audience time to react
  38. How long is my data? 💾A fun app to calculate the size of your data in terms of floppy disks.Fun stuff · 2 May 2020
  39. Illustrating lifecycle transitions in Amazon S3A picture speaks a thousand words, which is why I always have pen and paper to hand.AWS · 1 May 2020
  40. Using DynamoDB as a calculatorTaking advantage of Amazon’s least-loved compute platform.Code crimes · 27 Apr 2020
  41. Exploring an unknown SQL server
  42. Thinking about your gender
  43. Comparing JSON strings when testing in Scala
  44. Complex systems have complex failures
  45. Adventures in euphoria and embodiment
  46. Access to information is a privilege
  47. A snippet for downloading files with Python
  48. Storing language vocabulary as a graph
  49. Inclusion can’t be an afterthought
  50. Rich enough to make bad choices
  51. Sick leave while working from home
  52. Creating striped flag wallpapers with Pillow
  53. A Jekyll filter for adding non-breaking spaces
  54. Adjusting the dominant colour of an image
  55. Storing multiple, human-readable versions of BagIt bags
  56. Building Wellcome Collection’s new archival storage serviceHow we built the new cloud-based storage system for holding an ever-growing digital archive.Wellcome Collection · 5 Feb 2020
  57. An interactive map of British railway stations 🚂
  58. A deletion canary: testing your S3 bucket permissions
  59. Generating pride-themed Norse valknuts with Python 🌈
  60. Finding the CPU and memory bottlenecks in an ECS cluster

2019 49

  1. This YAML file will self-destruct in five seconds!
  2. Some useful spreadsheet functions: FORMULATEXT, MATCH, CONCATENATE and INDIRECT
  3. November 2019 scripts: downloading podcasts, retrying flaky errors, Azure and AWS
  4. How I scan and organise my paperwork
  5. Saving a copy of a tweet by typing ;twurl
  6. An AWS costs graph that works for me
  7. Digital preservation at Wellcome Collection
  8. Adventures in Python with concurrent.futuresHow I use concurrent.futures to speed up my batch scripting in Python.Python · 16 Oct 2019
  9. Sans I/O programming: what, why and how (PyCon UK talk)
  10. Adding religious holidays to my calendar
  11. The rough edges of filecmp
  12. Experiment: GitHub code search with de-duplication
  13. Drawing with triangular coordinates in SVG
  14. Streaming large objects from S3 with ranged GET requests
  15. Iterating over the entries of a compressed archive (tar.gz) in Scala
  16. Installing mimetype on Alpine Linux
  17. Getting a tint colour from an image with Python and k‑meansI want to display text next to an image that looks visually similar to the image, so I need to extract a tint colour.Colours · 19 Aug 2019
  18. Finding divisors of a number with Python
  19. Creating preview thumbnails of PDF documents
  20. Listing even more keys in an S3 bucket with Python
  21. Ten braille facts / ⠼⠁⠚⠀⠃⠗⠁⠊⠇⠇⠑⠀⠋⠁⠉⠞⠎
  22. Deleting nested JSON fields with Circe optics
  23. An inescapable conclusion
  24. A Jekyll filter for obfuscating email addresses
  25. Reading a Chinese dictionary / 读一本中文字典
  26. Converting Acorn images on the command-line
  27. A script for getting cover images from mobi ebooks
  28. Hey Apple, cycle tracking isn’t just for women
  29. Questions to ask when writing a trans inclusion policy
  30. Falsehoods programmers believe about Unix time
  31. Creating a locking service in a Scala type class
  32. How do Dreamwidth posts IDs work?
  33. Add a Git co-author credit with “Co-authored-by” in your commit message
  34. Conditional updates on nested fields in DynamoDB
  35. How to iterate over the lines of an InputStream in Scala
  36. Using TransportAPI and geopy to get the distance between stations
  37. Reversing a t.co URL to the original tweet
  38. Some tips for conferences
  39. Manipulating URL query parameters in JavaScript
  40. The Content-Disposition header can be used to name a downloaded file
  41. Getting a transcript of a talk from YouTube
  42. Creating a GitHub Action to auto-merge pull requests
  43. A day out to the Forth BridgePhotographs from a trip to North Queensferry to see the Forth Bridge, the Forth Road Bridge, and an unexpected light tower.The world around us · 15 Mar 2019
  44. Finding the latest screenshot in macOS Mojave
  45. Atomic, cross-filesystem moves in Python
  46. Checking Jekyll sites with HTMLProofer
  47. Working with really large objects in S3
  48. The Curb Cut Effect
  49. Notes from You Got This 2019

2018 39

  1. Notes on reading a UTF‑8 encoded CSV in Python
  2. Iterating in fixed-size chunks in Python
  3. Getting credentials for an assumed IAM Role
  4. A script for backing up Tumblr posts and likes
  5. How to suppress installing rdoc/ri docs when running gem install
  6. Keeping track of my book recommendations
  7. Replace black/white parts of an image with ImageMagick
  8. My visit to the Aberdulais Falls
  9. Finding SNS topics without any subscriptions
  10. Peering through MRI scans of fruit and veg
  11. How to set the clock on a Horstmann Electronic 7 water heater
  12. Create compact JSON with Python
  13. How to use xargs for parallel processing
  14. Pushing with --force-with-lease is safer than with --force
  15. Custom 404 responses in Finatra
  16. Assume worst intent (designing against the abusive ex)
  17. Building trust in an age of suspicious minds
  18. Signs of the time
  19. List all Git object IDs and their type
  20. A basic error logger for Python Lambdas
  21. Making the venue maps for PyCon UK
  22. Implementing parallel scan in DynamoDB with Scanamo
  23. Avoiding the automatic redirect on Tumblr posts
  24. Ideas for inclusive conferences and events
  25. A robot leaked my SSH keys
  26. My favourite iMac accessory
  27. Drawing ASCII bar charts
  28. Creating a data store from S3 and DynamoDB
  29. Beware of logged errors from subprocess
  30. Two shortcuts for using S3 in the shell
  31. (Anti) Social Media
  32. Python throws a TypeError if you return a non-string from a custom __repr__ or __str__ method
  33. Notes on A Plumber’s Guide to Git
  34. The Hypothesis continuous release process
  35. Keep an overnight bag in the office
  36. Getting helpful CloudWatch alarms in Slack
  37. Getting every message in an SQS queue
  38. Listing keys in an S3 bucket with Python, redux
  39. IP and DNS addresses for documentation

2017 22

  1. Your repo should be easy to build, and how
  2. Using Loris for IIIF at Wellcome
  3. Pruning old Git branches
  4. Downloading logs from Amazon CloudWatch
  5. Don’t tap the mic, and other tips for speakers
  6. My favourite WITCH story
  7. A plumber’s guide to Git
  8. Using privilege to improve inclusion
  9. Lightning talks
  10. Using hooks for custom behaviour in requests
  11. Four ways to underline text inLaTeK LaTeXI’m very picky about the way underlines look, and have spent a lot of time trying to get the perfect underline inLaTeK LaTeX.LaTeKLaTeX · 11 Oct 2017
  12. Using pip-tools to manage my Python dependencies
  13. Some useful Git commands for CI
  14. Ode to docopt
  15. A Python module for lazy reading of file objects
  16. Backing up full-page archives from Pinboard
  17. Listing keys in an S3 bucket with Python
  18. Backing up content from SoundCloud
  19. A visit to the Crossness pumping stationPictures from my recent visit to Crossness, a Victorian pumping station for London’s sewers.The world around us · 7 Jun 2017
  20. Accessibility at AlterConf
  21. A Python interface to AO3
  22. Experiments with AO3 and Python

2016 29

  1. Another example of why strings are terrible
  2. Why does Hypothesis try the same example three times before failing?
  3. Some low-tech ways to get more ideas
  4. Use keyring to store your credentials
  5. Rust macros are smarter than just text substitution
  6. Creating low contrast wallpapers with Pillow
  7. Tiling the plane with Pillow
  8. How to set the clock on a Tricity Bendix SIE454 electric cooker
  9. Why I use py.test
  10. How to set the clock on a Worcester 28CDi boiler
  11. A shell alias for tallying data
  12. My travelling tech bag
  13. aspell, a command-line spell checker
  14. Silence is golden
  15. Live captioning at conferences
  16. You can use shutil.which to check if an executable is in your PATH
  17. Python snippets: Cleaning up empty/nearly empty directories
  18. Python snippets: Chasing redirects and URL shorteners
  19. Clearing disk space on OS X
  20. Introduction to property-based testing
  21. Finding 404s and broken pages in my Apache logs
  22. A Python smtplib wrapper for Fastmail
  23. Safely deleting a file called ‘-rf *’
  24. Treat regular expressions as code, not magic
  25. Get images from the iTunes/App/Mac App Stores with Alfred
  26. Exclusively create a file in Python 3
  27. Germany has longer telephone numbers than the rest of the world
  28. How I use TextExpander to curb my language
  29. The Skeletor clip loop, 2015 edition

2015 15

  1. Pretty printing JSON and XML in the shell
  2. Backups and Docker
  3. Export a list of URLs from Safari Reading List
  4. Python and the BBC micro:bit
  5. Quick shell access for Docker containers
  6. Review: Effective Python
  7. Finding even more untagged posts on Tumblr
  8. Useful Bash features: exit traps
  9. Safer file copying in Python
  10. Useful Git features: a per-clone exclude file (.git/info/exclude)
  11. Cloning GitHub’s Contributions chart
  12. Some exam advice
  13. Pygmentizr
  14. Tidying up my 1Password
  15. Adding checkboxes to lists

2014 12

  1. Kitchen sink security
  2. Acronyms
  3. Skeletors All the Way Down
  4. Unpacking sets and ranges from a single string
  5. Notes on Tumblr
  6. Playing with 404 pages
  7. My new standing desk
  8. Updates to my site for finding untagged Tumblr posts
  9. A quick Alfred workflow for opening recent screenshots
  10. Getting plaintextLaTeK LaTeX from Wolfram Alpha
  11. Skeletor!
  12. Finding untagged posts on Tumblr, redux

2013 5

  1. Finding untagged posts on Tumblr
  2. Get an RSS feed of external audio posts on Tumblr
  3. Automatic Pinboard backups
  4. Darwin, pancakes and birthdays
  5. Zero