Python Quiz
489 subscribers
867 photos
727 links
🎓 Python-викторины для программистов.
Каждый день новая задача, разбор и неожиданные фишки языка.
Download Telegram
JIT в Python могут выпилить — и не потому что он глючит

Полгода никто не чесался — JIT спокойно жил в main с 3.13, разгонял x86-64/Linux в среднем на 8-9% и должен был стать одной из витрин 3.15 (тот уже во freeze, релиз в октябре). А 5 июня Steering Council внезапно нажал стоп.

Причина не техническая. Всё это держится на PEP 744, а он informational, не standards-track — то есть юридически никого ни к чему не обязывает: ни к поддержке, ни к метрикам успеха, ни к обещаниям совместимости. Совет сам признал: "мы не были достаточно строги к процессу для изменения такого масштаба".

Дали полгода: либо нормальный normative PEP с критериями принятия, либо код едет из main. Баги и security чинят как обычно, новые фичи — заморожены. Томас Воутерс из совета: "we're not unreasonable, but we do want this to be taken seriously" — не расстрельный дедлайн, но и не формальность для галочки.

Мне это скорее нравится, хоть и с опозданием. Три релиза фича жила без единого письменного обязательства на случай, если что-то пойдёт не так. Спохватились бы раньше — меньше народу сейчас зависело бы от undocumented behavior.
👍1
В comprehension наконец завезли распаковку через *

Все путаются в [x for row in matrix for x in row] — кажется, что for идут как во вложенном цикле, а на деле наоборот, второй for относится к первому. Классический баг на собеседованиях.

PEP 798 чинит это через *: [*row for row in matrix] — то же самое, но без риска перепутать порядок. Работает и для {*row for row in matrix}, и для словарей через **.

Забавная деталь из самого PEP: авторов натолкнул письменный экзамен по Python — студенты в ответах уже писали такую звёздочку, будучи уверены, что она существует. Не существовала — до сих пор.

Фича заморожена в 3.15.0b1, релиз в октябре. Двойной for никуда не денется, но хотя бы будет чем его заменить, когда не хочешь гадать про порядок циклов.
Ваш free-threaded питон может тихо врать про GIL

Поставил 3.14.0t (free-threaded сборку), жду параллелизма — а профиль как будто однопоточный.

Оказалось, это норм поведение, просто нигде не орёт об этом. Любое C-расширение, не помеченное как поддерживающее free-threading, при импорте тихо включает GIL обратно. Без варнинга, без ошибки — просто твой параллелизм молча схлопывается в обычный GIL-режим.

Проверяется одной строкой после всех импортов:

python3.14 -c "import sys; print(sys._is_gil_enabled())"

True после импорта нужных тебе либ — значит что-то из зависимостей откатило GIL обратно, и весь выигрыш от free-threaded сборки ты потерял, просто об этом не узнал.

Есть ещё жёсткий вариант — PYTHON_GIL=0 или -X gil=0: тогда откат запрещён, и несовместимое расширение уронит процесс вместо тихого отката. Для CI это честнее, чем молчаливая деградация.

Перед тем как постить в чат бенчмарки с линейным ускорением на 3.14t — сначала эта проверка, потом бенчмарк.
Что выведет этот код с lazy import в Python 3.15?

В 3.15 beta 4 подвезли lazy import (PEP 810) — модуль реально не грузится, пока не тронешь его атрибут. Звучит как мелочь, а ломает интуицию с ходу.

Взял классику: import this печатает дзен прямо при загрузке модуля, это все знают. Обернул в lazy import и написал zen = this. Был уверен, что присваивание — это уже «использование». Не угадал.

Прогони в голове, без REPL. Порядок печати — в опросе.
В Django чужая сессия могла прилететь вам в общем кэше

Django тихо чинит баг, из-за которого чужая сессионная кука могла закэшироваться и уйти следующему юзеру.

UpdateCacheMiddleware и cache_page() пропускали ответ с Set-Cookie в кэш, если проверяли только один случай: запрос пришёл вообще без кук. Логика была «нет кук на входе — значит и Set-Cookie можно смело кэшировать». Стоило браузеру принести любую левую куку — тему, язык, A/B-флаг — и проверка молча выключалась. Сессионный Set-Cookie улетал в общий кэш как ни в чём не бывало.

Дальше классика cache poisoning: если перед Django стоит Redis, Memcached или CDN — следующий, кто попал в тот же ключ кэша, мог получить в ответе чужую сессию.

Бесит не сама дыра, а то, как она написана — условие «если у запроса нет X» почти всегда придумано под один тестовый сценарий, а не под реальный трафик, где у половины пользователей уже висит кука с языком интерфейса или темой.

Пофиксили в 6.0.7 и 5.2.16 (заодно и в 6.1-бете), CVE-2026-48588, severity формально low — нужны shared cache и Vary: Cookie вместе. Но именно так у многих и настроено.

Если у вас cache_page или UpdateCacheMiddleware рядом с сессиями — это тот случай, когда апдейт не терпит «на следующем спринте».
Python откатил архитектуру сборщика мусора прямо в патч-релизе

Python 3.14.0 в октябре сменил сборщик мусора — с generational на incremental, обещали паузы на больших кучах короче на порядок. В 3.14.5 в мае это просто откатили назад. Патч-релиз отменяет архитектурное решение целого мажора — в CPython такое почти не бывает.

Что пошло не так: на долгоживущих процессах (веб-серверы, пайплайны) резидентная память росла без остановки. По разбору pydevtools, счётчик инкрементальной работы мог уходить в минус — и полный проход сборщика попросту не запускался: в одном из кейсов первый full collection случился только на 20 000-й итерации, а в устойчивом состоянии в памяти висело больше 90 тысяч непрочищенных циклов.

Core team не стала ждать плановый 3.14.6 — ускорили релиз и откатили incremental GC сразу в 3.14 и 3.15, вернув generational из 3.13.

Смущает не сам баг — memory-менеджмент штука тонкая. Смущает то, что фичу пять месяцев гоняли через альфы, беты и rc — и всё равно поймали только когда реальный прод начал жаловаться на RSS. Если вы успели обновиться до 3.14.0–3.14.4 и мониторили память — вы только что бесплатно побетатестили GC для всего коммьюнити.

🔗 discuss.python.org, pydevtools.com
Гитхаб был чистый. Пакет — нет

В mrmustard 0.7.4 — квантовой библиотеке Xanadu на PyPI — три дня назад нашли стилер. Тащит с диска SSH-ключи, AWS- и Kubernetes-креды и ставит три способа зацепиться в системе на будущее.

Но бесит не сам стилер, а то, как его спрятали.

У версии 0.7.4 нет ни тега, ни релиза, ни коммита в GitHub-репозитории. Открываешь исходники на гитхабе — там всё чисто, ревью ничего не найдёт. Вредонос добавили только в артефакт, который улетает через pip install: угнали токен CI из аккаунта мейнтейнера (это был ziofil, автор проекта) и залили пакет в PyPI напрямую, мимо обычного пайплайна.

Отдельная деталь для тех, кто вообще думает про такие атаки: пейлоад сначала проверяет, что он не в CI и не в контейнере, и только потом стартует. Целились именно в ноутбуки разрабов и исследователей, а не в одноразовые раннеры.

После такого фраза «да я гитхаб пролистал, там нормально» перестаёт что-либо значить. Гитхаб и то, что реально прилетает по pip install, — два разных артефакта, и связывает их только честность мейнтейнера в момент публикации. Если пиньте пакеты по хэшу wheel — вот ровно тот случай, когда это спасает, а не паранойя.

Нашёл всё не Xanadu, а сторонний исследователь из Aikido Security. PyPI пакет после этого отправил в карантин.

🔗 stepsecurity.io, safedep.io
PyPI перестал пускать в старые релизы

Запушил докрутку — забытое wheel под 3.14, например — в релиз, которому две недели? С 22 июля PyPI такое просто не примет.

Правило простое: через 14 дней после публикации релиз закрывается для новых файлов насовсем, без исключений. Тема всплывала ещё в PEP 740 в 2024-м, но реально завелась в марте — после того как через скомпрометированные токены и воркфлоу закинули мусор в LiteLLM и Telnyx. Старые доверенные релизы — идеальная мишень: их качают привычно, без опаски.

Перед тем как включать, проверили, насколько больно это ударит по обычным майнтейнерам: из топ-15k пакетов только 56 хоть раз добавляли cp314-колесо релизу старше двух недель. Почти никто так не работает — а вот атакующему такая лазейка была подарком.

Шероховатость: официального способа проверить статус релиза («закрыт/не закрыт») пока нет — узнаёшь только по 403 при аплоаде. Мелочь, но для CI, который доливает артефакт постфактум, это неприятный сюрприз.

🔗 blog.pypi.org
uv наконец умеет проверять типы точечно, а не всей монорепой разом

Пока весь техтвиттер спорит про JIT и Rust в CPython, Astral тихо прокачивает uv в сторону монорепо.

23 июля вышел uv 0.11.32 — добавили --package и --all-packages для uv check. Сама команда check появилась только в июне: она гоняет ty, тайп-чекер Astral, прямо из uv, без отдельного pip install ty.

До этого патча uv check в воркспейсе с десятком пакетов проверял всё разом — падает один сервис, и непонятно, на что смотреть в первую очередь. Теперь можно натравить проверку на конкретный пакет флагом --package, а --all-packages явно просит пройтись по всем.

Флаг мелкий, но по нему хорошо видно курс: после покупки Astral в марте OpenAI явно не делает из uv просто быстрый pip — это заявка на единый вход в lint/format/typecheck. Направление мне нравится: меньше pip install ruff mypy black в каждом новом проекте.

Минус один: check всё ещё за preview-флагом, синтаксис может поменяться до стабильного релиза — тащить в CI прямо сейчас я бы не спешил.

🔗 github.com, github.com
object() как сентинел годами тихо ломался в pickle — и почти никто не замечал

Сколько раз ты писал MISSING = object() для дефолта, который надо отличить от None? Я — постоянно. И не знал, что если такой сентинел уедет через pickle (сессии, кэш, multiprocessing, Celery) — на другом конце restored is MISSING вернёт False. Тихий баг, всплывающий только в проде, когда объект пересекает границу процесса.

В Python 3.15 завезли builtin sentinel() (PEP 661). Он запоминает модуль и имя, поэтому pickle и copy.deepcopy восстанавливают тот же объект, а не подделку. Плюс вменяемый repr из коробки — никаких <object object at 0x7f...> в логах.

Мелочь, а сколько самодельных class _Missing: pass теперь можно выкинуть.

🔗 peps.python.org
В Python 3.15 завезли нормальный сэмплирующий профайлер

Полгода таскал py-spy в докер-образ, потому что сэмплирующего профайлера в stdlib не было — только cProfile, который считает каждый вызов и заметно тормозит прод. В 3.15 это закрыли.

Новый модуль profiling.sampling (кодовое имя Tachyon, PEP 799) цепляется к живому процессу по PID и читает стек снаружи — без инструментирования кода и без рестарта:

python -m profiling.sampling attach 12345

Частота по умолчанию 1кГц, можно гнать хоть до 1МГц. На выходе — flamegraph, pstats или html, на выбор. Профайлер и целевой процесс должны быть на одной минорной версии — из 3.14 к процессу на 3.15 не приаттачиться.

Заодно cProfile переехал в profiling.tracing (алиас остался жить), а древний модуль profile объявили deprecated — выпилят в 3.17.

py-spy из докерфайла выкидываю на следующем спринте: не вижу смысла тащить внешний бинарник, когда то же самое теперь есть из коробки.

🔗 peps.python.org, docs.python.org
До Python 3.15 осталось 66 дней

Открыл вчера changelog beta 4 — и это последняя бета. Дальше только релиз-кандидаты.

7 мая зафиксировали фичи (PEP 790) — с тех пор в 3.15 можно только чинить баги, нового уже не прилетит. 18 июля вышла b4: ~298 фиксов с прошлой беты, и всё, бета-цикл закрыт.

Сегодня мы ровно между b4 и rc1 — до октябрьского релиза 66 дней. Если у вас в проекте есть код под sys.version_info >= (3, 15) — самое время гонять его на бете, а не ждать октября: после rc1 (4 августа) ABI и API стараются больше не трогать.

🔗 blog.python.org, peps.python.org
Последние дни ковырял тестирование AI-генерации в Vaulto Cards.

Само расширение довольно простое: выделяешь слово или фразу в браузере, и оно превращается в учебную карточку. Недавно я добавил библиотеку карточек и режим обучения, поэтому генератор стал заметно сложнее.

И появилась проблема: после каждой правки нужно было вручную открывать расширение, создавать карточки и смотреть, не стало ли хуже.

В итоге я вынес генерацию в отдельный Node.js-скрипт. Он запускает тот же пайплайн, что используется в расширении, только без интерфейса, а результат сохраняет в JSON и картинки.
Так довольно быстро нашлись странные ошибки. Например, целое предложение иногда обрабатывалось как одно случайное слово, а фраза el gato определялась просто как артикль.
Пришлось явно разделить режимы WORD и SENTENCE и добавить очистку входных данных.
Ещё отдельно описал весь запуск в TESTING_AI_CARDS.md и добавил ссылку в CLAUDE.md.
Теперь, когда я открываю новую сессию Claude Code, мне не нужно каждый раз объяснять, как про
У pip, uv и Poetry впервые будет общий выборный начальник

У pip, uv, Poetry и вообще всего Python-пакеджинга до сегодняшнего дня не было ни одного выборного органа — решения по интероп-спекам (wheel, PEP 440 и остальные) принимала неформальная PyPA, группа мейнтейнеров без единого мандата от комьюнити.

Как объявили в blog.python.org, сегодня в 14:00 UTC стартовали номинации сразу в новый Packaging Council (PEP 772, утверждён в апреле) и в PSF Board — окно до 11 августа, голосование 1–15 сентября.

Мест пять: топ-2 по голосам — два года (когорта A), следующие три — год (когорта B). Дальше когорты ротируются по очереди, чтобы совет не менялся весь разом.

Спорный PEP уровня будущего wheel v2 теперь будет решать голосование пятерых выборных, а не один решительный мейнтейнер, который раньше просто брал и делал. Медленнее, зато без личных вето — для стандартов, на которых держится весь пакеджинг, по-моему разумный обмен.

🔗 blog.python.org, blog.python.org, peps.python.org
GIL уходит поэтапно — и по цифрам это заметно

Три фазы у free-threaded CPython, и по цифрам видно, что вторая — не косметика.

Phase I (3.13, октябрь 2024): экспериментальный билд, adaptive-интерпретатор выключен ради безопасности — минус 40% на single-thread коде. Многие тогда попробовали, увидели просадку и закрыли тему для себя.

Phase II (3.14, сейчас): PEP 779 приняли 16 июня 2025-го, статус — официально поддерживается, но не по умолчанию. Adaptive-интерпретатор вернули, просадка упала до 1% на macOS aarch64 и максимум 8% на x86-64 Linux.

Phase III — билд без GIL по умолчанию — пока просто пустая графа в PEP 779: критериев для неё авторы сознательно не написали. Ни PEP, ни даты — Steering Council решила сперва пожить с двумя билдами и посмотреть, что покажет прод.

🔗 peps.python.org, docs.python.org
Один Host-заголовок обходил защиту в каждом FastAPI

Пересчитал даты в advisory — между «нашли дыру» и «патч в релизе» прошло почти четыре месяца.

27 января X41 D-Sec наткнулись на баг, когда по заказу OSTIF гоняли аудит vLLM. Причина глубже: в Starlette request.url собирали из заголовка Host без проверки, а роутинг при этом смотрел на сырой путь из scope. Подсунул кривой Host — и request.url.path, на который завязаны миддлвари с проверками доступа, начинает врать.

Мейнтейнеров уведомили 4 февраля, патч предложили 1 марта, а зашили в релиз только 21 мая — Starlette 1.0.1. Раскрыли на следующий день.

Starlette — подложка под FastAPI, vLLM, LiteLLM, 325 млн скачиваний в неделю. CVSS у мейнтейнера 6.5, у X41 — 7.0, и это не формальность: чем чаще access-контроль держится на пути запроса, а не на честной аутентификации, тем больнее такая дыра.

Если у вас FastAPI и Starlette не трогали с весны — гляньте версию.

🔗 github.com, ostif.org, badhost.org
Один релиз Pillow закрыл четыре дыры разом

Проверил pip show pillow в паре своих проектов — из них не все жили на актуальной версии.

12.3.0 вышла ещё 1 июля, но CVE на самый неприятный баг из четырёх — EPS-парсер уходит в бесконечный цикл на отрицательном byte count в %%BeginBinary — раскрыли только 14-го, и это до сих пор мало где обсуждали.

В том же релизе тихо закрыли ещё три штуки: PDF без лимита на декомпрессию (классический zip bomb), shell-инъекцию через ImageShow.WindowsViewer, если путь к файлу не свой, и утечку памяти на специально собранном JPEG2000.

Если Pillow где-то в проекте парсит файлы от пользователей — это апдейт на сегодня, а не «когда-нибудь занесу в спринт».

🔗 github.com, github.com
Ruff одним патчем увеличил число правил в 7 раз

59 → 413. Именно так вырос дефолтный набор правил в Ruff 0.16.0, который вышел 23 июля.

С версии 0.1.0 в линтере накопилось 260+ новых правил — но по умолчанию их не включали, боялись сломать людям CI. Тут решились: заодно выпилили 18 самых холиварных pycodestyle/pyflakes-правил вроде E731 и F403 — слишком спорные, чтобы навязывать их всем.

Саймон Уиллисон прогнал релиз по своим проектам — и в sqlite-utils нашлось 1618 новых замечаний на коде, который днём раньше проходил линт чисто.

Откатить старое поведение: select = ["E4", "E7", "E9", "F"] в конфиге.

🔗 astral.sh, simonwillison.net, github.com
Django 6.1 отрезает Python 3.10, 3.11 и Postgres 14

У меня в трёх сервисах на проде до сих пор Python 3.10 — и до 5 августа с этим лучше определиться.

Django 6.1 rc1 вышел 22 июля, финальный релиз — на днях. И это не косметический минор: под капотом подняли нижнюю планку сразу по всей матрице окружений.

PostgreSQL 14 выкидывают — не потому что кто-то поленился тестировать, а потому что апстрим сам хоронит эту версию в ноябре. SQLite подняли с 3.31 до 3.37 — это уже не «Ubuntu 20.04 из коробки», такое придётся доставлять руками. MariaDB и MySQL тоже сдвинули на пару минорных версий вверх.

Если в докерфайле годами лежит python:3.10-slim и старый postgres:14 — это тот редкий апгрейд, где pip install -U django сам по себе не сработает, пока не поднимете инфру под ним.

🔗 djangoproject.com, docs.djangoproject.com