Skip to Content
Course content

302: Type Checking with pyright

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

Imagine you're building a piece of high-end furniture from a kit. You have a set of blueprints that explicitly tell you, "This bolt is an M6 size; this hole is for an M6 bolt." Now, imagine if you had a magical assistant who watched you work in real-time. Before you even pick up the screwdriver, the assistant taps you on the shoulder and says, "Hey, you're about to try and force an M8 bolt into an M6 hole. It's not going to fit, and you're going to strip the thread."

That assistant is essentially what pyright does for your Python code. Without it, you're just "building the furniture" (running your code) and only discovering the bolt doesn't fit when you hear a loud crack (a TypeError at 3:00 AM in production). Pyright doesn't actually run your code; it just reads your blueprints—your type hints—and tells you where the logic doesn't line up.

Catching the 'NoneType' Nightmare

I've spent more hours than I care to admit debugging AttributeError: 'NoneType' object has no attribute 'x'. It usually happens because some function returned None when I expected a real object, and I forgot to check for it. This is where pyright becomes your best friend.

Take a look at this snippet. I'm writing a simple function to calculate a member's discount:

def get_discounted_price(price: float, discount_code: str | None) -> float:
    if discount_code == "SUMMER24":
        return price * 0.8
    return price

On the surface, this looks fine. But what happens if I change the logic to call a method on discount_code? If I write discount_code.upper() without checking if it's None first, Python won't complain while I'm typing. It'll only crash when the code actually hits that line with a None value. Pyright, however, will put a red squiggly line under discount_code.upper() immediately, warning you that the object might be None.

Teaching Pyright how to read your mind

Pyright relies on "Static Analysis." It doesn't care about the actual value of your variables at runtime; it cares about the contract you've signed using type hints. When you tell pyright a variable is a float | None, you're signing a contract that says, "I acknowledge this might be empty."

The magic happens through something called "Type Narrowing." When you write an if discount_code is not None: block, pyright is smart enough to realize that inside that specific block, the variable is no longer Optional—it's definitely a str. I love this because it forces me to handle the "edge cases" (the None values) explicitly before I'm allowed to use the data.

Getting that red squiggly line to work for you

You don't need to run pyright as a separate command-line tool every five seconds—though you can. Most of us use it via the Pylance extension in VS Code. Once it's active, you stop treating your editor as a fancy notepad and start treating it as a compiler.

I'll give you a tip: don't get overwhelmed by the wall of red lines when you first enable "strict" mode on an old project. Start with "basic" mode. The goal isn't to satisfy a pedantic tool; the goal is to eliminate entire categories of bugs before they ever reach your terminal. If pyright is complaining, it's usually not because you're "wrong," but because you've left a door open for a crash to happen.




📋 Practical Task

Fixing the User Profile Update Validator

You've inherited a piece of code that handles user profile updates. The current implementation is "blind"—it doesn't use type checking, and it's causing intermittent crashes because sometimes the email or phone fields are missing from the update request.

Your task is to add type hints to the following function and use pyright (or your IDE's type checker) to identify and fix the potential NoneType crashes. You must ensure that the code is "type-safe" so that pyright shows no errors.

def update_user_profile(user_id: int, updates: dict):
    # Imagine 'database.get_user' returns a User object or None
    user = database.get_user(user_id) 
    
    # BUG: What if user is None?
    # BUG: What if 'email' key is missing or its value is None?
    email = updates.get("email")
    user.email = email.lower() 
    
    database.save(user)
    return True

Requirements:

  • Define a User class or TypedDict to represent the user object.
  • Use Optional or | None for the database.get_user return type.
  • Implement "Type Narrowing" (if-statements) to ensure user and email are not None before accessing their attributes or methods.
  • Ensure the function signature clearly defines the expected types for updates.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.