Python Quiz
477 subscribers
875 photos
735 links
🎓 Python-викторины для программистов.
Каждый день новая задача, разбор и неожиданные фишки языка.
Download Telegram
3 тихих фикса в патч-релизе Python 3.14.7

Раз в полтора месяца выходит скучный x.y.z — обычно скроллю мимо changelog не глядя. Зря.

5 августа вышли 3.14.7 и 3.13.15, разом 499 фиксов. Из них — три, которые реально стоит знать, вынес в карточку.

Самый неприятный — base64: b64decode() в обычном режиме годами молча съедал всё, что шло после первого =. RFC 4648 такого не разрешает, а код, который доверяет base64 как проверке формата, из-за этого мог пропускать лишние байты мимо себя.

И да, quadratic [1]/[last()] в xml.etree — ровно та же болезнь, что была у html.parser на прошлой неделе, просто в другом модуле.

🔗 github.com, github.com, github.com
GitPython закрывал одну и ту же дыру шесть раз за три недели

Полез смотреть security advisories GitPython — и завис.

Одна причина, шестой раз подряд: kwargs с именем git-опции летят прямо в процесс git, без проверки, откуда они взялись. 22 июля закрыли Repo.archive() и git.ls_remote() (CVE-2026-67323). 24-го — сразу два: clone(template=...) с исполняемыми hooks и Remote.add(), где та же утечка $ENV через URL, что чинили неделей раньше, просто с другого входа.

1 августа выяснилось, что сам guard check_unsafe_options() обходится — опцию можно спрятать внутри значения kwarg из одной буквы. 3 августа та же болезнь нашлась в checkout() и TagReference.create().

Шесть advisory за три недели на одну причину. Фикс каждый раз точечный, паттерн — нет.

🔗 github.com, advisories.gitlab.com, advisories.gitlab.com
Октябрь 2026: Python 3.15 выходит — и в тот же месяц хоронит 3.10

Проверил, на чём у нас в проде крутится половина сервисов. 3.10. Ну как проверил — вспомнил после этой новости.

31 октября Python 3.10 официально уходит на пенсию: последний security-патч, дальше — только сам с собой. А 1 октября, ровно за месяц, релизится 3.15.0 (rc1 вышел 4 августа, rc2 — 1 сентября).

Один месяц, два события: новая версия въезжает, старая выезжает. Три с половиной года security-фиксов заканчиваются тихо — без анонса на весь блог, просто дата в таблице devguide.

Если у вас в CI до сих пор python:3.10 — сейчас август, время есть. В декабре его уже не будет.

🔗 python.org, devguide.python.org
pip 26.2: кэш индекса тихо ломает CI

У pip появилось кэширование индекса — и это тихо ломает то, что раньше работало само собой.

29 июля вышел pip 26.2: ответы индекса теперь берутся по Cache-Control, а не ревалидируются на каждый запрос. Для PyPI это значит — только что запушенный релиз pip может не увидеть примерно 10 минут.

CI, который пушит пакет и тут же ставит его следующим шагом, поймает No matching distribution found. Обходится флагом --refresh-package.

Заодно: --only-deps — поставить зависимости пакета без него самого. --no-require-hashes — хэшированные и обычные строки уживаются в одном requirements.txt. И наконец починили build isolation на Python 3.15 — раньше она там просто не работала.

4 августа вышел патч 26.2.1 — без изменений поведения, только баги.

🔗 discuss.python.org, sichard.ca, pip.pypa.io
PyPI наконец получил malware-алерты, как у npm

Наткнулся у Гитхаба на новость, которая прошла тихо: malware-алерты Dependabot почти всегда были про npm. Зловредный пакет с PyPI Advisory Database раньше обычно просто не видел, пока кто-то не заведёт CVE вручную.

7 августа GitHub рассказал, как это починили: Advisory Database тянет фид прямо из OpenSSF malicious-packages, и malware-advisories разом накрыли все восемь экосистем — npm, PyPI, Maven, RubyGems, NuGet, Go, crates.io, Composer. Включили ещё 28 июля, по changelog.

Отличие от обычного CVE: alert не ждёт ревью и CVSS — пакет попал в фид OpenSSF, и он уже в Dependabot. Настраивать ничего не надо, если малварь-алерты уже были включены.

Для меня это «наконец-то»: зловредные пакеты в PyPI раньше ловили постфактум, из твиттера security-исследователя.

🔗 github.blog, github.blog
Langflow раздавал root-токен любому в интернете

Эндпоинт /api/v1/auto_login в Langflow выдавал токен суперпользователя вообще любому, кто до него достучался — хоть с localhost, хоть с другого континента, проверки не было вовсе. Langflow — Python-конструктор LLM-агентов поверх LangChain, инструмент популярный, так что порт наружу торчит у многих.

Придумали это для удобства локальной разработки: не логиниться руками, а сразу получить доступ. Про то, что «локальный» и «сетевой» — не одно и то же, забыли.

Дальше /api/v1/validate/code берёт этот токен как доказательство, что ты свой, и гоняет присланный код через exec(). Два запроса без единого пароля — и у тебя shell с правами сервиса.

IBM закрыли дыру 2 июля, в 1.10.1. По телеметрии KEVIntel с 6 июля — уже 650 попыток атаки с 244 адресов из 41 страны. 4 августа CVE-2026-9198 (CVSS 9.8) попал в CISA KEV, федеральным агентствам дали три дня — до 7-го.

🔗 thehackernews.com, github.com, kevintel.com
PEP 686: Python 3.15 переводит open() на UTF-8 везде

PEP 686 наконец добрался до релиза: с 3.15 open() по умолчанию читает и пишет в UTF-8 — везде, а не только там, где повезло с локалью.

Раньше open("f.txt").encoding на Windows мог тихо оказаться cp1252, а на Linux — utf-8. Один и тот же код, разный результат, никакого предупреждения об этом.

Откат есть: PYTHONUTF8=0 вернёт старое поведение. Но чинить лучше через -W error::EncodingWarning — он показывает каждое место, где раньше молчаливо подставлялась локаль.

На линуксе я разницы вообще не замечу — там и так почти всегда был UTF-8. А вот у знакомых с Windows-продакшеном это повод прогнать код с этим флагом по CI до октября, пока не выкатили финал.

Бонус: доки на docs.python.org до сих пор в паре мест пишут «encoding is locale-dependent» — issue #148603 висит открытым.

🔗 peps.python.org, docs.python.org, github.com
Django 7.0 не выйдет — вместо неё будет 2028.0

Django выкидывает номера версий, которые все запомнили, и я не уверен, что мне это нравится.

Steering Council 10 августа принял DEP 20: вместо релиза раз в 8 месяцев — раз в год, в январе. И каждый релиз теперь живёт как раньше жили только LTS: год багфиксов, потом два года security-патчей. Ярлык «LTS» больше не нужен — так теперь у всех.

Версии переходят на CalVer. Первый релиз по новой схеме — 2028.0, в январе 2028-го. Того, что было бы Django 7.0 в декабре 2027-го, просто не будет — ждать 2028.0.

На 5.2 LTS и 6.2 LTS ничего не меняется, старые обещания по поддержке в силе.

Номер версии, из которого сразу видно год поддержки, — по мне удобнее семвера, ради которого лезешь в таблицу на djangoproject.com. Angular и Ubuntu до этого дошли раньше.

🔗 djangoproject.com, github.com
PEP 831: один флаг — и perf.data легче в 55 раз

55 раз — во столько легче один и тот же perf.data, если Python собран с frame pointers вместо DWARF-unwinding. Один прогон, ~38 000 сэмплов: 5.6 МБ против 306.5 МБ на той же нагрузке.

PEP 831 переводит это в default: с 3.15 флаг -fno-omit-frame-pointer включён в сборке сам по себе, без плясок с пересборкой интерпретатора. perf, py-spy, eBPF-тулзы получают нормальный стек из коробки — и расширения, собранные через sysconfig, подхватывают флаг тоже.

Плата — geomean 0.5–2.3% на реальных бенчмарках, у одного синтетического теста в Fedora вылезало и +9.5%. Не бесплатно, но и не повод спорить.

rc1 — 4 августа, финал 3.15.0 — 1 октября.

🔗 peps.python.org, python.org
FastAPI: 9 дней от «встроили OTel» до breaking change

Самый быстрый разворот дефолта, что я видел в мейнстрим-фреймворке.

29 сентября FastAPI 0.142.0 завёз нативный OpenTelemetry: увидел OTEL_EXPORTER_OTLP_ENDPOINT — сам поднял экспортёры, настраивать ничего не надо. Вчера, в 0.143.0, это уехало в раздел Breaking Changes: теперь только по явному opt-in.

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

Вернуть прежнее поведение: FASTAPI_OTEL_AUTO_CONFIGURE=true или FastAPI(telemetry={"auto_configure": True}).

И не перепутайте: сама инструментация HTTP-запросов по умолчанию осталась. Opt-in стала только автонастройка экспортёров.

🔗 fastapi.tiangolo.com, github.com, fastapi.tiangolo.com
uv 0.13 сменил дефолт на Python 3.15 — и сломал --constraint

--constraint a.txt b.txt в uv 0.13 больше не значит «два файла».

Релиз от 9 октября перевёл --constraint, --override, --exclude и --build-constraint на «одно значение — один путь». Нужно несколько файлов — повторяй опцию для каждого.

Там же сменился дефолтный stable Python: вместо 3.14 — 3.15. Дефолт, не приговор: его перебивают --python, .python-version и requires-python. Но если версия не закреплена нигде, новое окружение приедет на 3.15. Обратно — uv python pin 3.14.

Внутри подключённого через -c файла стало строже: --require-hashes оттуда теперь действительно требует хеши, а -e отклоняется с ошибкой — «matching pip's behavior».

И отдельная строка в ноутах: часть записей кэша от прежних версий переиспользовать нельзя. Первая установка после апгрейда что-то докачает и пересоберёт.

🔗 github.com, docs.astral.sh
Breaking change в SQLAlchemy приехал под тегом [bug]

Если вы уже на SQLAlchemy 2.1 и где-то используете tstring() — сходите посмотрите на строки с двоеточием. Поведение поменяли в патче.

До 2.1.2 каждый обычный кусок t-строки парсился так же, как text(). Двоеточие внутри SQL-литерала — 'time is :now' — уезжало в bound-параметр, и запрос падал на выполнении. Теперь строковые части рендерятся ровно как написаны: ни :name, ни escape \:. Явный параметр — только через bindparam().

Чинит оно больше, чем ломает, спорить не буду. Но в самой записи changelog сказано, что это breaking change, а теги у неё — [sql] [bug], и приехало всё это в патч. Тикет #13616, 2 октября.

2.1.0 вышла 24 сентября, сейчас свежая 2.1.4, а в changelog уже копится секция 2.1.5.

🔗 docs.sqlalchemy.org, github.com
Pydantic 2.14: null и «не прислали» — наконец разные вещи

Пришло {"timeout": null} — это «обнули поле» или «поле просто не прислали»? В обычной модели оба случая схлопываются в None, и дальше ты городишь model_fields_set или свой sentinel.

В Pydantic 2.14 от 8 октября MISSING перестал быть экспериментальным: from pydantic import MISSING. Приехал он ещё в 2.12 — то есть это не новая фича, а снятая табличка «можем сломать».

Поле со значением MISSING выпадает из model_dump() и не появляется в JSON Schema. Сравнивать — через is, не ==.

Там же выкинули Python 3.9 и добавили 3.15.

А «5–20% быстрее» из анонса в чат не тащите: в оригинале это выигрыш от каждой оптимизации по отдельности, на сборке модели, а не на валидации, и на бенчмарках самих авторов.

🔗 pydantic.dev, pydantic.dev
Polars 2.0: три рецепта на порядок строк

В релиз-ноутах Polars 2.0 сказано: порядок строк вернёшь через maintain_order=True. В их же upgrade guide пример для join — maintain_order="left". Строка, а не булев.

Откуда проблема: collect() по-прежнему engine="auto", но с 2.0 «auto» резолвится в streaming — раньше в in-memory. А streaming не держит порядок там, где он не нужен самой операции.

Список таких операций в доках открытый: group_by, join, unpivot «и др.». Так что опереться на «ну select-то точно стабилен» не на что — явной гарантии там нет.

С unpivot интереснее всего: флага не предлагают вовсе, советуют добить .sort(pl.all()) после collect(). В их же примере колонка value была 1,2,3,4,5,6, стала 1,4,2,5,3,6. Строки те же, порядок другой.

Откатить целиком — collect(engine="in-memory") или POLARS_ENGINE_AFFINITY=in-memory.

Релиз 2.0.0 — 6 октября.

🔗 pola.rs, docs.pola.rs
Tachyon: профайлер, который цепляется к живому процессу — уже в stdlib

Стандартная библиотека забрала работу у py-spy: в Python 3.15 есть python -m profiling.sampling attach <PID> — цепляется к живому процессу, без правки кода и рестарта.

Зовут его Tachyon, приехал с PEP 799 — тот свёл профилировку в пакет profiling: profiling.tracing (бывший cProfile) и profiling.sampling. Точные счётчики вызовов — всё ещё за tracing.

Ещё умеет run --live script.py и dump 12345 — один снимок стека и выход. Частоту крутят через -r: 10khz, дефолт 1 kHz.

Чего не ждать: на скрипте короче секунды сэмплов не наберётся, free-threaded сборка к обычной не подцепится, а про накладные расходы в доках только «overhead is virtually zero» без цифр.

И то, обо что споткнётесь первым, — права. Linux: root, CAP_SYS_PTRACE или правка Yama ptrace_scope. macOS: root, debugger-entitlement или выключенный SIP. Windows: админ или SeDebugPrivilege.

🔗 docs.python.org, docs.python.org, peps.python.org
pandas 3.1: inplace=True задепрекейтили у 11 методов

inplace=True не экономил память. Никогда. В pandas 3.1.0 от 22 октября кейворд наконец задепрекейтили — сразу у 11 методов.

Формулировка в whatsnew осторожная: депрекейт «for all methods that do not actually work "inplace"», удалят «eventually». Но ориентир есть: PDEP-8 пишет прямо — «The deprecated keyword is removed in pandas 4.0», а варнинг вы поймаете под именем Pandas4Warning (тикет #63207). У drop, query, sort_values и остальных восьми это был сахар над присваиванием.

fillna() и replace() кейворд оставили — они правят значения на месте. Правда, по PDEP-8 гарантии и там нет: «still silently copy when needed».

Из того же релиза, мимо депрекейтов: read_csv() на engine="c" однопоточно в 2–3 раза быстрее, чем в 3.0, а локальный несжатый файл от 5 МБ читается в несколько потоков по умолчанию — mode.max_threads, по числу CPU, максимум 6.

🔗 pandas.pydata.org, pandas.pydata.org, github.com
DuckDB 2.0: не все 9 удалений предупреждали

con.sql(query).fetchone() в duckdb 2.0 просто нет — и у этого удаления нет истории депрекейта. В release notes про relation-методы ни слова про «deprecated».

Про arrow-группу на DB-API-результате (fetch_arrow_table, arrow, pl и fetch_record_batch) сказано прямо: «Deprecated in 1.x» — warning был, и если вы его годами игнорировали, то вас предупреждали.

А чинить тихое удаление надо не так, как подсказывают release notes. Там советуют limit(1).fetchall() — но это одна строка, а не итерация: в цикле вы вечно получите первую. Migration guide точнее: нужен цикл — делайте результат через con.execute(query), а не con.sql(query).

Остались на relation — for row in rel.iter_rows() (весь результат держится в памяти движка) или rel.to_arrow_reader() для батчей стримом.

Ещё из релиза: Python 3.11+, колёс под 3.10 нет. А начать — grep по .sql( рядом с .fetchone().

🔗 github.com, duckdb.org, duckdb.org