Skip to Content
Course content

312: NewType for Semantic Type Aliases

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring pattern when I review code from developers moving into intermediate Python: they use type aliases to make their code more readable, but they mistakenly believe those aliases provide actual type safety. They think they've created a "UserId" type, but in reality, they've just given the integer type a nickname.

Simple Aliases Don't Actually Prevent Type Mismatches

You might have tried something like this to make your function signatures clearer:

UserId = int
OrderId = int

def fetch_user_settings(user_id: UserId):
    print(f"Fetching settings for user {user_id}")

# This looks fine, but look what happens...
my_order_id = 5005
fetch_user_settings(my_order_id)  # No type checker error!

At first glance, this looks clean. But here is the problem: UserId = int is just a variable assignment. To Python—and to your static type checker like Mypy or Pyright—UserId and OrderId are exactly the same thing: an int. If you accidentally pass an order ID into a function expecting a user ID, the type checker will stay silent, and you'll end up with a bug that is a nightmare to track down in production.

Using NewType to Enforce Semantic Boundaries

If you want the type checker to actually treat these as different types, you need NewType from the typing module. NewType creates a subclass of the base type that the type checker treats as a distinct entity, even though it's still the base type at runtime.

Here is how we fix the example above:

from typing import NewType

UserId = NewType('UserId', int)
OrderId = NewType('OrderId', int)

def fetch_user_settings(user_id: UserId):
    print(f"Fetching settings for user {user_id}")

# To create a value of this type, you "call" the NewType
my_user_id = UserId(123)
my_order_id = OrderId(5005)

fetch_user_settings(my_user_id)   # This is perfect.
fetch_user_settings(my_order_id)  # TYPE ERROR: Argument 1 has incompatible type "OrderId"; expected "UserId"

Notice that I'm calling UserId(123). This isn't actually converting the data or wrapping it in a class at runtime; in the compiled bytecode, it's essentially a no-op. I love this because you get the rigorous safety of a strong type system during development without paying any performance penalty when the code actually runs.

Handling the Transition from Raw Data

One thing to keep in mind: because NewType is a "helper" for the type checker, you have to be explicit when you first receive data (like from a JSON API or a database) that is just a raw int.

You can't just assume a variable is a UserId. You have to cast it: raw_id = int(data['id']); user_id = UserId(raw_id). This forces you to define exactly where the "semantic" meaning of the data begins. It's a small bit of extra typing for you, but it saves your future self from the "why is this Order ID being treated as a User ID?" debugging session at 2 AM.




📋 Practical Task

Exercise: Preventing Latitude and Longitude Swap in a Geolocation Helper

In geolocation systems, swapping Latitude and Longitude is a classic, catastrophic bug. Both are floats, but they are not interchangeable.

Your task is to refactor the following code. Currently, it uses simple aliases, which means a developer could accidentally swap the coordinates and the type checker wouldn't notice. Use NewType to ensure that a Longitude value cannot be passed into a parameter expecting a Latitude, and vice versa.

from typing import NewType

# TODO: Change these from simple aliases to NewTypes
Latitude = float
Longitude = float

def calculate_distance(lat: Latitude, lon: Longitude):
    print(f"Calculating distance for {lat}, {lon}")
    return 0.0

# This should trigger a type checker error after your changes
# because the arguments are swapped.
my_lat = Latitude(34.0522)
my_lon = Longitude(-118.2437)

calculate_distance(my_lon, my_lat) 

Requirements:

  • Redefine Latitude and Longitude using NewType.
  • Ensure that the call to calculate_distance(my_lon, my_lat) would be flagged as an error by a static type checker.
  • Keep the runtime behavior the same (the values should still be floats).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.