Explicit method overriding with @typing.override

Although I’ve been using Python 3.12 in production for nearly a year, one neat feature in the typing module that escaped me was the @override decorator. Proposed in PEP-6981, it’s been hanging out in typing_extensions for a while. This is one of those small features you either don’t care about or get totally psyched over. I’m definitely in the latter camp. In languages like C#, Java, and Kotlin, explicit overriding is required. For instance, in Java, you use @Override to make it clear you’re overriding a method in a sub class. If you mess up the method name or if the method doesn’t exist in the superclass, the compiler throws an error. Now, with Python’s @override decorator, we get similar benefits—though only if you’re using a static type checker. ...

November 6, 2024

Quicker startup with module-level __getattr__

This morning, someone on Twitter pointed me to PEP 5621, which introduces __getattr__ and __dir__ at the module level. While __dir__ helps control which attributes are printed when calling dir(module), __getattr__ is the more interesting addition. The __getattr__ method in a module works similarly to how it does in a Python class. For example: class Cat: def __getattr__(self, name: str) -> str: if name == "voice": return "meow!!" raise AttributeError(f"Attribute {name} does not exist") # Try to access 'voice' on Cat cat = Cat() cat.voice # Prints "meow!!" # Raises AttributeError: Attribute something_else does not exist cat.something_else In this class, __getattr__ defines what happens when specific attributes are accessed, allowing you to manage how missing attributes behave. Since Python 3.7, you can also define __getattr__ at the module level to handle attribute access on the module itself. ...

November 3, 2024

Docker mount revisited

I always get tripped up by Docker’s different mount types and their syntax, whether I’m stringing together some CLI commands or writing a docker-compose file. Docker’s docs cover these, but for me, the confusion often comes from how “bind” is used in various contexts and how “volume” and “bind” sometimes get mixed up in the documentation. Here’s my attempt to disentangle some of my most-used mount commands. Volume mounts Volume mounts1 let you store data outside the container in a location managed by Docker. The data persists even after the container stops. On non-Linux systems, volume mounts are faster than bind mounts because data doesn’t need to cross the virtualization boundary. ...

October 22, 2024

Discovering direnv

I’m not really a fan of shims—code that automatically performs actions as a side effect or intercepts commands when you use the shell or when a prompt runs. That’s why, other than the occasional dabbling, I’ve mostly stayed away from tools like asdf or pyenv and instead stick to apt or brew for managing my binary installs, depending on the OS. Recently, though, I’ve started seeing many people I admire extolling direnv: ...

October 2, 2024

Bash namerefs for dynamic variable referencing

While going through a script at work today, I came across Bash’s nameref feature. It uses declare -n ref="$1" to set up a variable that allows you to reference another variable by name—kind of like pass-by-reference in C. I’m pretty sure I’ve seen it before, but I probably just skimmed over it. As I dug into the man page1, I realized there’s a gap in my understanding of how variable references actually work in Bash—probably because I never gave it proper attention and just got by cobbling together scripts. ...

September 20, 2024

Shades of testing HTTP requests in Python

Here’s a Python snippet that makes an HTTP POST request: # script.py import httpx from typing import Any async def make_request(url: str) -> dict[str, Any]: headers = {"Content-Type": "application/json"} async with httpx.AsyncClient(headers=headers) as client: response = await client.post( url, json={"key_1": "value_1", "key_2": "value_2"}, ) return response.json() The function make_request makes an async HTTP request with the httpx1 library. Running this with asyncio.run(make_request("https://httpbin.org/post")) gives us the following output: { "args": {}, "data": "{\"key_1\": \"value_1\", \"key_2\": \"value_2\"}", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Content-Length": "40", "Content-Type": "application/json", "Host": "httpbin.org", "User-Agent": "python-httpx/0.27.2", "X-Amzn-Trace-Id": "Root=1-66d5f7b0-2ed0ddc57241f0960f28bc91" }, "json": { "key_1": "value_1", "key_2": "value_2" }, "origin": "95.90.238.240", "url": "https://httpbin.org/post" } We’re only interested in the json field and want to assert in our test that making the HTTP call returns the expected values. ...

September 2, 2024

Taming parametrize with pytest.param

I love @pytest.mark.parametrize1—so much so that I sometimes shoehorn my tests to fit into it. But the default style of writing tests with parametrize can quickly turn into an unreadable mess as the test complexity grows. For example: import pytest from math import atan2 def polarify(x: float, y: float) -> tuple[float, float]: r = (x**2 + y**2) ** 0.5 theta = atan2(y, x) return r, theta @pytest.mark.parametrize( "x, y, expected", [ (0, 0, (0, 0)), (1, 0, (1, 0)), (0, 1, (1, 1.5707963267948966)), (1, 1, (2**0.5, 0.7853981633974483)), (-1, -1, (2**0.5, -2.356194490192345)), ], ) def test_polarify(x: float, y: float, expected: tuple[float, float]) -> None: # pytest.approx helps us ignore floating point discrepancies assert polarify(x, y) == pytest.approx(expected) The polarify function converts Cartesian coordinates to polar coordinates. We’re using @pytest.mark.parametrize in its standard form to test different conditions. ...

August 28, 2024

HTTP requests via /dev/tcp

I learned this neat Bash trick today where you can make a raw HTTP request using the /dev/tcp file descriptor without using tools like curl or wget. This came in handy while writing a health check script that needed to make a TCP request to a service. The following script opens a TCP connection and makes a simple GET request to example.com: #!/bin/bash # Open a TCP connection to example.com on port 80 and assign file descriptor 3 # The exec command keeps /dev/fd/3 open throughout the lifetime of the script # 3<> enables bidirectional read-write exec 3<>/dev/tcp/example.com/80 # Send the HTTP GET request to the server # >& redirects stdout to /dev/fd/3 echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n" >&3 # Read and print the server's response # <& redirects the output of /dev/fd/3 to cat cat <&3 # Close the file descriptor, terminating the TCP connection exec 3>&- Running this will print the response from the site to your console. ...

August 8, 2024

The *nix install command

TIL about the install command on *nix systems. A quick GitHub search for the term brought up a ton of matches1. I’m surprised I just found out about it now. Often, in shell scripts I need to: Create a directory hierarchy Copy a config or binary file to the new directory Set permissions on the file It usually looks like this: # Create the directory hierarchy. The -p flag creates the parent directories # if they don't exist mkdir -p ~/.config/app # Copy the current config to the newly created directory. Here, conf already # exists in the current folder cp conf ~/.config/app/conf # Set the file permission chmod 755 ~/.config/app/conf Turns out, the install command in GNU coreutils2 can do all that in one line: ...

July 28, 2024

TypeIs does what I thought TypeGuard would do in Python

The handful of times I’ve reached for typing.TypeGuard in Python, I’ve always been confused by its behavior and ended up ditching it with a # type: ignore comment. For the uninitiated, TypeGuard allows you to apply custom type narrowing1. For example, let’s say you have a function named pretty_print that accepts a few different types and prints them differently onto the console: from typing import assert_never def pretty_print(val: int | float | str) -> None: if isinstance(val, int): # assert_type(val, int) print(f"Integer: {val}") elif isinstance(val, float): # assert_type(val, float) print(f"Float: {val}") elif isinstance(val, str): # assert_type(val, str) print(f"String: {val}") else: assert_never(val) If you run it through mypy, in each branch, the type checker automatically narrows the type and knows exactly what the type of val is. You can test the narrowed type in each branch with the typing.assert_type function. ...

April 27, 2024

Strategy pattern in Go

These days, I don’t build hierarchical types through inheritance even when writing languages that support it. Type composition has replaced almost all of my use cases where I would’ve reached for inheritance before. I’ve written1 about how to escape the template pattern2 hellscape and replace that with strategy pattern3 in Python before. While by default, Go saves you from shooting yourself in the foot by disallowing inheritance, it wasn’t obvious to me how I could apply the strategy pattern to make things more composable and testable. ...

February 17, 2024

Retry function in Go

I used reach for reflection whenever I needed a Retry function in Go. It’s fun to write, but gets messy quite quickly. Here’s a rudimentary Retry function that does the following: It takes in another function that accepts arbitrary arguments. Then tries to execute the wrapped function. If the wrapped function returns an error after execution, Retry attempts to run the underlying function n times with some backoff. The following implementation leverages the reflect module to achieve the above goals. We’re intentionally avoiding complex retry logic for brevity: ...

February 4, 2024

Type assertion vs type switches in Go

Despite moonlighting as a gopher for a while, the syntax for type assertion and type switches still trips me up every time I need to go for one of them. So, to avoid digging through the docs or crafting stodgy LLM prompts multiple times, I decided to jot this down in a gobyexample1 style for the next run. Type assertion Type assertion in Go allows you to access an interface variable’s underlying concrete type. After a successful assertion, the variable of interface type is converted to the concrete type to which it’s asserted. ...

January 31, 2024

Patching pydantic settings in pytest

I’ve been a happy user of pydantic1 settings to manage all my app configurations since the 1.0 era. When pydantic 2.0 was released, the settings portion became a separate package called pydantic_settings2. It does two things that I love: it automatically reads the environment variables from the .env file and allows you to declaratively convert the string values to their desired types like integers, booleans, etc. Plus, it lets you override the variables defined in .env by exporting them in your shell. ...

January 27, 2024

Omitting dev dependencies in Go binaries

As of now, unlike Python or NodeJS, Go doesn’t allow you to specify your development dependencies separately from those of the application. However, I like to specify the dev dependencies explicitly for better reproducibility. While working on a new CLI tool1 for checking dead URLs in Markdown files, I came across this neat convention: you can specify dev dependencies in a tools.go file and then exclude them while building the binary using a build tag. ...

January 21, 2024

Annotating args and kwargs in Python

While I tend to avoid *args and **kwargs in my function signatures, it’s not always possible to do so without hurting API ergonomics. Especially when you need to write functions that call other helper functions with the same signature. Typing *args and **kwargs has always been a pain since you couldn’t annotate them precisely before. For example, if all the positional and keyword arguments of a function had the same type, you could do this: ...

January 8, 2024

Statically enforcing frozen data classes in Python

You can use @dataclass(frozen=True) to make instances of a data class immutable during runtime. However, there’s a small caveat—instantiating a frozen data class is slightly slower than a non-frozen one. This is because, when you enable frozen=True, Python has to generate __setattr__ and __delattr__ methods during class definition time and invoke them for each instantiation. Below is a quick benchmark comparing the instantiation times of a mutable dataclass and a frozen one (in Python 3.12): ...

January 4, 2024

Debugging dockerized Python apps in VSCode

Despite using VSCode as my primary editor, I never really bothered to set up the native debugger to step through application code running inside Docker containers. Configuring the debugger to work with individual files, libraries, or natively running servers is trivial1. So, I use it in those cases and just resort back to my terminal for debugging containerized apps running locally. However, after seeing a colleague’s workflow in a pair-programming session, I wanted to configure the debugger to cover this scenario too. ...

December 22, 2023

Dotfile stewardship for the indolent

I’m one of those people who will sit in front of a computer for hours, fiddling with algorithms or debugging performance issues, yet won’t spend 10 minutes to improve their workflows. While I usually get away with this, every now and then, my inertia slithers back to bite me. The latest episode was me realizing how tedious it is to move config files across multiple devices when I was configuring a new MacBook Air and Mac Mini at the same time. ...

September 27, 2023

Self-hosted Google Fonts in Hugo

This site1 is built with Hugo2 and served via GitHub pages3. Recently, I decided to change the font here to make things more consistent across different devices. However, I didn’t want to go with Google Fonts for a few reasons: CDN is another dependency. Hosting static assets on GitHub Pages has served me well. Google Fonts tracks users and violates4 GDPR in Germany. Google Analytics does that too. But since I’m using the latter anyway, this might come off a bit apocryphal. I wanted to get a few extra Lighthouse points. Turns out, it’s pretty easy to host the fonts yourself. ...

September 14, 2023