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:
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:
“No, don’t want to be insured”
Don’t be tricked into unlocking check-in for your return flight, this costs.
Roll the dice by finding and selecting the random seat option. Do you feel lucky punk?
Confirm you understand the precarious and unsettling nature of random allocation. Maybe you want a break from your companions?
“Last chance to choose where you sit”
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…
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.
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.
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:
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.
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).
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?
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:
An instance of PostgresDatabase is created in the depends module.
The fast_api object is created in main.
db.setup() is run as a FastAPI startup event only. This means that all code files can be safely imported without side effects.
A route is definied which performs a simple DB query using FastAPI’s Depends.
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
It copies a SQLite database from disk into memory, so it’s very fast. It’s great for read-only workflows – dashboards and the like. It’s not suitable for sites that accept user input, as it makes no attempt to preserve updates to the database.
The config works well for pythondocs.xyz: I generate the site’s database “offline”, with a standalone parser application, and I ship the resulting database file with the web application. When the web app starts up, the database is copied into memory, and you get nice fast database access (even if your queries aren’t super efficient!)
The main dependencies are sqlalchemy, the predominant Python ORM, and aiosqlite, an async replacement for the Standard Library’s sqlite3. I use the database with FastAPI but it should work in other applications.
async def __call__(self) -> AsyncIterator[AsyncSession]:
"""Used by FastAPI Depends"""
assert self._async_sessionmaker, "No sessionmaker. Run setup() first."
async with self._async_sessionmaker() as session:
yield session