Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
312: NewType for Semantic Type Aliases
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
LatitudeandLongitudeusingNewType. - 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).
There are no comments for now.