Skip to Content
Course content

307: Union and Optional Types in Depth

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

Wait, isn't Optional just a shortcut for Union with None?

Short answer: Yes. Exactly. If you look under the hood of the typing module, Optional[int] is literally just an alias for Union[int, None]. There is no functional difference in how Python handles them at runtime.

But here is why we still distinguish them in conversation and code: intent. When I see Optional, my brain immediately flags this as "this value might be missing." When I see Union, I think "this value could be one of several different types of data."

from typing import Optional, Union

# This says: "The user might have a middle name, or they might not."
def greet_user(middle_name: Optional[str] = None):
    ...

# This says: "The ID must be provided, but it can be a database integer or a UUID string."
def get_record(record_id: Union[int, str]):
    ...

I've seen developers use Union[str, None] everywhere, and while it's not "wrong," it's noisier. Use Optional when the primary point is the possibility of absence.

Should I use the pipe operator (|) or the Union keyword?

If you're on Python 3.10 or newer, use the pipe operator. Period. It's cleaner, it's more concise, and it's becoming the industry standard. It effectively replaces the need to import Union from the typing module for most basic cases.

Compare these two. They do the exact same thing:

# The old way (Pre-3.10)
from typing import Union
def process_payment(amount: Union[int, float]):
    print(f"Processing {amount}")

# The modern way (3.10+)
def process_payment(amount: int | float):
    print(f"Processing {amount}")

The only reason to stick with Union is if you're maintaining a codebase that needs to support older versions of Python. If you're starting a fresh project today, the pipe operator is the way to go. It feels much more like the logical "OR" that it represents.

How do I actually use these variables without the type checker screaming at me?

This is where most people get stuck. If you declare a variable as int | str, you can't just call .upper() on it because integers don't have an upper() method. Your IDE or Mypy will highlight that line in red and tell you the method doesn't exist on int.

To fix this, you need type narrowing. You have to prove to the type checker that the variable is the type you think it is before you use it. I usually do this with a simple isinstance() check.

def format_identifier(uid: int | str) -> str:
    # At this point, uid could be either. 
    # I can't do uid.upper() yet.
    
    if isinstance(uid, str):
        # Inside this block, the type checker knows uid is definitely a str.
        return uid.upper()
    
    # If we reached here, the type checker knows uid must be an int.
    return f"ID_{uid}"

This is a pattern you'll use constantly. By using a conditional check, you "narrow" the type from a broad Union down to a specific type. It makes your code safer because you're explicitly handling the different possibilities instead of just hoping for the best and hitting an AttributeError at 3 AM in production.




📋 Practical Task

Refactoring the API Response Handler for Polymorphic IDs

You are working on a legacy module that handles API responses. The fetch_user_data function returns a user ID that could be an int (for legacy users) or a str (for new OAuth users). However, the current implementation is causing type-checking errors and occasional runtime crashes because it assumes the ID is always a string.

Your Task:

  • Refactor the process_user_id function to use the modern Python 3.10+ pipe operator (|) for the user_id parameter.
  • Implement type narrowing using isinstance() so that:
    • If the ID is a str, it is stripped of whitespace and converted to lowercase.
    • If the ID is an int, it is converted to a string prefixed with "legacy_".
  • Ensure the function always returns a str.
# STARTING CODE
def process_user_id(user_id):
    # This currently crashes if user_id is an int
    return user_id.strip().lower()

# Test cases to verify your fix
print(process_user_id("  User_123  ")) # Expected: "user_123"
print(process_user_id(456))             # Expected: "legacy_456"
Rating
0 0

There are no comments for now.

to be the first to leave a comment.