Just because each item makes sense doesn’t mean they make sense together

Years ago I worked somewhere which, for a couple of years, had a “staff club”. This was a room in a weird spot behind a staff canteen (also an odd place…) which I guess was otherwise dead space. So some committee somewhere decided to designate it as a club – which in practice means it had a ping-pong table.

A recently-retired person presided over the club (possibly as a sinecure?). There wasn’t much to do – it was a room with a ping-pong table – but you can’t just do nothing. So she made signs. Signs for everything. Some a little preachy, “please open the window on a hot day so any odours don’t affect other club users”. Every cupboard labeled. Administrative notices. Signs about coffee. Just a lot.

I mentioned this to her one day – “wow that’s a lot of signs!” – and her response was:

  • “For every sign you see, there is a reason it’s there”

And I thought this was kind of profound because it’s obviously true and completely wrong at the same time. In her mind, each sign had earned its place. But she was unaware, possibly because it happened over time, that together, with all of these signs, it looked like a sign shop. And any signal that might come from a really important sign – “do not open, you will die.” – is lost.

Perhaps something similar is happening at IKEA? (How’s that for a link?)

Here’s what I received for a recent purchase:

OK so it’s in French but you hopefully get the idea – this seems like a lot of emails for one transaction:

  • 1 x login security code. This is fine, we allow this. I don’t want anyone snooping on my pillow talk.
  • 1 x redemption of voucher that I had from a previous purchase. Not so bad. But it is a design decision to have this happen separately from the purchase, and the decision does have fallout.
  • 4 x order confirmation. YOU ARE CRAZY. A couple are slightly different – one is designated an invoice, one has T&Cs – but come on.
  • 3 x delivery updates. I think these are fine? People do like to see stuff moving towards them.
  • 1 x survey thing, the obligatory “how did we do?”. (Side note: are there any decent studies of the quality of data that these produce? I always ignore them and my feeling is that they give you a nice specific number, completely unrelated to reality.)

Did you spot the deliberate mistake?

There is one more email in the list – “Votre remboursement” / “Your reimbursement” – which is about the only thing that actually happened:

I guess this explains why there was no bed sheet in the delivery?

But see that it doesn’t say anything like “sorry, we didn’t have your bed sheet”. The customer, drowning in information, is left to deduce the true meaning for themselves. It’s just another transaction email, one of eleven, which only hints at the story left untold…

Dan

Ryanair dark UX patterns summer 2026 refresher

Everyone likes dark UX patterns – such fun!

Ryanair are Europe’s most profitable airline and they are masters of this noble form.

This is an all time classic from around 8 years ago – to not buy travel insurance, you must select Don’t Insure Me, midway down a list of countries:

"Don't Insure Me" listed between Denmark and Finland

I have the joy of doing some budget flying this summer and I thought I’d see how upsell-alicious the check-in process is in Trumpyear 2026:

I count 9 stages a user has to successfully navigate to avoid extra payment:

  1. “No, don’t want to be insured”
  2. Don’t be tricked into unlocking check-in for your return flight, this costs.
  3. Roll the dice by finding and selecting the random seat option.
    Do you feel lucky punk?
  4. Confirm you understand the precarious and unsettling nature of random allocation.
    Maybe you want a break from your companions?
  5. “Last chance to choose where you sit”
  6. Opt for 1 Small Bag only. A scary warning pops up about being charged at the gate.
    To be fair, I did recently see this happen to a couple at the airport. They feebly argued their case – pun intended – but I completely agreed with the airline staff. If the case don’t fit…
  7. Don’t click “Upgrade to Priority & 2 Cabin Bags”. This one is particularly sneaky as it doesn’t have a “No” option, you must dismiss the window.
  8. Scroll past security fast track and pre-paid credit, which at least just needs a “Continue”.
    The kid sitting next to me on the flight back bought some Versace aftershave and I pretended to care/be impressed because I am a nice person.
  9. Don’t rent a car, don’t buy parking, don’t buy a train(?)

Tada – you are checked in.

You get one final ad, I assume, for a Sam Altman fever dream in which humans EULA consent to become foie gras in exchange for tokens:

Order to Seat - Beat the trolley

I will finish with an actually sensible/possibly useful postscript.

Based on a small amount of recent experience, the best strategy for Ryanair is to check in at the last possible moment. If they’ve given away all the bad seats, they’ll be forced to give you a good one, and I got an exit aisle seat, which also gave access to the precious overhead bin.

The best strategy for Lufthansa is to check in as early as possible. They still offer to sell you a “better” seat. But you can immediately see what spot you’re assigned, and they fill up the plane from front to back in a refreshingly old-fashioned manner, so earlier is better.

Hope you enjoyed this, more soon.

Dan

Introducing Langwag, a mega interactive language learning app

Hi all,

Today I’m announcing a project that I’ve been working on for a while:

Langwag: a language learning app. It supports more than 30 languages. The idea is that you can copy and paste any news story into it to get a personalised translation and language lesson.

I’ve found this to be a satisfying way of learning since I spend a lot of time reading the news anyway. It seems to work well because it’s driven by your interests – and so it’s much more interactive than slot machine Owl-based alternatives. A friend said she understood more than expected because she already knew the “characters” (orange guy, drunk FBI guy, torso rocket guy, you know).

It has a bunch more features, inspired by what I’ve found I actually need to help me learn French while living in France: look up and practice phrases, hear things read out loud, ask questions. It also has a cute dog logo. I have ideas for improvements, but really it’s over now to you, the world: what would you like to see?

There is a hopefully pretty good demo on the public site so you can see how it works. You can add a few of your own news stories for free and, if it clicks for you, Early Adopter access is just $5 / month – less than a lactose free venti iced chai latte with lavender cold foam (a real drink that I learned about yesterday).

Dan

Async PostgreSQL with FastAPI dependency injection & SQLAlchemy

Hello!

It’s a clear-skied, crisp day here in Minnesota. Winter is coming, but not today.

While we wait for the world to turn and the seasons to change, why not pass some time thinking about database configuration?

I recently moved from SQLite to PostgreSQL as the database for pythondocs.xyz, my project that tries to bring a Google-like experience to Python’s official documentation. (The main motivation was to improve search results with Postgres’ full-text search capabilities.)

Swapping the DB engines proved to be remarkably straightforward: the SQLAlchemy ORM abstracted away dialect differences, and I was already using an abstract Database class with FastAPI, so I just needed to write another implementation.

Keep reading for a walkthrough of the code…

Pre-requisites

You need Postgres running on your machine or in a Docker container.

I’m using Docker Compose for this project. I won’t explain more today but I’m happy to make it the subject of a future blog post if people are interested?

You also need these Python packages:

  • uvicorn (web server), python-dotenv (settings loader), fastapi (web framework), sqlalchemy (ORM) , asyncpg (database driver)

I’m presenting this example as a single Python file so that it’s easy to copy and understand. But I’ll also mention original file paths, as you’ll definitely want more organization in a real project.

Here are the imports for the example:

import os
from abc import ABC, abstractmethod
from typing import AsyncIterator, Optional

import uvicorn
from dotenv import load_dotenv
from fastapi import Depends, FastAPI
from fastapi.responses import JSONResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker

# not included in example
from app.models import Folder

Config elation

The config module provides production or development settings, depending on environment variable. It also loads the Postgres password from an .env file. The idea is that you add .env to your .gitignore file so that you don’t mix secrets with the rest of your code.

# config.py
load_dotenv()


class Config(ABC):
    POSTGRES_USERNAME = "postgres"
    POSTGRES_DB_NAME = "postgres"
    # localhost for development purposes
    POSTGRES_HOST = "localhost"
    POSTGRES_PORT = "5432"
    # password stored in .env file
    POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")
    SQL_COMMAND_ECHO = False


class DevelopmentConfig(Config):
    SQL_COMMAND_ECHO = True


class ProductionConfig(Config):
    # hostname in Docker network for production
    POSTGRES_HOST = "db"


def get_config() -> Config:
    env = os.getenv("ENV")
    if env == "development":
        return DevelopmentConfig()
    return ProductionConfig()


config = get_config()

Data based

Here, we define an abstract Database class, with a __call__() method that works with FastAPI’s dependency injection. Its setup() method is provided in concrete implementations, like PostgresDatabase.

# database/base.py
class Database(ABC):
    def __init__(self):
        self.async_sessionmaker: Optional[sessionmaker] = None

    async def __call__(self) -> AsyncIterator[AsyncSession]:
        """For use with FastAPI Depends"""
        if not self.async_sessionmaker:
            raise ValueError("async_sessionmaker not available. Run setup() first.")
        async with self.async_sessionmaker() as session:
            yield session

    @abstractmethod
    def setup(self) -> None:
        ...


# database/postgres.py
def get_connection_string(driver: str = "asyncpg") -> str:
    return f"postgresql+{driver}://{config.POSTGRES_USERNAME}:{config.POSTGRES_PASSWORD}@{config.POSTGRES_HOST}:{config.POSTGRES_PORT}/{config.POSTGRES_DB_NAME}"


class PostgresDatabase(Database):
    def setup(self) -> None:
        async_engine = create_async_engine(
            get_connection_string(),
            echo=config.SQL_COMMAND_ECHO,
        )
        self.async_sessionmaker = sessionmaker(async_engine, class_=AsyncSession)

Stitch it all together

There are a few things going on here:

  1. An instance of PostgresDatabase is created in the depends module.
  2. The fast_api object is created in main.
  3. db.setup() is run as a FastAPI startup event only. This means that all code files can be safely imported without side effects.
  4. A route is definied which performs a simple DB query using FastAPI’s Depends.
  5. Finally, the uvicorn web server is started.
# depends.py
db = PostgresDatabase()

# main.py
fast_api = FastAPI()


@fast_api.on_event("startup")
async def setup_db() -> None:
    db.setup()


# routes.py
@fast_api.get("/example/")
async def db_query_example(
    session: AsyncSession = Depends(db),
) -> JSONResponse:
    results = await session.execute(select(Folder))
    return results.all()


# run_uvicorn.py
if __name__ == "__main__":
    uvicorn.run(
        "fastapi_postgres_async_example:fast_api",
    )

That’s it!

If the computer Gods smile upon you, you now have a working – and pretty fast – database configuration.

Here’s what my /example/ route looks like. You will need to supply your own data.

Thanks for reading! I hope this has been useful to someone. Please let me know in the comments.

The full code is over here on GitHub.

Inlay Type Hints: a cool new feature for Python & VS Code

Yesterday, I was playing around with my VS Code settings, which is something normal that I do for fun.

I came across settings for Inlay Type Hints, which is a new feature to me, but apparently has been available since July.

Here’s a quick demo:

Demo of Inlay Type Hints for Python in VS Code

You can see that Pylance (VS Code’s Python language server) now displays inferred types for function returns and variables that have not been explicitly type annotated.

The user experience is good but I have a few ideas for improvements:

  • It would be great if type inlays had color/mouse-over information before being inserted,
  • and if types were also automatically imported when inserted.

For now, I have function return annotation enabled all the time, and turn variable annotation on and off depending on what I’m doing – it can be a bit spammy in dense code, but it’s invaluable for debugging. (It would also be nice to be able to toggle annotations with a click!)

Overall, I think this a really cool feature. It underlines the big improvements in Python tooling – by VS Code in particular – that have been made possible by the gradual introduction of type annotation support to the language.

Timeline of changes to type annotations from Python 3.0 to 3.10.
Source: Towards Data Science