Python 3.15 получил встроенный sentinel вместо хаков с None

None, вероятно, самое перегруженное значение в Python. Его используют как универсальный инструмент: для обозначения «нет значения», «не задано», «произошла ошибка» или как обычное значение. Автор заметки представлял доклад о sentinel-значениях (сигнальных значениях-маркерах) на PyConDE/PyData Berlin 2026, EuroPython 2026 и, совсем недавно, на Swiss Python Summit 2026. В тексте он пересказывает, почему эта неоднозначность мешает и чем её заменять.

Проблема встречается повсюду. Простой кэш возвращает None и для отсутствующего ключа, и для ключа, у которого значение None. Параметр timeout=None при обновлении конфигурации может значить «без таймаута» или «оставить текущее значение». А функция, перехватившая TimeoutError и вернувшая None, использует его как сигнал об ошибке. Sentinel решает это тем, что служит маркером вне области допустимых значений, поэтому никогда не совпадёт с настоящим значением.

Сам Python активно использует sentinel-значения на виду: Ellipsis пропускает измерения в срезах NumPy и отмечает заглушки, а NotImplemented означает «ищи реализацию в другом месте» и обеспечивает двойную диспетчеризацию (double dispatch) для бинарных операторов через методы вроде rsub. MISSING в Pydantic отличает поле, явно заданное как null, от поля, которое вообще не присылали; в Pydantic 2.14 он вышел из экспериментальной стадии.

Сложности начинаются, когда делаешь собственные sentinel-значения. Классический MISSING = object() отлично работает во время выполнения, но ломает сужение типов (type narrowing) в статических анализаторах типов. Обходные пути, защитные функции TypeIs, «магические» строки Literal, трюк с Enum из одного члена, имеют свои недостатки, а проверки по тождеству ломаются, когда sentinel копируют или сериализуют через pickle.

Затем случился неожиданный поворот. На PyConDE в апреле автор был вынужден сказать, что PEP 661, предложение стандартизировать sentinel-значения, отложен. Всего через девять дней Руководящий совет (Steering Council) его принял. Начиная с Python 3.15 sentinel, встроенная функция языка, и анализаторы типов могут сужать его так же, как None. В примере из заметки MISSING = sentinel("MISSING") служит значением по умолчанию для параметра email типа str | None | MISSING: если email is MISSING, ничего не обновляется; если None, адрес удаляется из базы; иначе сохраняется строка в нижнем регистре, и тип уже сужен до str.

Для старых версий Python есть обратный порт в typing_extensions. Поддержка в анализаторах типов пока догоняет, поэтому автор советует проверить свой анализатор, прежде чем полагаться на встроенный sentinel.

Главный вывод автора: помнить о неоднозначности None и думать, что именно вы хотите выразить; использовать sentinel для устранения неоднозначности, предпочитать те, что уже даёт используемая библиотека, и заменять старые хаки с object() и Enum новым встроенным. Слайды с PyConDE по-прежнему описывают PEP 661 как отложенный; самая актуальная версия доклада, запись с EuroPython, где разобран новый встроенный sentinel.

Ключевые факты

  • None в Python перегружен: он означает и отсутствие ключа, и «не задано», и ошибку, и обычное значение; sentinel решает это маркером вне области допустимых значений.
  • Классический MISSING = object() работает во время выполнения, но ломает сужение типов в статических анализаторах; обходные пути (TypeIs, Literal, Enum из одного члена) имеют недостатки, а проверки по тождеству ломаются при копировании и pickle.
  • PEP 661 автор на PyConDE в апреле назвал отложенным, а всего через девять дней Руководящий совет (Steering Council) его принял.
  • С Python 3.15 sentinel, встроенная функция языка; анализаторы типов могут сужать его как None, а для старых версий есть обратный порт в typing_extensions.
  • MISSING в Pydantic отличает явно заданный null от непереданного поля и вышел из экспериментальной стадии в Pydantic 2.14.

Почему это важно

Неоднозначность None, повседневная проблема: кэши, конфигурации и обработка ошибок регулярно путают «значения нет» и «значение, None». Принятие PEP 661 и появление встроенного sentinel с Python 3.15 дают стандартный способ решить это, вместо того чтобы каждый проект изобретал свой MISSING = object() или трюк с Enum. Важно и то, что встроенный вариант, по словам автора, поддаётся сужению типов так же, как None.

Кому это важно

Разработчикам на Python, которые пишут API с необязательными параметрами, кэши, обновление конфигураций и модели данных, где нужно отличать «не передано» от «передано как null». Авторам библиотек и пользователям Pydantic, а также тем, кто следит за поддержкой новых возможностей языка в анализаторах типов.

Как это применить

Автор советует осознавать неоднозначность None и решать, что именно вы хотите выразить; использовать sentinel там, где None конфликтует с настоящим значением; предпочитать sentinel, уже предоставленные вашей библиотекой; а старые хаки с object() и Enum заменять новым встроенным. На Python 3.15 и новее достаточно вызова sentinel с именем, например sentinel("MISSING"). На более старых версиях можно взять обратный порт из typing_extensions.

Можно ли доверять

Это авторская заметка к собственному докладу, а не официальное сообщение: имя автора в тексте не указано. Утверждения о принятии PEP 661 и о появлении встроенного sentinel в Python 3.15 приведены со слов автора; точная дата принятия и дата выхода Python 3.15 в тексте не названы. Слайды с PyConDE устарели и по-прежнему описывают PEP 661 как отложенный; актуальна запись с EuroPython.

Риски и подводные камни

Поддержка нового встроенного sentinel в анализаторах типов, по словам автора, ещё догоняет, поэтому перед использованием стоит проверить свой анализатор. Какие именно анализаторы и в какой мере его поддерживают, в тексте не сказано. Для кода, работающего на версиях до 3.15, нужен обратный порт из typing_extensions. Самодельные sentinel-значения остаются уязвимыми: проверки по тождеству ломаются при копировании и pickle.

«Главный вывод: помните о неоднозначности None и думайте о том, что вы на самом деле хотите выразить.»

— Автор заметки о докладе, florianwilhelm.info