Many real-world values can be absent — a database row that doesn’t exist, a config field that wasn’t set, a function that finds nothing to return. Python uses None to mean “no value”.

Modern Python expresses this with X | None — read as “X or None”.

The X | None pattern

def find_user(user_id: int) -> dict[str, str] | None:
    if user_id == 1:
        return {"name": "Manikandan"}
    return None


result = find_user(99)
print(result)   # None

The return type says: this function returns either a user dict or None.

You may see older code with Optional[X]:

from typing import Optional


def find_user(user_id: int) -> Optional[dict[str, str]]:
    ...

Optional[X] is exactly the same as X | None. The | form is the modern standard — use it.

Type narrowing

When you check for None, pyright narrows the type of the variable inside the branch:

result = find_user(1)

# here, result could be a dict OR None
print(result["name"])   # ERROR — could be None

if result is not None:
    # here, pyright knows result is a dict (None is ruled out)
    print(result["name"])   # OK

This is the standard pattern: guard with if x is not None:, then use x freely inside the block.

You can also flip it:

if result is None:
    return        # or raise, or default
print(result["name"])   # OK — past this line, result can't be None

This early return style is preferred when the “missing” case doesn’t need to do much.

Walrus + Optional — a common combo

We mentioned the walrus := in Section 3. It shines when checking for None:

if (user := find_user(1)) is not None:
    print(user["name"])

In one line: call the function, name the result, check it, use it inside the block.

Type-narrowing with assertions

Sometimes you know a value isn’t None but pyright can’t prove it. Use assert:

result = find_user(1)
assert result is not None
print(result["name"])     # pyright now treats result as dict[str, str]

The assert acts as a runtime check and a type-narrowing hint. Use it sparingly — usually a proper check is clearer.

Default with or

You can use Python’s or operator to default None to something else:

name: str | None = get_name()
display: str = name or "Anonymous"

But beware: or treats any falsy value as missing — including "" and 0. If you only want to substitute when the value is None, use a conditional:

display: str = name if name is not None else "Anonymous"

Pyright generally prefers the conditional form because the types are more precise.

get() with defaults — dictionaries

The most common source of None in beginner Python is dictionary lookups:

config: dict[str, str] = {"host": "localhost"}

# may raise KeyError
host = config["host"]

# returns None if missing
host: str | None = config.get("host")

# returns the default if missing
host: str = config.get("host", "0.0.0.0")

config.get(key, default) is the cleanest way to handle “maybe-present” lookups.

Multiple-Optional juggling

In real code you’ll often combine several Optional values:

def find_email(user_id: int) -> str | None: ...
def normalize(email: str) -> str: ...


def main() -> None:
    email = find_email(42)
    if email is not None:
        clean = normalize(email)
        print(clean)

If you find yourself writing many nested if x is not None: blocks, consider raising an exception at the top instead — single failure point, simpler downstream code:

def main() -> None:
    email = find_email(42)
    if email is None:
        raise ValueError("Email not found")
    print(normalize(email))

Avoid Optional for collections — usually

A common smell: list[T] | None. Most of the time you can use an empty list instead of None:

# OK but awkward
def tags(post: Post) -> list[str] | None:
    if post.has_tags:
        return [...]
    return None

# better
def tags(post: Post) -> list[str]:
    if post.has_tags:
        return [...]
    return []

An empty list is its own “no tags” signal — no need to deal with None everywhere downstream.

The exception is when the absence has a different meaning from the empty value — e.g. “user not found” vs “user has zero followers”.

What’s next

Optional handles “value or no value”. Next — Protocols, structural typing that lets you describe shapes without inheritance.

Toggle theme (T)