PythonJuniorTechnical

Чем Optional[X] отличается от X | None?

Семантически эквивалентны: typing.Optional[X] — alias для Union[X, None]; X | None — синтаксис PEP 604, Python 3.10+. Оба означают «значение типа X или None». Это про nullable type, НЕ про «параметр необязательный» — обязательность задаёт наличие default value.

Что означают

  • typing.Optional[X] — alias для typing.Union[X, None]. Введён в Python 3.5 с модулем typing.
  • X | None — синтаксис union types (PEP 604), доступен в аннотациях с Python 3.10. Использует types.UnionType, isinstance(v, int | str) работает в runtime.
  • Семантически эквивалентны: mypy, pyright, pyre трактуют их одинаково.

Это про тип, не про обязательность

Распространённая путаница: Optional[X] ≠ «параметр необязательный». Обязательность аргумента определяется только наличием default value в сигнатуре.

# обязательный, может быть None
def f1(name: str | None) -> None: ...
f1()           # TypeError: missing required argument
f1(None)       # OK

# необязательный, по умолчанию None
def f2(name: str | None = None) -> None: ...
f2()           # OK

# необязательный, default не None
def f3(name: str = "anon") -> None: ...
f3()           # OK

Когда что использовать

  • Python 3.10+ — пишите X | None; короче и не требует импорта.
  • Поддержка 3.9 и старше — Optional[X] или from __future__ import annotations (annotations станут строками, можно использовать новый синтаксис, но runtime-генерик от 3.9 не получите).
  • В сложных union: str | int | None читается лучше, чем Optional[Union[str, int]].

Narrowing после проверки

def greet(name: str | None) -> str:
    if name is None:
        return "hi, anon"
    # mypy/pyright теперь знают: name: str (None отброшен)
    return name.upper()


def length(x: str | int | None) -> int:
    if x is None:
        return 0
    if isinstance(x, str):
        return len(x)  # x: str
    return x           # x: int


# walrus + narrowing
def get_user(d: dict[str, str | None]) -> str:
    if (name := d.get("name")) is not None:
        return name.upper()  # name: str
    return "anon"

Runtime-нюансы

  • В 3.10+ isinstance(x, int | str) работает (PEP 604).
  • В 3.9 и ниже нужен isinstance(x, (int, str)).
  • Optional[X] в runtime = Union[X, None]; typing.get_args(Optional[int]) == (int, NoneType).
  • Pydantic v2 различает Optional[X] как nullable и default; для X | None = None поле опционально и принимает null.

Подводные камни

  • Думать, что Optional[X] делает аргумент опциональным — нет, нужен default.
  • Смешивать стили Optional[X] и X | None внутри одного проекта — выбирайте один.
  • Использовать X | None в коде, который должен запускаться на 3.9 без from __future__ import annotations — SyntaxError на этапе импорта.
  • Не сужать тип после if x is None — линтер ругается на доступ к атрибутам Optional.
  • Pydantic v1 vs v2: разное поведение для Optional полей без default — лучше всегда указывать default явно.
  • В словарях dict.get("k") возвращает X | None; типизатор требует narrowing перед использованием.

Common mistakes

  • Говорить, что Optional делает параметр необязательным.
  • Не знать Union[X, None].
  • Игнорировать target Python version.

What the interviewer is testing

  • Объясняет semantic equivalence.
  • Различает None-ability и default.
  • Знает современный синтаксис.

Sources

Related topics