
Всем привет! С вами Леша Жиряков, техлид backend-команды витрины онлайн-кинотеатра KION. Сегодня будет обзор Python 3.14 — в общем и целом он построен на официальной документации, которая ежедневно обновляется в преддверии финального релиза. Так что «улыбаемся и читаем» (привет, «Мадагаскару»), но сильно не напрягаемся.
Когда уже релиз? Скоро, коллеги, скоро. Финальная версия должна появиться на свет 7 октября. Багфиксы будут выходить примерно каждые два месяца в течение двух лет, а обновления безопасности — вплоть до 2030 года. Пока можно насладиться новыми фичами 7-ой альфа-версии. Точнее, последней альфа-версией — дальше нас ждет бета. Ну, погнали!
Автостопом по пепам (PEPs)
Все самое новое и интересное для Python описывается в PEPs (Python Enhancement Proposals). Не буду томить — сразу прыгнем в них с головой!
Теперь except можно без скобок

Теперь разрешена обработка нескольких исключений без скобок. Важно заметить, что их можно не указывать только в том случае, если не используется условие as. Нужно это для улучшения читаемости кода. Покажу на примерах.
В Python 3.14 обработать исключения можно будет так:
try:
...
except Exception_One, Exception_Two, Exception_Three:
...
Чуть меньше скобок и чуть больше минимализма. Но при использовании ключевого слова as скобки должны быть:
except (Exception_One, Exception_Two, Exception_Three) as exception:
...
Это нововведение влияет только на синтаксис. Поведение механизма обработки исключений остается таким же.
Расширен API языка C

Добавлен API языка C для конфигурации инициализации Python. Фух. Непросто такое прочитать, не правда ли?
Теперь можно:
создавать конфиг (структура PyInitConfig, PyInitConfig_Create, PyInitConfig_Free);
получать настройки (PyInitConfig_HasOption, PyInitConfig_GetInt, PyInitConfig_GetStr, PyInitConfig_GetStrList, PyInitConfig_FreeStrList);
устанавливать их (PyInitConfig_SetInt, PyInitConfig_SetStr, PyInitConfig_SetStrList, PyInitConfig_AddModule);
инициализировать (Py_InitializeFromInitConfig);
обрабатывать ошибки (PyInitConfig_GetError, PyInitConfig_GetExitcode);
заниматься настройкой конфигурации текущей среды выполнения (PyConfig_Get, PyConfig_GetInt, PyConfig_Set, PyConfig_Names).
То есть в одном API объединены настройки как преинициализации, так и инициализации Python.
Отложенное вычисление аннотаций

Теперь аннотации будут вычисляться по требованию. Как? С помощью annotate, который можно найти у всех объектов, поддерживающих аннотации: функции, классы и модули.
Считается, что это изменение повысит производительность аннотаций и сделает их более удобными для использования. В связи с этим появился новый модуль annotationlib, который предоставляет инструменты для исследования отложенных аннотаций.
Есть три варианта вычисления аннотаций:
VALUE — вычисляет аннотации к значениям runtime.
FORWARDREF — заменяет неопределенные имена специальными маркерами.
STRING — возвращает аннотации в виде строк:
>>> from annotationlib import get_annotations, Format
>>> def func(arg: Undefined):
pass
>>> get_annotations(func, format=Format.VALUE)
Traceback (most recent call last):
...
NameError: name 'Undefined' is not defined
>>> get_annotations(func, format=Format.FORWARDREF)
{'arg': ForwardRef('Undefined', owner=<function func at 0x...>)}
>>> get_annotations(func, format=Format.STRING)
{'arg': 'Undefined'}
Прекращение использования PGP-сигнатур

PGP (Pretty Good Privacy) — компьютерная программа, которая позволяет выполнять операции шифрования и цифровой подписи сообщений, файлов и другой информации. Если мы говорим про PGP, нужно коснуться и Sigstore. С одной стороны, это стандарт подписания, проверки и защиты ПО, а с другой — набор инструментов и сервисов.
Вернемся к главному. С версии Python 3.11 предоставлялось два типа проверяемой цифровой подписи для всех артефактов CPython: Sigstore и PGP. В итоге из-за особенностей каждого из них было отдано предпочтение Sigstore. Поэтому ожидается прекращение использования подписей PGP в новых релизах.
Код в finally может вызвать SyntaxWarning

Итак, по порядку. У конструкции TRY/EXCEPT/ELSE есть еще одна часть — FINALLY. С ней как раз все не так просто.
Допустим, есть код:
def hello_habr() -> str:
try:
return "hello! (from 'try')"
except Exception as exception:
print(exception)
return "hello! (from 'except')"
finally:
return "hello! (from 'finally')"
print(hello_habr())
Можно заметить, что return есть как в TRY, так и в FINALLY. Какую же строку выведет print? Правильно, “hello! (from 'finally')”
. То есть выводится строка из блока FINALLY.
Поведение, которое рассмотрено в примере выше, не совсем явное. Это и привело к тому, что теперь код выше будет выводить SyntaxWarning:
/test.py:8: SyntaxWarning: 'return' in a 'finally' block
return "hello! (from 'finally')"
И положительные примеры, то есть не вызывающие предупреждения:
try:
...
finally:
def f():
return 1
try:
...
finally:
for x in "hello":
break
Получается, все зависит от ситуации. Если как в примерах выше в коде в блоке FINALLY расположена функция или цикл FOR, предупреждения не будет.
На 30% быстрее

В CPython добавлен новый интерпретатор, который использует промежуточные вызовы между небольшими функциями языка программирования C. Эти функции реализуют отдельные коды операций Python вместо одного большого оператора case ЯП C.
Для некоторых компиляторов этот интерпретатор дает значительный прирост производительности. По предварительным данным, код на Python выполняется примерно на 30% быстрее, чем в предыдущей версии. За основу берется Python 3.14, собранный с Clang 19 без использования вышеописанного интерпретатора.
Новый интерпретатор пока работает только с Clang 19 и более новыми версиями на архитектурах x86-64 и AArch64. Но ожидается поддержка в будущих версиях GCC.
Новая функциональность сейчас доступна. Рекомендуется включить оптимизацию с учетом профиля в новом интерпретаторе, поскольку это единственная конфигурация, которая была протестирована и подтвердила улучшения в производительности. В документации можно почитать подробнее об опции --with-tail-call-interp.
Всего понемножку
Опциональный аргумент strict в map
Теперь в функцию map добавлен опциональный аргумент strict. Он вызывает ошибку, если переданные итерируемые объекты не равны по длине. Рассмотрим на примере:
names = ["Anna", "Tony", "Obi-Wan"]
greetings = ["Hello", "Привет"]
result = map(lambda greeting, name: f"{greeting}, {name}!", greetings, names)
print(list(result))
Видим такой вывод:
['Hello, Anna!', 'Привет, Tony!']
Получается, что не хватило одного значения для имени Obi-Wan. Как мы об этом узнали? Посмотрев на вывод. Честно говоря, хотелось бы сделать это более явным. И вот тут на помощь приходит strict. Отредактируем код, добавив strict=True:
result = map(lambda greeting, name: f"{greeting}, {name}!", greetings, names, strict=True)
print(list(result))
Теперь вызывается исключение ValueError:
Traceback (most recent call last):
File "test.py", line 4, in <module>
print(list(result))
~~~~^^^^^^^^
ValueError: map() argument 2 is longer than argument 1
Из текста ошибки видно, что один из аргументов длиннее другого.
Методы для копирования и перемещения (pathlib.Path)
Для класса Path модуля pathlib добавлены методы для копирования (copy, copy_into) и перемещения (move, move_into) файлов и директорий.
Например, рассмотрим, как работает метод copy_into. Пусть есть директория Dir_For_test, в которую нужно скопировать файл test.py:

Можно сделать это так:
>>> import pathlib
>>> path = pathlib.Path("test.py")
>>> path.copy_into("Dir_For_test")
PosixPath('Dir_For_test/test.py')

Добавлена поддержка UUID версий 6-8
Теперь при помощи модуля uuid можно сгенерировать UUID 6-8 версий:
>>> import uuid
>>> uuid.uuid6()
UUID('1f0202f4-56a2-6558-badb-4e4d8087977a')
>>> uuid.uuid7()
UUID('0196623b-7abd-7366-bcb1-15a48c7284a8')
>>> uuid.uuid8()
UUID('f811af07-fd8e-8834-a356-1bef8ee68d9c')
Щепотка асинхронности
Элементы asyncio (iscoroutinefunction, set_event_loop и то, что относится к loop policy) теперь считаются устаревшими и будут удалены в Python 3.16. Уже удалены классы и функции, которые относятся к process watchers.
Изменился принцип работы get_event_loop. Теперь, если функция вызывается, но цикла обработки событий не существует, возникает ошибка RuntimeError:
>>> import asyncio
>>> asyncio.get_event_loop()
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
asyncio.get_event_loop()
~~~~~~~~~~~~~~~~~~~~~~^^
File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/events.py", line 719, in get_event_loop
raise RuntimeError('There is no current event loop in thread %r.'
% threading.current_thread().name)
RuntimeError: There is no current event loop in thread 'MainThread'.
Важно учитывать этот момент в работе get_event_loop, чтобы не было ненужных ошибок.
Время итогов

Python 3.14 приносит с собой различные изменения: нотку минимализма (except без скобок), чуть больше внимания к явному коду (SyntaxWarning в finally), откладывание на потом (отложенное вычисление аннотаций) и, конечно, прирост производительности почти на 30% (новый интерпретатор).
Изменения в модулях и C API я затронул кратко. На самом деле в документации их намного, нет, НАМНОГО больше! Поэтому, если интересно, что еще нового, смело в нее заглядывайте: она не кусается.
Буду рад, если поделитесь в комментариях, что больше всего вам понравилось или не понравилось из нововведений в Python 3.14. А может, вы уже установили альфа-версию локально и получили новые впечатления? В общем, пишите. Уже бегу читать.