Гарантия или деталь
На что в Python можно опереться архитектурно, что использовать с оговоркой, а что не проверять даже тестом.
Один вопрос, который отличает знание языка от знания реализации: сломается ли мой код на другой реализации или в следующем релизе? Ниже — реестр всех неочевидных мест канона, разложенных по этому вопросу. Читать подряд не обязательно: страница задумана как справочник, к которому возвращаются.
🔒Гарантия языка. Зафиксировано спецификацией или документацией. Опираться смело — если сломается, это баг интерпретатора.
🔧Деталь CPython. Работает и, скорее всего, продолжит. Но никто не обещал: на PyPy или в следующем минорном релизе может исчезнуть без предупреждения.
🕳Не определено. Зависит от платформы, сборки, порядка выполнения или просто никем не зафиксировано. Опираться нельзя даже в тестах.
В реестре: 25 гарантий · 25 деталей · 7 неопределённостей
Как этим пользоваться на практике. Правило простое: 🔒 можно закладывать в архитектуру, 🔧 можно использовать, но нельзя делать несущим, 🕳 нельзя проверять тестом — тест будет зелёным ровно до чужой машины. И отдельно: если 🔧 нужен как несущий, это должно быть записано в коде комментарием, а в требованиях — «только CPython такой-то версии».
Объектная модель и имена
🔒
is сравнивает идентичность, == — значение; идентичность уникальна на время жизни объекта
Опираться можно. Но адрес переиспользуется после смерти объекта, поэтому id() нельзя хранить как долгоживущий ключ — он совпадёт с чужим.
глава 01 →🔧
id() возвращает адрес объекта в памяти
Верно для CPython. Язык обещает только уникальность числа среди живых объектов; на PyPy это не адрес.
глава 01 →🔒
Дефолт аргумента вычисляется один раз при выполнении def
Это зафиксированная семантика, а не недосмотр. Mutable default опасен именно потому, что поведение гарантировано.
глава 01 →🔧
Малые целые от −5 до 256 закешированы: a is b для них истинно
Границы диапазона менялись и не документированы как контракт. Сравнивать числа через is нельзя никогда.
глава 02 →🔧
Одинаковые литералы внутри одного объекта кода — один объект
Свёртка констант компилятором. В функции 'ab' is 'ab' истинно, а собранная в рантайме такая же строка — уже другой объект.
глава 01 →🔧
Какие строки интернируются автоматически
Зависит от того, литерал ли это и похож ли на идентификатор; менялось между версиями. Гарантия есть только у явного sys.intern.
глава 02 →Числа
🔒
int произвольной точности; // округляет вниз, знак % совпадает со знаком делителя
Это спецификация языка, а не свойство реализации. Именно здесь Python расходится с C, где деление усекает к нулю.
глава 02 →🔧
Внутреннее представление int: массив 30-битных цифр, sys.getsizeof(2**70) == 36
Конкретные размеры и разрядность цифры — деталь сборки. В 3.12 добавили ещё и лимит на длину десятичного представления.
глава 02 →🔒
float — это IEEE 754 double со всеми его свойствами
Опираться можно: 0.1 + 0.2 != 0.3 здесь ровно по той же причине, что в C.
глава 02 →Контейнеры, dict, хеши
🔒
Амортизированная сложность: list.append — O(1), list.insert(0, …) — O(n), x in list — O(n), x in set и x in dict — O(1)
Записано в документации и является контрактом. Всё, что выводится из сложности, безопасно.
глава 03 →🔧
Закон роста списка (0, 4, 8, 16, 25, 35, …) и момент, когда буфер сжимается
Формула менялась и может меняться. Опираться на конкретный sys.getsizeof списка — способ написать хрупкий тест.
глава 03 →🔒
dict сохраняет порядок вставки (с 3.7)
Редкий случай: в 3.6 это была деталь реализации 🔧, в 3.7 стало гарантией — потому что на неё уже оперлись. Канонический пример того, как деталь становится обещанием.
глава 05 →🕳
Порядок обхода set
Выглядит стабильным в пределах запуска, зависит от хешей и истории вставок. Никогда не полагаться, даже в тестах.
глава 05 →🕳
Конкретное значение hash('строка') между запусками процесса
Рандомизируется солью (PYTHONHASHSEED) — защита от атак на коллизии. Отсюда: хеш нельзя сохранять на диск.
глава 05 →🔒
Контракт хеша: a == b влечёт hash(a) == hash(b), и хеш не меняется за время жизни объекта
Нарушение контракта — не ошибка интерпретатора, а тихая потеря ключей. Отсюда запрет на изменяемые ключи.
глава 05 →🔧
Компактная раскладка dict, разделяемые ключи инстансов (PEP 412), хранение значений в преамбуле объекта
Гарантируются поведение и сложность, а не байты. Раскладка менялась в 3.3, 3.6 и 3.11.
глава 05 →🕳
Мутация списка во время итерации по нему
Не запрещено и не проверяется — просто пропускает элементы. У dict и set та же операция даёт явный RuntimeError 🔒.
глава 03 →Атрибуты, классы, протоколы
🔒
Порядок поиска: дескриптор данных на типе → __dict__ инстанса → MRO типа → __getattr__
Процедура зафиксирована языком. Всё поведение property, classmethod и super() выводится отсюда.
глава 04 →🔒
MRO вычисляется C3-линеаризацией; super() идёт по MRO инстанса, а не по родителю класса
Опираться можно. Из этого следует, что кооперативное множественное наследование работает только если его соблюдают все участники цепочки.
глава 06 →🔒
Специальные методы ищутся на типе, минуя __getattr__ и __dict__ инстанса
Отсюда: дандер, назначенный инстансу, не работает; прокси-объекты вынуждены перечислять дандеры списком.
глава 11 →🔧
Слоты типа обновляются при присваивании дандера классу после его создания
Работает, но это механика type.__setattr__ в CPython, а не обещание.
глава 11 →🔧
__slots__ экономит память; конкретный выигрыш в байтах
Эффект гарантирован качественно (нет __dict__), величина — нет: она зависит от версии, потому что раскладка инстанса менялась.
глава 04 →🔧
Инлайн-кеши и специализация байткода (PEP 659) ускоряют доступ к атрибуту и вызовы
Ускорение реально и измеримо, но условия срабатывания не документированы. Мономорфный цикл быстрее полиморфного — на 3.11 в 1.4 раза, на другой версии иначе.
глава 21 →Время жизни, память, ресурсы
🔒
Объект будет уничтожен, и при уничтожении освободит свои ресурсы
Гарантируется факт, не момент. Это ровно та строка, вокруг которой построен весь корень R3.
глава 09 →🔧
Уничтожение происходит немедленно при обнулении счётчика ссылок
Самая дорогая деталь во всём каноне: на неё опирается половина написанного Python-кода (open(…).read() без with). PyPy, GraalPy и Jython ведут себя иначе, и это не их баг.
глава 09 →🕳
Порядок финализации при выходе интерпретатора; вызов __del__ вообще
К моменту выхода модули частично разобраны. Никакая критичная работа не должна висеть на __del__ — только на with или atexit.
глава 10 →🔧
Пороги поколений (700, 10, 10) и сама трёхпоколенная схема
В 3.12 схему переработали. Идея «молодые проверяются чаще» осталась, цифры — нет.
глава 10 →🔧
Возврат памяти ОС через арены pymalloc; RSS как индикатор
Освобождение объекта и возврат памяти ОС — разные события. Отсюда: растущий RSS не доказывает утечку, а стабильный не доказывает её отсутствие.
глава 10 →🔒
with вызывает __exit__ при любом выходе из блока, включая исключение и return
Опираться смело. Это единственный механизм детерминированного освобождения, который язык действительно обещает.
глава 12 →🔒
__exit__, вернувший истинное значение, подавляет исключение
Полномочие, которого нет у деструктора в C++. Отсюда contextlib.suppress.
глава 12 →🔒
return или break в finally проглатывает летящее исключение
Зафиксированная семантика, а не баг. В 3.14 на это добавили предупреждение компилятора, но поведение не тронули.
глава 12 →Итерация, генераторы, async
🔒
Итератор одноразов, и iter(it) is it для любого итератора
Отсюда: функция, принимающая «последовательность», молча ломается на переданном генераторе, если проходит по нему дважды.
глава 13 →🔒
yield from пробрасывает send, throw и возвращаемое значение насквозь
Обычный цикл for x in gen: yield x этого не делает. На этой разнице стоит await.
глава 13 →🔧
Момент, когда брошенный генератор получит GeneratorExit и выполнит свой finally
Факт получения гарантирован, момент — нет: он наследует неопределённость момента разрушения. Та же логика, что с файлом без with.
глава 13 →🔒
Между двумя await код корутины исполняется без вытеснения
Отсюда главное свойство asyncio: гонок внутри одного шага нет, зато один синхронный вызов блокирует весь процесс.
глава 14 →Параллелизм
🔒
GIL не делает составные операции атомарными
x += 1, «проверить и записать», «прочитать и обновить» — гонки, несмотря на GIL. Правильный ответ всегда блокировка или очередь.
глава 19 →🔧
Список «атомарных операций», кочующий по интернету
Он описывает компилятор байткода конкретной версии. Опираться на него в коде нельзя — специализация в 3.11+ уже поменяла границы инструкций.
глава 19 →🔧
Интервал переключения потоков — 5 мс, настраивается sys.setswitchinterval
Значение по умолчанию и сама модель переключения — детали. Уменьшение интервала не делает код корректным, только меняет вероятность увидеть гонку.
глава 19 →🔧
numpy отпускает GIL в тяжёлых операциях — отсюда 1.9× на двух потоках
Отпускает не всякая функция, а мелкие операции не отпускают вовсе: накладные расходы больше выигрыша. Проверяется замером, а не верой.
глава 15 →🕳
fork из процесса, в котором есть потоки
В потомке остаются заблокированные локи, взятые несуществующими потоками. Отсюда зависания и переход на spawn по умолчанию.
глава 20 →🔧
Free-threading (PEP 703) как отдельная сборка интерпретатора
Не переключатель поведения, а отдельный ABI и отдельные колёса — прямое следствие R9. Расширения нужно пересобирать и проверять на потокобезопасность.
глава 19 →Импорт, типы, упаковка
🔒
Тело модуля исполняется ровно один раз за процесс, результат кэшируется в sys.modules
Отсюда: всё на верхнем уровне — глобальное состояние процесса, протекающее между тестами.
глава 17 →🔒
Объект модуля попадает в sys.modules до исполнения его тела
Именно поэтому циклический импорт даёт «partially initialized module», а не бесконечную рекурсию.
глава 17 →🔒
from x import y создаёт отдельную привязку в импортирующем модуле
Отсюда единственное правило патчинга: патчить по месту использования, а не по месту определения.
глава 17 →🔒
Аннотации не проверяются в рантайме; x: int в теле класса не создаёт атрибут
Аннотация — данные в словаре. Всё поведение dataclass и pydantic выводится из того, что кто-то эти данные читает.
глава 18 →🔧
Когда именно вычисляются аннотации и что лежит в __annotations__
PEP 563 так и не стал умолчанием, PEP 649 меняет модель снова. Код, читающий типы в рантайме, чувствителен к версии.
глава 18 →🕳
Совместимость ABI скомпилированного расширения между minor-версиями
Ломается каждый релиз, если не собрано под abi3. Отсюда матрица колёс и «пакет не ставится на новом Python».
глава 16 →Конвейер компиляции и машина исполнения
🔒
Тело модуля компилируется и исполняется, и результат кешируется — но что закешировано, языком не описано
Гарантирован факт кеширования и его прозрачность. Формат .pyc, magic number и содержимое — нет: .pyc от другой минорной версии просто игнорируется.
глава 22 →🔒
Имя, которому в функции где-либо присваивается значение, локально во всей функции
Решение принимает таблица символов при компиляции, статически, без анализа потока управления. Отсюда UnboundLocalError, global и nonlocal. Это спецификация, а не деталь.
глава 22 →🔒
Имя после except ... as удаляется на выходе из блока
Всегда и безусловно, даже если существовало до try. Сделано, чтобы трассировка не удерживала фрейм. Нужен объект дальше — присвоить другому имени внутри блока.
глава 23 →🔧
Валидация .pyc по времени модификации и размеру исходника
Умолчание, а не требование: PEP 552 даёт валидацию по хешу. Развернул код так, что mtime оказался старше записанного — исполнится старый байткод.
глава 22 →🔧
Набор опкодов, их номера, наличие инлайн-кешей, формат таблицы исключений
Меняется каждый релиз. 110 базовых опкодов и 71 специализированный — числа для 3.11; в 3.12 цикл вычисления генерируется из другого файла и набор другой.
глава 23 →🔧
try без броска не стоит ничего
Верно с 3.11 и только для CPython: обработчики вынесены в co_exceptiontable, которая читается лишь при исключении. До 3.11 вход в блок исполнял инструкцию, и бенчмарки тех лет показывают накладные расходы.
глава 23 →🔧
Вызов Python-функции не расходует C-стек
Появилось в 3.11: CALL подкладывает фрейм и продолжает тот же цикл. Поэтому 20 000 кадров рекурсии не роняют процесс — но рекурсия через C расходует стек по-прежнему, и там лимит защищает по-настоящему.
глава 23 →🕳
Момент, когда __del__ модульной глобальной переменной будет вызван при выходе
Модули к этому моменту частично разобраны, порядок не определён, вызова может не быть вовсе. atexit отрабатывает раньше и надёжнее; os._exit не даёт отработать ничему.
глава 23 →Производительность
🔧
Любое конкретное число в этом каноне
Всё измерено на CPython 3.11, Linux, x86-64. Порядок величины устойчив, конкретная цифра — нет. Мерить надо на своей версии и своих данных.
глава 21 →🔧
«List comprehension быстрее цикла с append»
На 3.11 разница 6% — совет устарел вместе с версией, для которой был написан. Пример того, как деталь реализации закрепляется в фольклоре.
глава 21 →
Откуда берётся эта таблица. Не из документации: там она не собрана в одном месте, и половина строк с 🔧 в документации отсутствует именно потому, что документировать деталь реализации — значит превратить её в обещание. Каждая строка выведена из механизма в соответствующей главе; если утверждение кажется спорным, спор надо вести с главой, а не с таблицей.