__pycache__: кэш компиляции Python
Каталог __pycache__ появляется сам и живёт по простым правилам: pyc свеж, пока у исходника прежние время и размер. Разбираем инвалидацию, очистку и отключение кэша.
Редакция Питоники
Каталог __pycache__ в проекте появляется без приглашения: первый же импорт модуля — и рядом с исходником лежит свежий pyc с magic-числом версии из пятого урока. Новички спрашивают, можно ли его удалить; опытные — когда он может наврать. Оба ответа следуют из одного устройства: это кэш, который интерпретатор сам создаёт, сам проверяет и сам пересобирает. Урок учит управлять этим механизмом, а не терпеть его.
Когда появляется каталог
Кэш пишется при импорте модуля — не при запуске скрипта. python app.py компилирует app.py и держит байткод в памяти: главный скрипт кэша не получает. А вот каждый импортированный модуль проекта честно компилируется один раз, и результат сохраняется:
with open('helper.py', 'w', encoding='utf-8') as f:
f.write('def twice(n):\n return n * 2\n')
import helper
import os
print('модуль работает:', helper.twice(21))
print('каталог кэша появился:', os.path.isdir('__pycache__'))
os.remove('helper.py')
модуль работает: 42 каталог кэша появился: True
Блок намеренно не запускается в песочнице: там импорт сработает и функция вернёт 42, но каталог не появится — запись кэша в браузерном интерпретаторе выключена. На локальной машине этот же код после первого запуска создаст __pycache__ с файлом helper.cpython-312.pyc (метка вашей версии). Именно поэтому главный файл проекта в кэше не ищут: его байткод живёт один запуск, а модулей — столько, сколько запусков.
В песочнице урока запись кэша при импорте отключена — но собрать pyc явно можно любым инструментом из третьего урока. Такой кэш в этой статье мы и разбираем на поля:
import glob
import os
import py_compile
with open('cached_demo.py', 'w', encoding='utf-8') as f:
f.write('VALUE = 10\n')
py_compile.compile('cached_demo.py')
print('каталог кэша существует:', os.path.isdir('__pycache__'))
print('pyc в кэше:', len(glob.glob('__pycache__/*.pyc')))
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('cached_demo.py')
каталог кэша существует: True pyc в кэше: 1
Инвалидация: время и размер
Кэш полезен, только пока он соответствует исходнику. Проверка свежести опирается на два поля заголовка pyc из второго урока: время изменения и размер исходника. При импорте интерпретатор сравнивает поля с текущим файлом — не совпало, значит исходник менялся, и компиляция выполняется заново. Посмотрим на поля своими глазами — сначала время:
import glob
import os
import py_compile
with open('stamp_demo.py', 'w', encoding='utf-8') as f:
f.write('VALUE = 10\n')
pyc = py_compile.compile('stamp_demo.py')
with open(pyc, 'rb') as f:
data = f.read()
stored = int.from_bytes(data[8:12], 'little')
print('время исходника сохранено в pyc:', stored == int(os.path.getmtime('stamp_demo.py')))
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('stamp_demo.py')
время исходника сохранено в pyc: True
Поле с 8-го по 12-й байт заголовка — unix-время исходника на момент компиляции, и оно совпало с os.path.getmtime файла. Второе поле — размер — даёт проверке второе измерение: время может соврать на файловых системах, где копирование сбрасывает метки, а размер врёт реже:
import glob
import os
import py_compile
with open('stale_demo.py', 'w', encoding='utf-8') as f:
f.write('VALUE = 10\n')
pyc = py_compile.compile('stale_demo.py')
with open(pyc, 'rb') as f:
old_size = int.from_bytes(f.read()[12:16], 'little')
with open('stale_demo.py', 'w', encoding='utf-8') as f:
f.write('VALUE = 10\nVALUE += 5\n')
new_size = os.path.getsize('stale_demo.py')
print('размер исходника изменился:', new_size != old_size)
print('старый pyc устарел:', old_size != new_size)
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('stale_demo.py')
размер исходника изменился: True старый pyc устарел: True
Мы дописали строку в исходник после компиляции — размер вырос, и кэш, собранный по старому содержимому, теперь числится устаревшим. Дальше события развиваются без нашего участия: при следующем импорте интерпретатор заметит несовпадение и молча перекомпилирует модуль. Никаких команд для обновления кэша вводить не надо.
import glob
import os
import py_compile
with open('fresh_demo.py', 'w', encoding='utf-8') as f:
f.write('VALUE = 1\n')
pyc = py_compile.compile('fresh_demo.py')
with open(pyc, 'rb') as f:
old_stamp = int.from_bytes(f.read()[8:12], 'little')
os.utime('fresh_demo.py', (1000000000, 1000000000))
new_stamp = int(os.path.getmtime('fresh_demo.py'))
print('время исходника изменилось:', new_stamp != old_stamp)
print('Python заметит такое обновление:', new_stamp != old_stamp)
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('fresh_demo.py')
время исходника изменилось: True Python заметит такое обновление: True
Функция os.utime передвинула время изменения исходника на 2001 год — содержимое то же, а вердикт изменился: pyc собран по свежему времени, исходник теперь «старше», проверка не сойдётся, и модуль будет перекомпилирован. Этим приёмом пользуются сборочные скрипты: коснуться времени файлов, чтобы кэш гарантированно пересобрался.
Управление кэшем
Кэш полностью управляем: создаётся любым из инструментов третьего урока, проверяется по полям заголовка и удаляется как обычный каталог. Соберём pyc и уберём за собой:
import glob
import os
import py_compile
with open('cleanup_demo.py', 'w', encoding='utf-8') as f:
f.write('VALUE = 1\n')
py_compile.compile('cleanup_demo.py')
before = len(glob.glob('__pycache__/*.pyc'))
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
after = len(glob.glob('__pycache__/*.pyc'))
print('pyc до очистки:', before)
print('pyc после очистки:', after)
os.remove('cleanup_demo.py')
pyc до очистки: 1 pyc после очистки: 0
Удаление кэша — не поломка: второй урок показывал, что pyc восстанавливается при первом же импорте. Кэш компиляции самодостаточен: удалили каталог — интерпретатор соберёт его заново при первом же импорте. Из-за этого каталог и исключают из git: репозиторий хранит исходники, кэш у каждой машины свой.
| Действие | Как | Результат |
|---|---|---|
| Отключить запись | python -B или переменная PYTHONDONTWRITEBYTECODE=1 | Импорты работают, pyc не создаются |
| Очистить кэш | Удалить каталоги __pycache__ в проекте | Пересоберётся при первом импорте |
| Пересобрать принудительно | python -m compileall с force | Все pyc пересоздаются |
| Проверить свежесть вручную | Сверить поля 8:16 заголовка с исходником | Тот же критерий, что у интерпретатора |
find . -type d -name __pycache__ -exec rm -rf {} +
python -m compileall -f мой_проект/
- кэш пишется при импорте модулей; главный скрипт запуска не кэшируется;
- свежесть проверяется по двум полям заголовка pyc: времени и размеру исходника;
- изменился исходник — pyc пересобирается сам при следующем импорте, команд не нужно;
- очистка безопасна: __pycache__ восстанавливается автоматически;
- запись отключается флагом -B или переменной PYTHONDONTWRITEBYTECODE, а не правкой кода.
Кэш — забота интерпретатора, и после этого урока — ваша тоже: вы знаете, когда он появляется, как проверяется и как им управлять. Дальше по разделу — marshal в седьмом уроке: формат, которым байткод упаковывается внутрь pyc.
Сначала предскажи ответ в голове — это главный навык программиста.
import glob
import os
before = len(glob.glob('__pycache__/*.pyc'))
if os.path.isdir('__pycache__'):
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
os.rmdir('__pycache__')
after = len(glob.glob('__pycache__/*.pyc'))
print(before >= 0, after)
import os
with open('quiz_mod.py', 'w', encoding='utf-8') as f:
f.write('V = 1\n')
size_before = os.path.getsize('quiz_mod.py')
with open('quiz_mod.py', 'a', encoding='utf-8') as f:
f.write('V += 2\n')
size_after = os.path.getsize('quiz_mod.py')
print(size_after != size_before)
os.remove('quiz_mod.py')
import os
import time
with open('quiz_time.py', 'w', encoding='utf-8') as f:
f.write('V = 1\n')
stamp = int(os.path.getmtime('quiz_time.py'))
print(isinstance(stamp, int))
os.remove('quiz_time.py')
1. Когда Python пишет pyc в __pycache__?
2. По каким полям интерпретатор решает, устарел ли pyc?
3. Исходник изменился. Что нужно сделать, чтобы обновить кэш?
4. Что произойдёт после удаления каталога __pycache__?
5. Как отключить запись pyc-файлов?
6. Почему __pycache__ не коммитят в git?
7. Замедляет ли python -B программу?
Соберите pyc файла book.py с содержимым PAGES = 120, прочитайте поле размера из заголовка и напечатайте, что оно совпадает с текущим размером файла. За собой уберите и pyc, и исходник.
Что такое __pycache__ и зачем он нужен?
Это каталог кэша компиляции: при импорте модуля Python сохраняет его байткод (pyc), чтобы при следующих запусках пропустить компиляцию. Свежесть проверяется по времени и размеру исходника; изменился исходник — pyc пересобирается сам.
Урок 2: устройство pyc-файлаМожно ли удалять __pycache__?
Да, безопасно: это только кэш. При первом же импорте Python пересоберёт его из исходников. Единственный эффект удаления — чуть более медленный первый запуск: компиляция модулей выполнится заново.
Почему __pycache__ появился, хотя я ничего не компилировал?
Компиляция в Python происходит всегда при импорте — это часть работы интерпретатора, а не отдельная команда. Каталог создаёт сам интерпретатор при первом импорте модулей проекта.
Урок 1: байткод и VMКак отключить создание __pycache__?
Запускайте интерпретатор с флагом -B или задайте переменную окружения PYTHONDONTWRITEBYTECODE=1. Импорты продолжат работать, компиляция останется — просто pyc не записывается на диск.
Может ли __pycache__ подсовывать устаревший код?
Только в экзотике: копирование проекта без времён файлов или совпавший размер при изменённом содержимом. Лечение простое — удалить каталог кэша; после пересборки вопрос закрыт. Классические IDE-сценарии «проверка сломалась» лечатся так же.
Понравился урок? Сошлитесь на него
«Кэш компиляции самодостаточен: удалили каталог — интерпретатор соберёт его заново при первом же импорте.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 2
pyc-файл: устройство скомпилированного файла Python
pyc — это кодовый объект, упакованный в байты: 16 байт заголовка с magic-числом, временем и размером исходника, дальше marshal-блок с байткодом. Разбираем файл до последнего байта.
Компиляция · Урок 3
py_compile и compileall: компилируем Python сами
Два инструмента стандартной библиотеки компилируют код без запуска: py_compile — один файл, compileall — целый каталог. Собираем pyc вручную и ловим ошибки компиляции.
Компиляция · Урок 5
Версии Python и magic-число: почему pyc не совместим
Каждая минорная версия CPython получает новое magic-число, и pyc одной версии не грузится другой. Находим число в importlib, сверяем с заголовком pyc и проверяем версию кодом.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Flask · Урок 8
Blueprint: организуем большое приложение на Flask
Восьмой урок курса Flask: Blueprint — мини-приложения внутри одного проекта. Регистрируем блюпринты, задаём префиксы адресов, разносим шаблоны по папкам и рефакторим блог из одного файла в модули.
флаг flask большой проект
re · Урок 10
Флаги в регулярных выражениях: re.IGNORECASE, MULTILINE, DOTALL, VERBOSE
Четыре флага, которые меняют поведение целого движка: поиск в любом регистре, якоря на каждой строке, точка с переводом строки и шаблоны с комментариями вместо «полотна».
инлайн флаг (?i)
openpyxl · Урок 3
Ячейки в openpyxl: адреса, значения и типы данных
Два способа попасть в ячейку: по адресу и по координатам. Числа против строк, ловушка «2026 как текст», очистка через None и чтение значений обратно.
очистить ячейку python