Optional and Union Types

RoadmapsPython

Overview

In Python type hinting, a `Union` indicates that a variable can be one of several distinct types. Since Python 3.10, this is written concisely using the `|` operator. def process(data: int | str): pass `Optional[T]` is just a shorthand for `Union[T, None]` (or `T | None`). It represents a value that might be of type `T` or might be `None` (missing). def get_user(user_id: int) -> str | None: pass If a box has a union type, it's like sticking two labels on it: 'Only ints OR strings go here'. `Optional` specifically warns: 'This box might be empty (`None`), so check before you use it'. When combining unions with collections, `list[int | str]` is completely different from `list[int] | list[str]`. The latter means the entire list must be strictly integers or strictly strings, which bites developers expecting a mixed list. # Trap: Expecting a mixed list but typing it as two strict lists def process_items(items: list[int] | list[str]): items.append('hello') # Type error: items might be list[int] This interaction is covered fully in generic-types.

It forces you to explicitly handle cases where data might be missing (`None`) or take multiple valid forms, preventing runtime errors like `AttributeError: 'NoneType' object has no attribute '...'`.

Where used: `FastAPI` optional query parameters, Database queries that might return no rows, `Pydantic` model fields without defaults

Why learn this

Code walkthrough

def format_status(code: int | str | None) -> str:
  if code is None:
    return 'Unknown'
  if isinstance(code, int):
    return f'Code {code}'
  return code.upper()

print(format_status(404))

Focus: if isinstance(code, int):

Aha moment

from typing import Optional, Union

print(Optional[int] == Union[int, None])

Prediction: What does this print?

Common guess: `False`, they are different things.

It prints `True`. `Optional` is not a special type behavior; it is literally just an alias for a `Union` that includes `None`.

Common mistakes

Glossary

type hinting
Adding labels to your code to indicate what kind of data a variable should hold, helping catch errors early. Example: `name: str`.
Static type checkers
Tools that analyze your code without running it to find potential errors related to data types. Example: `mypy myfile.py`.

Recall questions

Understanding checks

What does this code print?

It prints `0`. The function guards against `None` by returning `0` early, so `len(None)` is never called and no error occurs.

The function correctly handles the case where `text` is `None`, returning `0` instead of crashing when attempting to call `len(None)`.

What is wrong with this function according to static analysis tools?

It calls `.upper()` on `user_id` without checking its type first. If `user_id` is an `int`, `.upper()` will cause an `AttributeError`.

When a variable has a `Union` type, you can only safely perform operations that are valid for ALL types in the union. You must check the type (`isinstance`) to narrow it before using type-specific methods.

Practice tasks

Handle optional data

Given this working code, modify `get_display_name` so it accepts a `str` OR `None`. If `name` is `None`, it should return the string `'Guest'`. Ensure the return type hint is always a `str`.

Challenge

Type a parsing function

Write a function `parse_id` that takes an argument that is either an `int` or a `str`. If it's a `str`, try to convert it to an integer using `int()`. If the conversion fails (`ValueError`), return `None`. If it's already an `int`, return it as-is. Provide complete type hints.

Continue learning

Previous: Basic Annotations

Next: Generic Types

Return to Python Roadmap