TypeIs does what I thought TypeGuard would do in Python

· 4 min

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 narrowing. 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.

Patching pydantic settings in pytest

· 5 min

I’ve been a happy user of pydantic 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_settings.

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.

Eschewing black box API calls

· 6 min

I love dynamically typed languages as much as the next person. They let us make ergonomic API calls like this:

import httpx

# Sync call for simplicity
r = httpx.get("https://dummyjson.com/products/1").json()
print(r["id"], r["title"], r["description"])

or this:

fetch("https://dummyjson.com/products/1")
  .then((res) => res.json())
  .then((json) => console.log(json.id, json.type, json.description));

In both cases, running the snippets will return:

1 'iPhone 9' 'An apple mobile which is nothing like apple'

Unless you’ve worked with a statically typed language that enforces more constraints, it’s hard to appreciate how incredibly convenient it is to be able to call and use an API endpoint without having to deal with types or knowing anything about its payload structure. You can treat the API response as a black box and deal with everything in runtime.

Annotating args and kwargs in Python

· 3 min

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:

Statically enforcing frozen data classes in Python

· 3 min

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):

Debugging dockerized Python apps in VSCode

· 4 min

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 straightforward. 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.

Banish state-mutating methods from data classes

· 4 min

Data classes are containers for your data - not behavior. The delineation is right there in the name. Yet, I see state-mutating methods getting crammed into data classes and polluting their semantics all the time. While this text will primarily talk about data classes in Python, the message remains valid for any language that supports data classes and allows you to add state-mutating methods to them, e.g., Kotlin, Swift, etc. By state-mutating method, I mean methods that change attribute values during runtime. For instance:

Taming conditionals with bitmasks

· 9 min

The 100k context window of Claude 2 has been a huge boon for me since now I can paste a moderately complex problem to the chat window and ask questions about it. In that spirit, it recently refactored some pretty gnarly conditional logic for me in such an elegant manner that it absolutely blew me away. Now, I know how bitmasks work and am aware of the existence of enum.Flag in Python. However, it never crossed my mind that flags can be leveraged to trim conditional branches in such a clever manner that Claude illustrated. But once I looked at the proposed solution, the whole thing immediately clicked for me.

Memory leakage in Python descriptors

· 5 min

Unless I’m hand rolling my own ORM-like feature or validation logic, I rarely need to write custom descriptors in Python. The built-in descriptor magics like @classmethod, @property, @staticmethod, and vanilla instance methods usually get the job done. However, every time I need to dig my teeth into descriptors, I reach for this fantastic Descriptor how-to guide by Raymond Hettinger. You should definitely set aside the time to read it if you haven’t already. It has helped me immensely to deepen my understanding of how many of the fundamental language constructs are wired together underneath.

Unix-style pipelining with Python's subprocess module

· 6 min

Python offers a ton of ways like os.system or os.spawn* to create new processes and run arbitrary commands in your system. However, the documentation usually encourages you to use the subprocess module for creating and managing child processes. The subprocess module exposes a high-level run() function that provides a simple interface for running a subprocess and waiting for it to complete. It accepts the command to run as a list of strings, starts the subprocess, waits for it to finish, and then returns a CompletedProcess object with information about the result. For example: