Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
105

Plates and states

from alexwlchan [alt+shift+b] in programming

I was recently visiting Vermont for a work trip – my first time in the USA since I was a child. I was drawn to the license plates on passing cars, and how they look different to the cars I’m used to. Whenever I visit new places, I enjoy looking for the tiny bits of infrastructure and design that make life different – stuff that everybody who lives there is used to, but which is novel or unusual to me. The open-plan layout of a telephone booth in Berlin. Slovenian post boxes which are bright yellow. The variety of call buttons used on pedestrian crossings. These details are only small, but they add up to give every place a unique feel. Often they don’t get written down or recorded, because “everybody knows” how these things work, and they don’t seem remarkable to somebody who experiences them every day. It’s hard to know these things without visiting, because so few people think they’re worth sharing – and many of them are lost to time, forgotten as places change and evolve. As I looked into US license plates, I learnt more about both American cars and America in general. Not all states require both plates In the UK, every car must have a number plate on both the front and back of the car. In the US, it varies by state – some states require both plates, other states only require a rear license plate. I’d seen US cars without a front license plate in TV shows and movies, and I thought maybe it was a Hollywood thing – special cars used for filming – but no, it turns out that’s perfectly legal in 19 US states. I only realised this was a thing a few weeks ago, when I heard Casey Liss mention “front plate states” in a podcast. Until then, I just took it for granted that all cars have plates on the front and back, because that was what seemed “normal” to me. I can see why you might prefer to have a car without a front license plate – if it’s a car with a striking design, it looks better without a plate. The aesthetics are why I thought maybe it was a specific choice for...
14th Aug 2024

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from alexwlchan

Abusing ID3 chapters to turn videos into glanceable podcasts

I listen to a lot of podcasts, and I like how they fit around other tasks. I press play, lock my phone, and put it down. I’m free to wash the dishes, fold the laundry, or shop for groceries. Unfortunately, more and more information is only published as a video. Technical talks, conference sessions, video essays – they don’t work in an audio-only podcast app. I could convert these videos to MP3 files, but that breaks down the moment a video isn’t pure spoken word. If a speaker says, “Look at this slide” or holds up a diagram, an audio-only file leaves me stranded. I don’t want to give up the podcast player I like, nor stare at a screen for an hour – but I do want the information in these videos. To solve this, I’m abusing my podcast player’s chapter support. This gives me the best of both worlds: I can listen to a video as audio-first, and glance at my lock screen if I need a moment of visual context. The idea: Chapters every few seconds MP3 files can have ID3 metadata, and ID3 metadata can include chapters. A chapter covers a particular time range, and it can have an associated title, description, and cover art. My podcast app of choice is Overcast, which can’t play videos, but it does have robust chapter support. I can jump between chapters, navigate a table of contents, and see per-chapter cover art. To get videos into Overcast, I’m creating MP3 files with a new chapter every few seconds, and the per-chapter cover art is a corresponding frame from the video. As I play the file, I get a slow, stop-motion-like rendition of the original video. If my phone is locked, I can glance at my lock screen and see the current frame in the Now Playing screen. Overcast is developed by Marco Arment, and I got this idea from Forecast, his app for adding chapters to podcasts. In particular, I was struck by its ability to create chapters that don’t display in the chapter list – ideal if I don’t want a table of contents with hundreds of entries. As I was developing my script, I compared my output to the output from Forecast to ensure I was creating the chapters correctly. The code: FFmpeg and Mutagen There are three steps in this process: Convert a video file to an MP3 Extract images from the video at a fixed interval Insert the images as hidden chapters in the MP3 file Let’s go through each in turn. 1. Convert a video file to an MP3 Converting a video file to an MP3 is a single FFmpeg command: ffmpeg -i video.mp4 audio.mp3 This is consistently the slowest step of the process, and I do wonder if I could use different settings or an alternative encoder to make it go faster – but it’s not slow enough to be worth further investigation. 2. Extract images from the video at a fixed interval Extracting images from a video needs a more complicated FFmpeg command: ffmpeg -i video.mp4 \ -vf 'fps=1/5,scale=iw*sar:ih,scale=min(iw\,945):min(ih\,945):force_original_aspect_ratio=decrease' \ thumbnail_%04d.jpg This extracts an image every 5 seconds, downscales any image larger than 945 pixels square (while preserving the original aspect ratio), and saves the results as sequentially numbered JPEG images (thumbnail_0001.png, thumbnail_0002.png, and so on). The key is the -vf flag, which defines two FFmpeg filters: The fps filter selects one frame every 5 seconds (fps=1/5). The first scale filter scales the width based on the sample aspect ratio (scale=iw*sar:ih). Without this filter, frames can be stretched and distorted. The second scale filter scales the input video, preserving the original aspect ratio (force_original_aspect_ratio=decrease), and ensuring the output images fit within 945×945px or the size of the input video, whichever is smaller. My limit is 945 pixels because that’s the largest size that cover art is shown on my iPhone. This filter still isn’t completely correct – it sometimes creates images from portrait videos that are smaller than I’m expecting – but it’s good enough. These are only thumbnails for glancing at, and if I want to change it later, I can always do the image resizing outside FFmpeg. 3. Insert the images as hidden chapters in the MP3 file Inserting the chapters into the MP3 file is more complicated. Although FFmpeg has basic support for ID3 metadata, as far as I know, it can’t insert chapters with per-chapter artwork. Instead, I’m going to reach for Python and the Mutagen library. Here’s the code to add a chapter to an MP3 file: from mutagen.id3 import APIC, CHAP, ID3, PictureType audio = ID3("audio.mp3") with open("thumbnail_0001.jpg", "rb") as f: img_data = f.read() image_frame = APIC(mime="image/jpeg", type=PictureType.OTHER, data=img_data) chapter_frame = CHAP( element_id="chp1", start_time=0, end_time=5 * 1000, sub_frames=[image_frame] ) audio.add(chapter_frame) audio.save() This creates a single chapter that lasts the first 5 seconds (0 to 5000 milliseconds), and the per-chapter cover art is thumbnail_0001.jpg. If we ran this in a loop, we could add images for every 5 second slice of the original video. This code is inserting two frames into the ID3 metadata: The CHAP (chapter) frame contains the timing information, and it can have subframes for metadata like title, chapter art, or associated URL. The APIC (attached picture) subframe contains information about a picture, which can either be a blob of image data or a URL to an image on the web. Normally, you’d also insert a CTOC frame which defines a table of contents, but I don’t want a TOC with hundreds of 5-second chapters, so I’m deliberately not doing this here. This is allowed by the ID3 spec – you’re not required to insert a CTOC frame if you’re using chapters, and you can have chapters that aren’t listed in your table of contents. To work out which frames I needed, I used Forecast to create some chapters by hand, and I inspected their frames. In particular, loading an MP3 and calling Mutagen’s pprint() method shows a human-readable list of frames, and then I could drill into the individual fields: from mutagen.id3 import ID3 audio = ID3("audio.mp3") print(audio.pprint()) I wrapped all this code in a project called glancecast, which allows you to convert a video file with a single command, with optional flags to set the frame length and chapter art size: $ python3 glancecast.py interesting_talk.mp4 interesting_talk.mp3 The process takes a minute or so to complete, most of which is spent transcoding the video file to MP3. The resulting MP3s are usually 40 to 50 MB in size, which is very reasonable. The outcome: How it looks in practice Here’s what one of these “glanceable” podcasts looks like in Overcast and on my lock screen: Maggie Appleton presented this talk over two years ago and it’s been on my “talks to watch” list ever since. Once I put it in Overcast? I listened to it in less than a day. It’s not a lot of extra information, but enough that I can quickly glance down and get the gist of what a speaker is saying. Both views update with a new frame every few seconds, or I can put my phone in my pocket and ignore the screen. I’ve used this approach for half a dozen videos so far, and I’m happy with the results. I expect to keep using it, because I have a long queue of videos I’ve been meaning to watch. If you’d like to try this, check out glancecast for the full code and instructions. [If the formatting of this post looks odd in your feed reader, visit the original article]

a week ago
How Tailscale tracked down a 16-year-old SQLite bug →

I wrote a post for the Tailscale blog about a long-running series of corruption incidents, and how they eventually led us to find an SQLite bug that predates my entire programming career. I’m incredibly proud of this, both the work and the blog post. Before Tailscale, I was coming from smaller teams where I didn’t get to tackle problems of this scale or complexity. This was exactly the sort of tricky, deep technical challenge I wanted to be part of (though I’d rather it hadn’t been quite so stressful)! I’m glad I got to play a small part in these incidents, and I learnt so much from the more experienced engineers I worked with. I never want to hear the words “SQLite corruption” again, but if I do, I’d want to have Tailscalars at my side. Writing the blog post has a blast, too. The piece transformed from a rough draft into a solid, engaging piece of writing, thanks to thoughtful feedback from many people at Tailscale. Most of my writing is self-edited, and it’s always a pleasure to work with a dedicated editor. Please check out the blog post if you haven’t read it already – I think it’s a fascinating technical story, and one readers of this site are bound to enjoy. [If the formatting of this post looks odd in your feed reader, visit the original article]

13th Aug 2026 • 2 votes
I don’t want to repeat repeat myself

Yesterday at work, a customer spotted a typo in our UI: “you can use the use the Tailscale CLI”. After the typo was fixed, I wanted to find other cases of accidentally repeated words or phrases. I used two regular expressions to search every codebase for unnecessary repetition. The first regex finds repeated words: \b([A-Za-z]+) \1\b Backfill product data from from Stripe Learn more about about inviting users Argument must be be one of host name, IP set name, IP prefix, or IP There’s a capturing group for a single word made up of letters ([A-Za-z]+), a space, then a backreference to the group. That expression is surrounded by word boundary assertions \b, which check that I’m at the start/end of a word – this avoids finding repeated character sequyences that within longer words, like “with the reason”. The second regex finds repeated phrases: \b([A-Za-z]+ [A-Za-z]+) \1\b Follow the steps in the in the "How to" section Log in to in to your account To configure federated identities federated identities using the Go SDK I’ve changed the capturing group, so now it looks for two words separated by a space. Sometimes repetition is useful, like when I really really went to emphasise a point, but often it’s just a typo. Cleaning up these mistakes has been a fun Friday cleanup task. [If the formatting of this post looks odd in your feed reader, visit the original article]

3rd Jul 2026 • 1 votes
Using Pytester to test my Playwright fixtures

A month ago, I wrote about my Playwright fixture for testing static websites in a browser. I’ve been copying that fixture from project-to-project, but recently I decided to add it to chives, the utility library I use for all my static websites (or tiny archives). One of my rules for chives is that everything in it has to be tested – but how do you test a pytest fixture? Test code is just code, and it isn’t immune to bugs. Who tests the tests? Enter Pytester, a tool designed for testing pytest plugins. Pytester allows you to run isolated test suites, make assertions about the outcomes, and verify the behaviour of custom fixtures. In your top-level test suite, you always want everything to be passing, but with Pytester you can write a mixture of passing and failing tests, and check the results are what you expect. Pytester is disabled by default, so you first enable it in your top-level conftest.py file (the pytest configuration file where you configure plugins and fixtures): # conftest.py pytest_plugins = ["pytester"] Here’s an example of using Pytester where we create a test suite with two tests and check that one passes, one fails: from pytest import Pytester def test_with_pytester(pytester: Pytester): """ Run an isolated test suite with pytester. """ # Make a temporary pytest test file pytester.makepyfile( """ def test_arithmetic(): assert 2 + 2 == 4 def test_list_inclusion(): assert "yellow" in ["red", "green", "blue"] """ ) # Run the isolated test suite with pytest result = pytester.runpytest() # Check that one test passed, one failed result.assert_outcomes(passed=1, failed=1) I can imagine creating something similar with some complicated collection of nested functions, exec() and pytest.raises, but using Pytester is a cleaner interface than what I’d build. Under the hood, Pytester creates a temporary directory, writes specified files into it, then runs a fresh pytest subprocess against it. It has helper functions for writing files, including Python files (makepyfile), a conftest.py file (makeconftest), and plain text files (maketxtfile). When we’re testing a fixture, we can create a conftest.py file that imports that fixture, then reference it in the tests. Here’s a more complicated example, where we import one of my Playwright fixtures in my conftest.py, write an HTML file into the temporary directory, then use them both in the test: from pytest import Pytester def test_browser_fixture(pytester: Pytester): """ Try testing the browser fixture with pytester. """ # Make a conftest.py file pytester.makeconftest(""" from chives.browser_fixtures import browser """) # Make an HTML file (pytester.path / "greeting.html").write_text(""" <p>Hello world!</p> """) # Make a temporary pytest test file pytester.makepyfile( """ from chives.browser_fixtures import file_uri from playwright.sync_api import Browser, expect def test_browser_fixture(browser: Browser) -> None: uri = file_uri("greeting.html") p = browser.new_page() p.goto(uri) expect(p.get_by_text("Hello world!")).to_be_visible() """ ) # Run the isolated test suite with pytest result = pytester.runpytest() # Check that one test passed result.assert_outcomes(passed=1) This pattern is sufficient for many fixtures, but it doesn’t work for Playwright – if you run this test, the isolated test suite gives an error rather than a passing test. Playwright needs you to install a web browser to work (for example, playwright install webkit), and Pytester runs in a sufficiently isolated environment that Playwright can’t find the browsers you already have installed. We could run the install command inside the temporary directory, but that would be slow and inefficient – it would be better if we could tell Playwright to look for the already-installed browsers elsewhere. If we set the PLAYWRIGHT_BROWSERS_PATH environment variable inside our isolated test suite, Playwright will look there for browsers. First, we need to work out where browsers are installed – we could hard-code the location, or we could inspect the executable_path property property on a browser: from pathlib import Path from playwright.sync_api import sync_playwright import pytest @pytest.fixture(scope="session") def playwright_browsers_path() -> str: """ Return the cache directory where Playwright browsers are installed. """ with sync_playwright() as p: # In my local builds, this returns a path like: # # ~/Library/Caches/ms-playwright/webkit-2272/pw_run.sh # # Unwrap two levels to get to the `ms-playwright` folder. return str(Path(p.webkit.executable_path).parent.parent) Then we need to set this as an environment variable inside the Pytester test suite. I couldn’t find an easy way to set an environment variable; the best approach I came up with was to modify os.environ inside the conftest.py file. (Perhaps we could access the MonkeyPatch object and set more environment variables, but using private attributes is icky.) Here’s how the new test starts: def test_browser_fixture(pytester: Pytester, playwright_browsers_path: str): """ Test the browser fixture with pytester. """ # Make a conftest.py file pytester.makeconftest(f""" from chives.browser_fixtures import browser import os os.environ["PLAYWRIGHT_BROWSERS_PATH"] = {playwright_browsers_path!r} """) ... and now the overall test passes. Here’s the complete code for the new test: test_browser_fixture.py from pathlib import Path from playwright.sync_api import sync_playwright import pytest from pytest import Pytester @pytest.fixture(scope="session") def playwright_browsers_path() -> str: """ Return the cache directory where Playwright browsers are installed. """ with sync_playwright() as p: # In my local builds, this returns a path like: # # ~/Library/Caches/ms-playwright/webkit-2272/pw_run.sh # # Unwrap two levels to get to the `ms-playwright` folder. return str(Path(p.webkit.executable_path).parent.parent) def test_browser_fixture(pytester: Pytester, playwright_browsers_path: str): """ Test the browser fixture with pytester. """ # Make a conftest.py file pytester.makeconftest(f""" from chives.browser_fixtures import browser import os os.environ["PLAYWRIGHT_BROWSERS_PATH"] = {playwright_browsers_path!r} """) # Make an HTML file (pytester.path / "greeting.html").write_text(""" <p>Hello world!</p> """) # Make a temporary pytest test file pytester.makepyfile( """ from chives.browser_fixtures import file_uri from playwright.sync_api import Browser, expect def test_browser_fixture(browser: Browser) -> None: uri = file_uri("greeting.html") p = browser.new_page() p.goto(uri) expect(p.get_by_text("Hello world!")).to_be_visible() """ ) # Run the isolated test suite with pytest result = pytester.runpytest() # Check that one test passed result.assert_outcomes(passed=1) The full test suite is more extensive, and checks that certain scenarios fail or error – will the fixtures spot the mistakes I expect them to? For example, my Page fixture is meant to load a page and fail the test if there are any console warnings or errors; does it actually fail the test correctly? I don’t expect to use Pytester very often, because it’s rare for me to write fixtures complex enough to need their own test suite – but sometimes I do, and it’s good to know how to create another layer of safety net. [If the formatting of this post looks odd in your feed reader, visit the original article]

5th Jun 2026 • 1 votes
Using Playwright to test my static sites

I build a lot of static websites – including this site and all of my local media archives – and I want to test them. Most of my pages are static HTML and I can write automated tests that analyse the HTML, but for more complex sites I have JavaScript that runs in the browser and modifies the page. The only way to test that functionality is to open the page in a browser, click around, and see what happens. I could do that manually, but it quickly gets tedious. To automate this process, I’ve been using a testing framework called Playwright, which is designed for this sort of end-to-end testing. It’s a tool that allows you to programatically control a web browser, look at the contents of a page, and make assertions about what’s there. Playwright can be used to test or script any kind of web app; I’m using it for static sites because those are the only web apps I have. Playwright is available as a CLI, or there are libraries to use it with TypeScript, Python, .NET, and Java. All my other tests are written in Python, so that’s what I’m using. Writing a basic test with Playwright To set up Playwright with Python, you install the playwright library using pip or uv, then install a web browser for Playwright to control. (You can’t use Playwright with the browser you use day-to-day; you need special binaries with control hooks.) I use Safari as my main browser, and Safari is based on WebKit, so let’s install that: $ uv pip install playwright $ python3 -m playwright install webkit Then we can start writing tests. Here’s a basic test in which Playwright launches WebKit, opens example.com, and checks the text Example domain is visible on the page: from playwright.sync_api import expect, sync_playwright def test_basic_playwright() -> None: """ Run a basic test with Playwright: load a web page and check it contains the expected text. """ with sync_playwright() as p: browser = p.webkit.launch() page = browser.new_page() page.goto("https://example.com/") expect(page.get_by_text("Example domain")).to_be_visible() browser.close() For a larger app, you might run your tests with multiple browsers to check compatibility – Playwright supports lots of other browsers, including Chromium, Firefox, and Mobile Safari in emulation. I’m just testing private sites where I’m the only user, so a single browser is fine. This test passes in about half a second on my computer. That’s fine for a single test, but it would add up if I had lots of tests, each starting and stopping the browser every time. It would be nice to make that process faster, and to reduce some of the boilerplate as well. A pair of Playwright fixtures To reduce the repetition and reuse the browser instance, I have a couple of pytest fixtures to simplify things. The first is a session-scoped fixture that starts the browser at the start of the test run, and closes it when I’m done: from collections.abc import Iterator from playwright.sync_api import Browser, sync_playwright import pytest @pytest.fixture(scope="session") def browser() -> Iterator[Browser]: """ Launch an instance of WebKit to interact with in tests. """ with sync_playwright() as p: webkit = p.webkit.launch() yield webkit webkit.close() Because this is a session-scoped fixture, it only runs once per test suite – that means the browser is only started once, then the same instance is reused for all the tests. This makes a large test suite significantly faster. My other fixture is a bit more complicated – it gives you a page to interact with, and at the end of the test it checks the page didn’t have any warnings or errors. This is a strict approach, which helps me spot errors in areas I wasn’t explicitly testing. Here’s the fixture: from collections.abc import Iterator from playwright.sync_api import Browser, Page import pytest @pytest.fixture(scope="function") def page(browser: Browser) -> Iterator[Page]: """ Open a new page in the browser. If there are any errors or warnings when loading the page, the test will fail when this fixture is cleaned up. """ p = browser.new_page() # Capture anything that gets logged to the console. console_messages = [] p.on("console", lambda msg: console_messages.append(msg)) # Capture any page errors page_errors = [] p.on("pageerror", lambda err: page_errors.append(err)) yield p # Check there weren't any console errors logged to the page. console_errors = [ msg.text for msg in console_messages if msg.type == "error" or msg.type == "warning" ] assert console_errors == [] # Check there weren't any page errors assert page_errors == [] These two fixtures allow for tighter, faster tests, focusing on what the test is actually checking. Here’s the example test, rewritten to use this fixture: def test_playwright_with_fixture(page: Page) -> None: """ Run a test using my Playwright fixture: load a web page, check it contains the expected test, and check it loads without errors. """ page.goto("https://example.com/") expect(page.get_by_text("Example domain")).to_be_visible() I use the page fixture for most tests, where I want to spot any unexpected errors or warnings. If I’m testing error handling specifically, I use the browser fixture and create a new page which isn’t treated as strictly. Getting file:/// URIs for Playwright Normally Playwright is used with http: and https: URLs, but my static websites are stored as HTML files on my local disk, and I often open them with file: URLs. I could spin up a web server in my tests, but that’s extra overhead and might affect the results – there are subtle differences between how browsers handle pages opened with file: vs http:. To convert file paths to file: URLs, I use the pathname2url function from the urllib.request module. I combine this with os.path.abspath to get a full URL I can pass to Playwright: >>> from os.path import abspath >>> from urllib.request import pathname2url >>> path = "index.html" >>> pathname2url(abspath(path), add_scheme=True) 'file:///Users/alexwlchan/repos/alexwlchan.net/index.html' Assertions in Playwright Playwright has a different set of assertion helpers to regular Python tests, and it takes some getting used to – I still have to consult the documentation when I write new tests. Here are examples of assertions I’ve written using Playwright: Testing that a redirect is working: resp = page.goto("https://alexwlchan.net/projects/chives/files/doesnotexist.txt") assert resp is not None assert resp.status == 200 assert resp.url == "https://alexwlchan.net/projects/chives/files/?missing=doesnotexist.txt" Test that text does or does not appear on a page: from playwright.sync_api import expect page.goto("https://www.example.com") expect(page.get_by_text("Example Domain")).to_be_visible() expect(page.get_by_text("Alex Chan")).not_to_be_visible() or: assert "Example Domain" in page.content() assert "Alex Chan" not in page.content() Locate an element with a CSS selector, and check it does or doesn’t appear on a page: page.goto("https://www.example.com") expect(page.locator("h1")).to_be_visible() expect(page.locator("h2.title")).not_to_be_visible() Locate an element, and make assertions about its attributes: page.goto("https://www.example.com") href = page.locator("a").first.get_attribute("href") assert href == "https://iana.org/domains/example" Locate an element, and make assertions about the text it contains: page.goto("https://www.example.com") assert page.locator("a").inner_text() == "Learn more" Check that an element with particular inner text is visible on the page: page.goto("https://www.example.com/") expect(page.locator('//h1[text()="Example Domain"]')).to_be_visible() Locate an element immediately following a different element. I’ve used this a couple of times when I have tables or definition lists with a label in one element, and a value in another: dt_locator = page.locator('//dt[text()="Profile page:"]') next_dd = dt_locator.locator("xpath=following-sibling::*") assert ( next_dd.inner_html().strip() == '<a href="https://www.flickr.com/photos/nasahqphoto/">NASA HQ PHOTO</a>' ) Check the number of matching elements on a page; for example, the length of a list: page.goto("https://alexwlchan.net/articles/") assert page.locator("#list_of_posts li").count() >= 10 Check the title of the page: page.goto("https://www.example.com/") assert page.title() == "Example Domain" Check the behaviour of the page when JavaScript is disabled: context = browser.new_context(java_script_enabled=False) page = context.new_page() expect(page.locator("noscript .error")).to_be_visible() noscript_elem = page.locator("noscript .error") assert noscript_elem.inner_text() == "You must enable JavaScript to use this page." This is just a fraction of what Playwright can do; it can be used to build far more complicated tests that walk through a web app and test multi-step user flows. I’m only using it to make assertions about snippets of JavaScript, but it’s still useful. For a long time, I told myself that my static sites were simple enough not to need testing, but that didn’t prevent bugs from slipping in, and it limited what I could build. Now I can write proper tests for my sites, I can be more confident I haven’t broken anything, I can experiment faster, and I can try more ambitious ideas. [If the formatting of this post looks odd in your feed reader, visit the original article]

2nd May 2026 • 1 votes

More in programming

George Dryden

Reading my recent computing retrospective, I realised there was a big section missing: the people in my life that made an impact and helped shape my career. Outside my immediate family, one person made an outsized contribution, and I’m fairly certain that without his influence my life would have taken a very different path. The fact that I’m still here in 2026, still writing code and being fortunate enough to have a career in something I love is testament to him. So I’d like to take a few moments to talk about my old secondary school teacher, George Dryden. Denied Back in 1995, I had a problem. I knew I wanted to study computing at university and build a career out of my passion, but there was a snag. For those unfamiliar with the UK schooling system, when you’re 15-16 you take a set of GCSE exams in a broad range of subjects. After that, you pick around 3 subjects to really focus on over a period of 2 years. These are called A Levels, and they are a big step up and are meant to prepare you for a degree-level course at university. Admission to university is also governed by these results - if you want to study computing, you’re going to need a computing A-Level, and most universities will only accept you (or “make an offer”) if you achieve a certain grade. And whilst I had taken computing at a GCSE level, my school did not offer a computing A-Level course. I instead had to settle on “Design & Technology”, which just didn’t inspire me. Instead of working on my portfolio and projects, I spent most of my time daydreaming and writing code on the Acorn Archimedes computers that were the staple of every 90s UK school. No disrespect to the teachers - they were all awesome - but it just wasn’t for me. I was miserable, and by the end of my first year, I was well on my way to failing outright with my entire future plans seemingly going up in smoke. Someone noticed That’s when George stepped in. He’d taught me computing right the way through my GCSEs, and with no A-Level course on offer, that was officially where his involvement was supposed to have ended. It didn’t. He had noticed my constant presence in the computing labs - before and after school, during lunch breaks, free “study” periods - working on some little pet project or digging into RISC OS internals. I remember him as warm, with a wicked, dry sense of humour, and a refreshingly spiky attitude to authority - I always got the sense he’d worked out for himself which rules were worth taking seriously and which ones weren’t. And he always had time for me. I spent years pestering him with questions that had nothing to do with anything on the syllabus, and he’d always find a way to answer them that actually made sense. He was just as supportive of my odd little obsessions. At one point I’d got deep into the BBS scene, which I thought was the coolest thing ever, and decided what the school really needed was an internal BBS running on its own network. So I wrote one. It was deeply cringeworthy, obviously - but George helped me put posters up around the school advertising it, and even gave it a mention in assembly one morning. I think about five people in total ever checked it out. It didn’t matter: a teacher had stood up in front of the entire school and treated my weird little project like it was worth something, and that was a hugely validating moment for me. Off the books He recognised the passion, and eventually he made a suggestion: What if I quit the Design and Technology course, and instead attempt the A-level course myself? Personally, I also suspect he was enjoying himself. There was some internal school politics behind why computing wasn’t offered at A-Level in the first place - I never knew the details - and I think the prospect of one of his students simply going out and getting the qualification anyway appealed to him on two separate levels. It would get me where I wanted to go, and it would wind up exactly the right people. It wouldn’t be easy, he warned. The school would be against it, plus it was a two-year course which I’d have to cram into one year. I’d have to do it all myself - studying, lesson planning, coursework - he couldn’t help me in an official capacity, but he said he’d advocate for me and help where he could. If I had assignments, he’d send them off to be graded and would give feedback in his own time. He’d enter me in for the exams and also gave me a set of keys to the computer lab so I could use it whenever I needed. It was the first time anybody outside my own family had really shown faith in my abilities and encouraged me to take a stand. It was a pivotal moment for me - I realised if I wanted something badly enough I would have to fight for it, but I could still make it happen. I didn’t have to take “NO” for an answer - a lesson I think I picked up from watching him as much as from anything he ever actually said to me. After a few weeks of dithering, I took the jump. I remember a few awkward meetings with the school administration but thanks to his behind-the-scenes work, I was soon following my dream. An intense year And yes, it was bloody hard work. I had to condense an entire two year course into under a year, be disciplined enough to produce my own study plan, and be critical enough of my own shortcomings that I could focus my study where it was needed. I pretty much lived and breathed it for months straight and was more-or-less a permanent fixture in the labs or school library poring over my course books. I’d make lists of questions and chat to George over lunch, and he’d provide guidance and encouragement. It was a lonely way to learn with no classmates to compare notes with, no lessons to turn up to, and right up until the end I had no real idea whether any of it was good enough - but bit by bit, it started to feel like something I could actually pull off. And sure enough, in the late spring of 1996, I sat down in an exam hall with my fellow students, the only one with an A-Level computing question paper in front of me. The final exam went by in a blur - I can only remember a few of the questions now (and a peculiar obsession with the Pascal language) - but I do remember the euphoria as the invigilator called “time’s up, pens down, close your papers NOW”. I had done it. A few nerve-wracking months later, my Mum drove me into school to pick up my results. I ripped open the envelope and saw it - I’d passed with an A grade! I literally ran up the stairs to George’s office next to the computing labs to thank him personally. I’d taken my camera into school to take a few last photos for memory’s sake and snapped this photo of him before I walked out the school gates for the last time: A different path Because of him, I managed to get into my university of choice, studying computing with a focus on networks. Because of that, I landed my first job working as a “webmaster”, and my career since has been one of the highlights of my life. All these years later, it’s a real privilege to be able to get up each morning and actively look forward to working in an industry I love. Without George stepping up for me and encouraging me to believe in myself, none of that would have happened. I wouldn’t have had the career I have, and I wouldn’t be where I am now. I met my wife when we both worked at a software company - she sat at the desk behind me - so even my home and family life can be traced back to that spring of 1996. And the A-Level itself was only half of what I took away from that year. The qualification opened the door to university, but the lesson that came with it was every bit as important: that a “no” isn’t always the end of it, and that sometimes the answer can be argued with. I’m so proud of what I managed to achieve all those years ago, and even more thankful to have had someone like George in my life to put me on the right track. Mr. Dryden I did see him again after I left. He drank in one of my local pubs - a pub I’d been going to for a good while before I was technically old enough to be in it - and I’d say hello if I spotted him in there, mostly in the months before I moved away to university. After that it was only a handful of times. For years I’d find myself scanning the room whenever I was back home and in for a pint, half expecting him to be at the bar. At some point I stopped seeing him altogether and eventually moved across the country. The trouble was I never really knew how to talk to him outside of school. He was always Mr. Dryden, or just “Sir”, I don’t think I ever once called him George to his face! I was (and still am if I’m honest) fairly socially awkward, and I never worked out how to phrase the thing I actually wanted to say: that he had changed the entire direction of my life, and I wasn’t sure he knew it. So instead I’d say hello, and ask how he was, talk about nothing much, and go back to my friends. Epilogue Sadly, 3 years ago now, I opened the latest issue of my old school alumni newsletter to read that he’d passed away. The photo at the start of this article was taken from his obituary article and I read that he’d had a long illness and had suffered from dementia at the end. I did write to him years ago by email - I don’t know if he ever got it, or was in any capacity to understand what he’d done for me, but I hope so. There’s an old saying by one of my favourite authors (Terry Pratchett) that “no one is finally dead until the ripples they cause in the world die away”. In one of his books, a character keeps the memory of his son alive by passing his name along a series of telegraph towers. It’s in that spirit that I’m writing this post - I debated it for many years as it’s very personal to me and I also have no contact with any of George’s family so I have no idea what they would make of it all. But even though it’s 30+ years ago now, I will never forget him or what he did for me - and at least now, if someone searches his name it’ll be recorded here for as long as I’m alive and running this site. Thank you, Sir. George Dryden 1942-2023

yesterday • 1 votes
How Copy-on-Write Works with Memory-Mapped Files

Let’s step inside the kernel and understand how it implements copy-on-write and what are its implications for the performance of user-space systems

yesterday • 1 votes
If we do not stop to help each other, what do we become?

Yesterday, I received this email as a response to You Can't Vibe Code Love. It's such a remarkable and powerful statement that I asked permission to share it here, in its entirety, with personal information redacted: Hey Jeff, Hope you and your family are doing well.

3 days ago • 1 votes
You are still valuable

A frustrated Reddit post about being a condom between an AI and production made the rounds in our team. Here is why I think the opposite is true and what it means for how we review code, plan work and think.

4 days ago • 2 votes
A Short Update on Designing for Foldable Devices

And here we are three years after I wrote about the Google Pixel Fold being announced, followed now with the announcement of the iPhone Duo...(I have questions about the naming by the way). Four years ago I was talking about web primitives in the platform for the Surface Duo. My how time flies. There are CSS media features, a Viewport Segments API, a Device Posture API but Chromium based browsers are the only ones currently supporting these things. I haven't been able to find any signal yet on whether Safari will support these things in the web platform as the developer docs focus on application development. If you're interested in trying out the platform features, you can emulate the Surface Duo and Galaxy Z Fold in the developer tools. And if you're thinking, do I really have to have my website adapt to two screens? The answer is no. Adding a design to an application or dual screen makes sense if you have an experience that has two simulataneous contexts that are useful e.g. a list of email messages/inbox on one screen, an open message, email thread or email composer on the other. Here's one of my talks from 2022 if you're interested in learning more about what's available in the browser for dual screen/foldable devices. Happy building :)

4 days ago • 1 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in