skip to content

Optional, Unions, and None

Saying a value may be one of several types, or may be missing. The classic trap is assuming a `= None` default makes a parameter Optional on its own, which modern checkers refuse.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does `Optional[str]` mean in a Python type annotation?

level: juniorimportance: must knowfreq 72%

answer

  1. Read it as a value, not a call rule
  2. Nothing to do with omitting arguments
  3. Count the admissible runtime types
  4. The second one is a singleton object

basics

~20 s

Optional[str] from the typing module means the value is either a str or None - exactly the union str | None. It says nothing about omitting an argument: such a parameter is still required unless it also has a default.

solid answer

~40 s

`Optional[X]` is shorthand for the two-member union `X | None`, and nothing else. The name is a historical wart: it was chosen for the *value* being possibly absent, not for the *parameter* being possible to leave out. In `def rebuild(shard: str, encoding: Optional[str]) -> None:` both arguments are still required at every call site; what makes an argument omittable is a default value, which is a separate decision from the annotation. Since Python 3.10 the spelling `str | None` says the same thing with no import, and modern code prefers it precisely because it cannot be misread. A checker then treats the value as possibly `None`, so `encoding.lower()` is an error until the `None` case has been ruled out.

code

python · 12 lines
python
from typing import Optional


def greet(name: Optional[str], punct: str = "!") -> str:
    return f"hello, {name or 'stranger'}{punct}"


print(greet(None), "|", greet("ada"))
try:
    greet()
except TypeError as exc:
    print("still required:", exc)

go deeper

for a junior

Be ready to say in one sentence that Optional[X] means X or None. Interviewers use it as a quick check that you read an annotation as a statement about values, not as a rule about how the function is called.

for a middle

Explain that Optional[X] is precisely the union X | None, that a default value rather than the annotation is what makes an argument omittable, and that Optional accepts exactly one type argument.

for a senior

Show the review habit: a signature that is Optional everywhere pushes branching onto every caller, so be ready to argue when None genuinely belongs to the domain and when it is a leaked failure that should raise instead.

for a principal

Own the convention across the codebase: which spelling is house style, how legacy Optional[...] migrates to the union form, and where None-as-missing should be replaced by a richer result before it spreads across team boundaries.

### What the annotation actually says `Optional[str]`, imported from the `typing` module, describes a value with exactly two admissible types: a `str` object, or `None`. That is the whole of its meaning. It is defined as sugar for a two-member union — the union of `str` with the type of `None` — and since Python 3.10 you can write the identical thing as `str | None` with no import at all. ### Where the confusion comes from In English "optional" means "you may leave it out". In typing it means "the value may be absent, and absence is spelled `None`". Those are different claims, and on a Python signature they live on different parts of the same parameter: - the **annotation** says what values the name may hold; - the **default** says whether the caller may omit the argument; - the `*` and `/` markers say how the argument may be passed. `Optional[X]` touches only the first. Given `def rebuild(shard: str, encoding: Optional[str]) -> None:`, the call `rebuild("docs")` still raises `TypeError: rebuild() missing 1 required positional argument: 'encoding'`, exactly as it would with no annotation at all. What makes an argument omittable is a default value, and that is a separate decision you make deliberately. The name is a historical wart that predates the operator spelling; it is one reason most modern code writes `str | None`, which cannot be misread. ### Optional takes exactly one type argument `Optional[int, str]` is not a two-member union — it raises `TypeError: typing.Optional requires a single type`. A union of several types plus `None` is `Union[int, str, None]`, or `int | str | None`. Likewise `Optional[Optional[str]]` is not "doubly optional": unions flatten and deduplicate, so it is simply `str | None`. ### Placement inside a container changes the meaning This is the distinction reviewers actually catch: - `Optional[list[str]]` — the value is either `None` or a list, and if it is a list every element is a `str`. - `list[Optional[str]]` — the value is always a list, but individual elements may be `None`. - `Optional[list[Optional[str]]]` — both. Choosing the wrong one is a real bug: code that has proved the value is not `None` then iterates and calls a string method on an element that is `None`. ### Where you meet it Lookups that may miss (`dict.get` returns `V | None`), reads of process configuration such as `os.environ.get`, attributes set to `None` in `__init__` and filled in later, and any function whose result is "the thing, or nothing". In each case the annotation is doing useful work: it makes the possibility of `None` visible in the signature instead of leaving it to be discovered as an `AttributeError` in production. ### What it costs the caller A value typed `str | None` cannot be used as a `str` until the `None` case has been ruled out, so every use site grows a branch. That is the point of the annotation, but it is also why an API whose every parameter and return is `Optional` is a smell: the author has pushed a decision onto every caller instead of making it once. Ask whether `None` genuinely belongs to the domain (a cache miss, an unset configuration value) or whether it is a leaked failure that should be an exception, or a real empty value such as `""` or `[]`. ### It is a claim, not a guard Annotating a parameter `Optional[str]` does not make the interpreter reject an `int`; nothing at call time consults the annotation. The annotation is a claim checked by a static tool and read by humans. Its value is precisely that it is machine-checkable — but only if it is true, which is why the sibling trap of writing `str` next to a `None` default matters so much. ### How to say the things Optional does not say - "The caller may omit this argument": give the parameter a default — `encoding: str = "utf-8"` when there is a sensible one, `encoding: str | None = None` when there is not. - "This key may be missing from the mapping": in a `TypedDict`, that is `typing.NotRequired`, which is about key presence rather than value type — a genuinely different statement from `X | None`. - "This value is unknown to the checker": that is `Any`, and it is a different tool with different consequences. ### The one-line summary `Optional[X]` means `X | None` and nothing else. If you find yourself explaining it as "the argument is optional", re-read the signature: the default is what makes an argument optional, and it is written after the `=`.

  • If `Optional[str]` does not mean "may be omitted", how do you spell a parameter the caller may leave out?
    You give it a default. When a sensible real default exists, use it and keep the annotation narrow: `encoding: str = "utf-8"`. When the only meaningful stand-in is nothing at all, write both halves explicitly: `encoding: str | None = None`. The default controls omission, the annotation controls the value type, and a signature states the two independently.
  • What is the difference between `Optional[list[str]]` and `list[Optional[str]]`?
    `Optional[list[str]]` means the value is either `None` or a list, and when it is a list every element is a `str`. `list[Optional[str]]` means the value is always a list, but individual elements may be `None`. Confusing them is a real bug: code that has ruled out a `None` list then iterates and calls a string method on a `None` element.
  • Why does `Optional[int, str]` fail?
    `Optional` takes exactly one type argument and raises `TypeError: typing.Optional requires a single type`. It is sugar for a union with `None`, not a general union constructor. A union of several types plus `None` is written `Union[int, str, None]`, or `int | str | None` with the operator spelling.

Optional[str] is like a form field where "none" is one of the permitted answers - the field itself is still mandatory, you just have something valid to write in it.

saying these in an interview costs you the question

  • Says Optional means the argument can be left out
  • Claims Optional[str] gives the parameter a default of None
  • Treats None and the empty string as the same case
  • Believes Optional[str] stops None reaching the body at runtime
  • Writes Optional[int, str] expecting a two-type union

context

open as a page

Why do type checkers reject `def load(path: str = None)` in Python?

level: middleimportance: must knowfreq 60%

basics

~20 s

Because the annotation says path is always a str while the default hands it None. Under the no-implicit-Optional rule a None default no longer widens the declared type, so you must write str | None = None yourself.

open as a page

How does Python's `int | str` union spelling differ from `typing.Union[int, str]`?

level: middleimportance: should knowfreq 48%

basics

~20 s

They denote the same type. The operator form, added by PEP 604 in Python 3.10, needs no import, reads better nested, and works in isinstance; typing.Union is the older spelling, still common in pre-3.10 code.

open as a page

Why does adding `| None` to a function's return annotation break callers when adding it to a parameter does not?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Widening a parameter accepts everything callers already passed, so it stays compatible. Widening a return hands callers a value they were never told to handle: every use of the result must now rule None out before touching it.

open as a page