19037
Полный Дзен Пайтона в одном канале Разместить рекламу: @tproger_sales_bot Правила общения: https://tprg.ru/rules Другие каналы: @tproger_channels Сайт: https://tprg.ru/site Регистрация в перечне РКН: https://tprg.ru/xZOL
Путь к ресурсу пакета может быть временным
Ресурс установленного пакета не обязательно лежит обычным файлом рядом с модулем. Пакет может читаться из zip-архива, а importlib.resources.files() возвращает объект Traversable, без гарантии, что ресурс лежит на диске.
Для чтения текста часто достаточно read_text(). Если стороннему API нужен настоящий pathlib.Path, используйте as_file(resource) как контекстный менеджер. При необходимости он извлечёт ресурс во временный файл и удалит его при выходе из with.
Поэтому передавайте путь потребителю внутри блока, пока ресурс нужен. Не сохраняйте его для отложенного чтения после выхода. Очистка касается созданного временного файла: если ресурс уже лежал на диске, исходный файл пакета при закрытии контекста не удаляется.
fileno может отправить маленький файл на диск
Даже если в SpooledTemporaryFile(max_size=1_000_000) записана всего сотня байт, запрос файлового дескриптора перенесёт содержимое из памяти в дисковый файл.
У этого временного файла несколько причин перехода к дисковому файлу. При положительном лимите это превышение max_size. Вызов fileno() делает то же самое независимо от размера, а rollover() явно запускает этот переход. Поэтому библиотека, которой передали file-like объект, может изменить способ хранения одним запросом дескриптора.
Лимит здесь полезен для буферизации, но условие «размер меньше порога» не доказывает отсутствия файлового I/O. При этом слово «диск» описывает файловое поведение API и не указывает на конкретный физический носитель или задержку операционной системы.
Исправление Ruff может сменить исключение
Замена list(values)[0] на next(iter(values)) позволяет получить первый элемент без построения всего списка. Но на пустом входе меняется тип исключения.
Первое выражение поднимет IndexError, второе StopIteration. Обработчик except IndexError больше не сработает. Именно поэтому исправление правила RUF015 в документации Ruff относится к unsafe, хотя на непустом входе оно решает ту же задачу.
По умолчанию Ruff применяет safe fixes. Флаг ruff check --fix --unsafe-fixes разрешает и изменения, способные повлиять на поведение или удалить комментарии. Перед таким запуском стоит проверить пустые входы и обработчики ошибок. Ускорение из примера в документации нельзя переносить на любой код.
Пять секунд HTTPX не равны дедлайну запроса
Сервер может долго отдавать ответ маленькими порциями, а HTTPX не прервёт чтение по истечении пяти секунд с начала запроса. Тайм-аут чтения ограничивает ожидание очередного фрагмента данных. Если фрагменты приходят вовремя, суммарное чтение может длиться дольше.
У HTTPX четыре отдельных ограничения: connect, read, write и pool. Последнее относится к ожиданию свободного соединения в пуле. Поэтому PoolTimeout не исправляется одним увеличением времени подключения к серверу.
По умолчанию библиотека ограничивает сетевую неактивность пятью секундами. Настройки тайм-аутов позволяют задать эти ограничения отдельно. Если нужно ограничить общую длительность операции, задайте отдельный общий дедлайн с корректной отменой и освобождением ресурсов.
compare_digest не выходит на первом различии
Когда ожидаемая и полученная подписи уже вычислены, остаётся проверить равенство. Обычное сравнение может закончиться на первом различии. Для криптографических значений в стандартной библиотеке есть hmac.compare_digest(expected, received).
Функция избегает досрочного выхода по содержимому. Передавайте аргументы одного типа: bytes-like либо строки только из ASCII, например два результата hexdigest(). Произвольные Unicode-строки и смесь str с bytes сюда не подходят.
В документации есть оговорка: разные длины или ошибка могут теоретически раскрыть сведения о длине и типе входов, но не об их значениях. Поэтому обещать одинаковое время для любых аргументов нельзя. Само сравнение также не вычисляет подпись и не решает, кому разрешён доступ.
iterator не ограничивает память драйвера
Для одноразового обхода большой выборки у QuerySet есть iterator(). Он не наполняет обычный кеш результатов самого QuerySet. Если выборка уже была вычислена, вызов iterator выполнит запрос ещё раз.
В Django 5.2 его можно сочетать с prefetch_related(), явно передав chunk_size. Более крупная порция уменьшает число запросов для подгрузки связей, но требует больше памяти. При нескольких связях учитывайте ещё и разрастание списка для SQL IN.
Гарантии «в памяти только одна порция» здесь нет. Документация разделяет уровни: MySQL-драйвер загружает весь результат, а потоковое чтение PostgreSQL зависит от серверных курсоров и настройки пула соединений. Проверять расход памяти нужно на конкретном backend.
NewType различает ID без новых объектов
Идентификаторы пользователя и заказа могут быть обычными int, но случайно поменять их местами всё равно ошибка. UserId = NewType("UserId", int) позволяет описать это различие для статического анализатора.
Функция с параметром UserId принимает UserId(42), а на голое 42 проверяющий должен пожаловаться. В обратную сторону преобразование не нужно: UserId допустим там, где ожидается int. После арифметики вроде UserId(5) + 1 статический тип результата снова int.
Во время выполнения вызов возвращает тот же объект. Это отдельный тип для проверяющего, а не валидатор входных данных и не runtime-подкласс. Использовать его вторым аргументом isinstance нельзя. Проверку внешнего ID всё ещё придётся написать.
ChainMap читает все слои, а пишет только в первый
Для конфигурации удобно читать переопределения поверх значений по умолчанию. ChainMap делает это без предварительного слияния словарей:
from collections import ChainMap
defaults = {"timeout": 30}
overrides = {}
config = ChainMap(overrides, defaults)
config["timeout"] = 5
print(overrides, defaults)
{'timeout': 5} {'timeout': 30}. Чтение ищет ключ в слоях по очереди, а присваивание меняет только первый словарь. Туда же направлены update и удаление. Ключ, который есть лишь в нижнем слое, удалить через цепочку не получится.ChainMap. И копирования вложенных объектов здесь тоже нет. Если прочитанное значение оказалось общим списком, его изменение затронет этот список в исходном слое. Правило записи в первый словарь не означает изоляцию всех доступных через него данных.
Читать полностью…
urljoin спокойно заменит ваш домен
Базовый адрес в первом аргументе не удерживает результат внутри вашего сайта. Второй аргумент может содержать собственный домен даже без схемы:
from urllib.parse import urljoin
print(urljoin(
"https://safe.example/users/",
"//other.example/admin",
))
https://other.example/admin. Сетевой путь //host берёт схему из базы, а домен задаёт свой. Полный абсолютный URL во втором аргументе тоже может заменить адрес. Это штатное поведение, о котором прямо предупреждает документация urljoin.urlsplit поможет выделить схему, хост и путь, но сам по себе не проверит, можно ли им доверять. Успешно разобранный URL ещё не означает, что по нему безопасно переходить.
Читать полностью…
Подмена в pytest может закончиться раньше теста
Тест сначала проверяет код с подменённой зависимостью, а затем хочет вызвать настоящий объект. Если все изменения сделаны через обычную фикстуру monkeypatch, автоматический откат произойдёт лишь после завершения теста или использующей её фикстуры.
Сузить срок жизни можно через with monkeypatch.context() as m. Подмены, зарегистрированные через m.setattr и другие методы этого контекста, откатятся при выходе из блока. После него можно проверять поведение с восстановленной зависимостью, не завершая тест.
В примере pytest так ограничивают подмену functools.partial. Это важно: стандартной библиотекой пользуется и сам pytest, поэтому глобальная подмена может сломать тестовый раннер. Короткий контекст уменьшает время воздействия, но не делает любую подмену безопасной. Для своего кода документация рекомендует явную передачу зависимостей, если это возможно.
Pydantic может создать модель с неверными данными
Наличие объекта BaseModel ещё не доказывает, что данные прошли проверку. В Pydantic V2 model_construct() создаёт модель без валидации. Если поле объявлено вложенной моделью, переданный туда словарь сам в неё не превратится.
Метод не отключает вообще всё: значения полей по умолчанию по-прежнему подставляются. Зато extra="forbid" при таком создании не даст привычной ошибки за лишнее поле. Оно будет проигнорировано. Поэтому контракт обычного конструктора нельзя механически переносить на model_construct.
В документации метода его предлагают для уже проверенных или заведомо доверенных данных. Выбирать этот путь только ради скорости тоже рано: в V2 разрыв сократился, а на простых моделях обычная валидация может оказаться быстрее. Прежде чем убрать проверку, измерьте свой сценарий и определите, кто теперь отвечает за корректность вложенных объектов.
Задача в очереди, а строки в базе ещё нет
Внутри transaction.atomic() вы создаёте заказ и сразу отправляете его ID фоновому воркеру. Воркер может успеть обратиться к базе раньше коммита. Или транзакция откатится, а задача уже останется в очереди.
В Django 5.2 для такого сценария есть transaction.on_commit. Зарегистрируйте функцию отправки задачи внутри atomic. Она будет вызвана после успешного коммита, а при откате отброшена. Callback из вложенного блока ждёт внешнего коммита и не выполняется, если произошёл откат к его savepoint. В обычном режиме autocommit без открытой транзакции вызов происходит сразу.
У этой границы есть предел: база уже закоммичена, когда callback обращается к брокеру. Ошибка отправки не откатит заказ и не гарантирует повторную доставку задачи. on_commit устраняет преждевременный запуск, но надёжность передачи между базой и очередью нужно проектировать отдельно.
На PyPI снова нашли заражённый LiteLLM, популярный шлюз к API разных LLM: версия 1.107.0 провисела около двух часов и ставила стилер. Он срабатывает при старте любого Python-процесса в окружении, даже если litellm никто не импортирует. Заражены были и Docker-образы с тегами latest, main-latest и main-stable. Сейчас версия снята с PyPI, а BerriAI подтвердила удаление образов.
Стилер забирает SSH-ключи, доступы к AWS, GCP, Azure и Kubernetes, пароли из браузеров, секреты CI, токены Claude Code, Cursor, Codex и других ИИ-инструментов, а при доступном DATABASE_URL пытается выгрузить из базы LiteLLM-прокси ключи провайдеров. На Linux и macOS отдельный демон раз в час готов скачать и выполнить новый код, хотя вторая стадия так и не была замечена. По provenance образов исследователи Upwind нашли, что релиз собрал официальный конвейер BerriAI из репозитория-двойника Berri-AI/litellm. Как атакующий это провернул, пока неизвестно.
В марте проект уже взламывали через украденные учётки публикации, у нынешней атаки другие код и инфраструктура, связь между ними не установлена. Если ставились wheels 1.107.0 или запускались заражённые образы, доступные им секреты считаются скомпрометированными. До запуска pip из shell удаляют cook.pth, cook.py, sitecustomize.py и их файлы в __pycache__, останавливают фоновый Python-демон, пересобирают окружения с litellm==1.106.2 и меняют секреты. Индикаторы заражения собраны в issue проекта.
@neuro_channel
ExitStack: закрываем ресурсы в обратном порядке
Список файлов приходит из конфигурации. Их может быть два, а может двадцать. Вкладывать один with в другой неудобно, а при ошибке на пятом файле первые четыре всё равно нужно закрыть.
Для этого есть contextlib.ExitStack. Внутри with ExitStack() as stack вызывайте stack.enter_context(open(path)) для каждого пути. Метод вернёт открытый файл и зарегистрирует его выход из контекста. Если следующий open завершится ошибкой, уже открытые файлы будут закрыты в обратном порядке.
В тот же стек можно добавить обычную функцию очистки через stack.callback. Она тоже выполнится при выходе, но подавить исключение не сможет. В документации ExitStack есть важная граница: сборка мусора сама по себе очистку не запускает. Управляйте сроком жизни стека через with или явно вызывайте close().
Как найти медленную функцию новым профилировщиком Tachyon
Приложение тормозит, но непонятно, где оно проводит время. В Python 3.15 появился статистический профилировщик profiling.sampling, известный как Tachyon.
В разборе Astral показывают запуск через uv:
uv run --python 3.15 -m profiling.sampling run --flamegraph --browser app.py
Где Hypothesis хранит найденные сбои
Hypothesis нашёл неудачный вход, а следующий запуск будет уже на новой машине. Будет ли пример проверен снова? Это зависит в том числе от базы примеров: она сохраняет провалившиеся случаи, а фаза reuse загружает их в следующих запусках.
В обычной конфигурации используется .hypothesis/examples. Если каталог недоступен для чтения или записи, библиотека предупреждает и переходит к базе в памяти. При database=None хранения нет вообще. В текущем встроенном профиле ci задано именно это значение, так что постоянный кеш в CI нельзя считать включённым автоматически.
Проверьте активный профиль и срок жизни хранилища CI-раннера. База ускоряет повтор известных сбоев, но не заменяет регрессионный тест. Важный вход закрепите через @example или отдельный тест, а не оставляйте только в каталоге кеша.
asdict копирует глубже, чем кажется
Вызов asdict(obj) не ограничивается созданием словаря полей dataclass. Функция рекурсивно обходит вложенные dataclass, словари, списки и кортежи, а прочие объекты копирует через copy.deepcopy.
Если поле содержит большой список, рассчитывать на сохранение ссылки на него нельзя. Для поверхностного представления документация предлагает собрать словарь через fields() и getattr(). На карточке видно различие: у такой сборки вложенный список остаётся общим с исходным объектом.
Для чтения полей без копирования вложенных объектов подходит поверхностный словарь. Если нужно менять вложенный список независимо от dataclass, он не подойдёт: вложенные данные остаются общими. Ни один из этих способов сам по себе не превращает произвольное содержимое полей в готовый JSON.
После commit атрибут может потребовать SQL
В асинхронном коде обычное чтение ORM-атрибута не должно неожиданно ходить в базу. Но после commit() SQLAlchemy по умолчанию помечает состояние объектов как устаревшее. Следующее обращение к полю может попытаться загрузить его заново, уже вне явного await.
В руководстве SQLAlchemy 2.0 для AsyncSession используют expire_on_commit=False. Уже загруженные значения остаются доступны после commit. Нужное поле или связь можно обновить явно через await session.refresh(obj, ["field"]).
Это не предзагрузка всех связей и не гарантия свежести после чужой транзакции. Ленивую связь по-прежнему надо загрузить явным способом. Для параллельных задач всё равно нужны отдельные AsyncSession.
shield защищает задачу, но не ожидание
Корутина ждёт важную операцию, и в этот момент её отменяют. Если ожидание обёрнуто в asyncio.shield(task), внутренняя задача не отменяется вместе с вызывающей корутиной. Но у вызывающей корутины await всё равно поднимет CancelledError.
Получаются два разных состояния: ожидающий уже отменён, операция ещё идёт. Создайте задачу явно и сохраните сильную ссылку на неё. Цикл событий держит только слабые ссылки, поэтому один вызов shield не заменяет управление сроком жизни фоновой работы.
shield также не защищает от прямого task.cancel() и отмены из самой операции. В документации разобраны эти случаи. Заранее решите, кто дождётся задачи и обработает её результат. Отмена ожидания не означает, что операция завершилась.
Хеши pip должны покрывать и зависимости
Добавить хеш только главному пакету недостаточно для строгой установки. Команда python -m pip install --require-hashes -r requirements.txt требует закрепить все требования, включая транзитивные зависимости, и указать для них локальные хеши разрешённых артефактов.
Версию фиксируют через == либо задают конкретный URL или путь. Если разрешены разные wheels, одному пакету может понадобиться несколько хешей. Хеш из ссылки самого индекса не заменяет локальный список: индекс не должен сам определять, какой файл вы считаете доверенным.
В текущем руководстве pip описан флаг, добавленный в версии 26.2: --no-require-hashes отключает автоматическое требование хешей для всех строк. Для строгой политики включайте режим явно. Совпавший хеш подтверждает выбранный артефакт, но не безвредность кода внутри.
match в pytest ищет регулярное выражение
Тест с pytest.raises(ValueError, match="bad.file") может пройти на сообщении badXfile. Точка здесь означает любой символ, а не буквальную точку в имени файла. И совпадать со всем сообщением шаблон тоже не обязан.
Параметр match использует re.search. Для буквального фрагмента передайте re.escape("bad.file"). Это защитит метасимволы, но по-прежнему будет искать фрагмент внутри более длинной строки. Если важно полное равенство, отдельно сравните str(exc.value) с ожидаемым текстом.
Ещё одна деталь контракта pytest: поиск учитывает заметки __notes__ из PEP 678. На Python 3.11+ совпадение может найтись в добавленной заметке, хотя его нет в основном сообщении исключения.
Кто закроет async-генератор после break
Вы читаете поток до первого подходящего элемента и выходите через break. Цикл закончился, но сам по себе этот выход не гарантирует немедленное выполнение finally у async-генератора. Если там освобождается ресурс, его срок жизни лучше задать явно.
В Python 3.10+ для этого есть contextlib.aclosing. Оберните генератор в async with, как на карточке. При выходе из блока менеджер вызовет await generator.aclose(), в том числе после раннего выхода или исключения.
Очистка пройдёт в том же контексте, что и перебор. Это существенно, если она использует контекстные переменные или зависит от ещё работающей задачи. В примере из документации очистка выполняется при выходе из блока, без ожидания сборки мусора.
Цикл в графе: независимые задачи ещё можно выполнить
Задача a ждёт b, а b ждёт a. Полностью упорядочить такой граф нельзя. Но если рядом есть независимая задача free, ошибка цикла не обязана останавливать и её.
У graphlib.TopologicalSorter вызов prepare() поднимет CycleError. После обработки этого исключения можно продолжить через get_ready(): он выдаст узлы, которым не мешает цикл. В примере с a ↔ b и отдельным free доступен только free. Завершив его обработку, вызовите done("free").
Важно различать «готов к запуску» и «выполнен». Именно done разблокирует зависящие узлы. Если после независимой части is_active() вернул False, это не доказывает завершение всего графа: цикл остался. Такой частичный прогресс разрешён контрактом TopologicalSorter с Python 3.9. Сам CycleError сообщает один обнаруженный цикл, а не все сразу.
bisect ждёт ключ, insort ждёт запись
У двух функций из одного модуля разные требования к искомому значению. Если записи отсортированы по числовому полю, в bisect_left передайте число, а в insort саму запись:
from bisect import bisect_left, insort
rows = [("Ada", 10), ("Lin", 30)]
print(bisect_left(rows, 20, key=lambda row: row[1]))
insort(rows, ("Sam", 20), key=lambda row: row[1])
print(rows)
1, а вставка поставит ("Sam", 20) между Ada и Lin. У bisect_left функция key применяется только к элементам списка. У insort она применяется и к вставляемому объекту для поиска позиции, но в список попадает исходный объект.key доступен с Python 3.10. Список должен быть уже отсортирован по тому же ключу. И ещё одно ограничение из документации bisect: логарифмический поиск не делает вставку дешёвой. Сдвиг элементов списка по-прежнему занимает O(n).
Читать полностью…
bind принимает строку в параметр intinspect.Signature.bind проверяет, можно ли разложить аргументы по параметрам функции. Аннотация int при этом не превращается в проверку типа:
from inspect import signature
def ship(item: int, *, retries=2):
return item, retries
bound = signature(ship).bind("text")
print(bound.arguments)
bound.apply_defaults()
print(bound.arguments)
{'item': 'text'}. Второй добавляет 'retries': 2. bind не вставляет пропущенные значения по умолчанию в arguments, для этого нужен apply_defaults(). Саму ship этот код не вызывает.TypeError при несовместимых аргументах. Но проверку значений нужно организовать отдельно. В документации bind есть и bind_partial, который разрешает пропустить обязательные параметры. Для проверки полного вызова это уже слишком мягкий контракт.
Читать полностью…
В строке ABA у groupby три группы
Две буквы A не объединятся, потому что между ними стоит B. itertools.groupby начинает новую группу каждый раз, когда меняется ключ соседнего элемента:
from itertools import groupby
print([(key, list(group)) for key, group in groupby("ABA")])
[('A', ['A']), ('B', ['B']), ('A', ['A'])]. Это удобно для серий одинаковых событий в потоке. Для объединения всех записей с одним ключом данные обычно сначала сортируют по тому же key. Получить поведение SQL GROUP BY одним названием функции не выйдет.
Decimal начинается до первого float
Две почти одинаковые записи дают разное округление. На Python 3.14.4 этот пример выводит сначала 2.68, затем 2.67:
from decimal import Decimal, ROUND_HALF_EVEN
cent = Decimal(".01")
print(Decimal("2.675").quantize(cent, rounding=ROUND_HALF_EVEN))
print(Decimal(2.675).quantize(cent, rounding=ROUND_HALF_EVEN))
Decimal из строки получает ровно заданную десятичную дробь. Из float он точно переносит уже существующее двоичное значение, которое чуть меньше 2.675. Конструктор не может восстановить исходное намерение по округлённому представлению числа.Decimal. Метод quantize задаёт нужный показатель степени, а режим округления выбирайте по правилам расчёта. ROUND_HALF_EVEN в примере округляет точную середину к чётной последней цифре. Это конкретная политика, а не универсальное правило для любых сумм.
Читать полностью…
Зелёный CI с устаревшим lock-файлом
Вы добавили зависимость в pyproject.toml, а обновлённый uv.lock забыли закоммитить. Флаг --frozen может показаться строгой проверкой, но у него другой контракт: использовать существующий lock-файл, не проверяя его соответствие проекту.
Чтобы заметить такое расхождение, нужен --locked. По документации uv, команда завершится ошибкой, если lock-файл требует обновления. Например, uv sync --locked проверит его перед синхронизацией окружения. Отдельная проверка без установки зависимостей: uv lock --check.
«Актуальный» здесь означает согласованный с метаданными проекта. Выход новой версии пакета на PyPI сам по себе не делает lock-файл устаревшим. А --no-sync решает третью задачу: отключает проверку и обновление окружения перед запуском. Эти флаги отвечают за разные условия воспроизводимости.
Почему обновления Django важны даже без SQL-инъекций
Запрос может вредить серверу, просто заставляя его тратить слишком много памяти или процессорного времени на разбор данных.
В обновлении безопасности Django от 6 октября исправили несколько таких сценариев. Один связан с кешированием множества длинных языковых кодов, другой — с квадратичной сложностью разбора параметров HTTP-заголовка.
Исправления вошли в 6.1.2, 6.0.9 и 5.2.18. Обновляться нужно до актуального исправленного выпуска своей поддерживаемой ветки.
После обновления проверьте обработку необычных заголовков: внутри парсера изменился механизм разбора, поэтому некоторые некорректные значения могут обрабатываться иначе.
Общий урок для своего кода: ограничивать размер входа до дорогой обработки и помещения в кеш. Даже полезный кеш становится проблемой, если ключи бесконтрольно задаёт внешний запрос.
#python #django
Python 3.16 alpha: что уже может проверить автор библиотеки
Первая альфа Python 3.16 вышла 13 октября. Набор возможностей ещё меняется, но ждать финального выпуска для первой проверки совместимости необязательно.
В анонсе разработчики предлагают запускать библиотеки и приложения на предварительной версии и сообщать об ошибках CPython.
Для библиотеки полезно проверить установку, импорт, тесты и сборку расширений. Если установка не удалась, отделите несовместимость своего кода от отсутствия готовых сборок зависимостей.
Такую проверку можно добавить отдельным заданием CI, сохранив основную матрицу поддерживаемых версий. Сбой предварительной версии нужно разобрать, а не автоматически считать регрессией библиотеки.
Это ранний выпуск для тестирования. Перенос рабочего сервиса на него не требуется; финальный Python 3.16 запланирован на октябрь 2027 года.
#python #тестирование