py_compile и compileall: компилируем Python сами
Два инструмента стандартной библиотеки компилируют код без запуска: py_compile — один файл, compileall — целый каталог. Собираем pyc вручную и ловим ошибки компиляции.
Редакция Питоники
В первом уроке мы выяснили, что компиляция происходит при каждом запуске и импорте, а второй урок разобрал устройство pyc-файла. Остался вопрос практики: когда компилировать нужно самим. Ответов три: проверка синтаксиса большого проекта без запуска ни одной строки, прогрев кэша перед выкладкой на сервер и сборка дистрибутива, где pyc едут вместе с исходниками. Компиляция без запуска — это проверка синтаксиса всего проекта за секунды: ни одна строка кода не исполнилась. Стандартная библиотека закрывает всё это двумя модулями: py_compile компилирует один файл, compileall — каталог целиком.
py_compile: один файл
Функция py_compile.compile принимает путь к исходнику, компилирует его и пишет pyc туда же, куда его положил бы обычный импорт — в __pycache__ рядом. Возвращаемое значение — путь к созданному файлу:
import glob
import os
import py_compile
with open('tool_demo.py', 'w', encoding='utf-8') as f:
f.write('def run():\n return 1 + 1\n')
pyc = py_compile.compile('tool_demo.py')
print('исходник на месте:', os.path.exists('tool_demo.py'))
print('pyc создан:', os.path.exists(pyc))
print('компилятор вернул строку-путь:', isinstance(pyc, str))
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('tool_demo.py')
исходник на месте: True pyc создан: True компилятор вернул строку-путь: True
Три факта о поведении: компиляция не трогает исходник, результат действительно появился и путь вернулся строкой. Имя файла внутри __pycache__ содержит метку версии интерпретатора, поэтому мы проверяем существование, а не печатаем путь: на CPython 3.12 и 3.13 хвосты имени разные. Если имя нужно своё — есть параметр cfile.
import os
import py_compile
with open('pack_demo.py', 'w', encoding='utf-8') as f:
f.write('VERSION = 1\n')
py_compile.compile('pack_demo.py', cfile='pack_compiled.pyc')
print('компиляция по своему пути сработала:', os.path.exists('pack_compiled.pyc'))
os.remove('pack_compiled.pyc')
os.remove('pack_demo.py')
компиляция по своему пути сработала: True
Параметр cfile отправил результат по указанному пути — файл появился в текущем каталоге, а не в __pycache__. Так собирают pyc для дистрибутивов с заранее известной структурой каталогов. Назвали файл сами — значит, и метку версии в имени можно не тащить: интерпретатор всё равно сверит magic-число из заголовка (пятый урок).
Ошибки компиляции: doraise
Главный сценарий ручной компиляции — проверка кода без запуска. Файл с ошибкой синтаксиса компилятор замечает мгновенно, но по умолчанию py_compile не считает это ЧП: он печатает ошибку в поток ошибок и возвращает None. Чтобы ошибка стала исключением, включают doraise=True:
import os
import py_compile
with open('cracked_demo.py', 'w', encoding='utf-8') as f:
f.write('x = (1 +\n')
try:
py_compile.compile('cracked_demo.py', doraise=True)
print('скомпилировалось без ошибок')
except py_compile.PyCompileError:
print('поймали PyCompileError: файл с ошибкой')
os.remove('cracked_demo.py')
поймали PyCompileError: файл с ошибкой
Незакрытая скобка превратилась в исключение PyCompileError, которое скрипт ловит и обрабатывает: так строятся проверки всего проекта в CI. Без doraise тот же файл проскочил бы молча — функция вернула None, и если код не проверяет возвращаемое значение, битый файл уехал бы в продакшен незамеченным.
compileall: каталог целиком
Для проекта из десятков файлов вызывать py_compile по одному — мучение. compileall.compile_dir проходит каталог рекурсивно и компилирует каждый .py файл. Возвращаемое значение — True, если всё скомпилировалось; посчитать результат можно любым способом, здесь — количеством pyc в кэше:
import compileall
import glob
import os
import shutil
shutil.rmtree('pkg_demo', ignore_errors=True)
os.makedirs('pkg_demo')
for name in ('alpha', 'beta', 'gamma'):
with open('pkg_demo/' + name + '.py', 'w', encoding='utf-8') as f:
f.write('N = 1\n')
result = compileall.compile_dir('pkg_demo', quiet=1)
print('каталог скомпилирован:', result)
print('pyc собрано:', len(glob.glob('pkg_demo/__pycache__/*.pyc')))
shutil.rmtree('pkg_demo')
каталог скомпилирован: True pyc собрано: 3
Три модуля — три pyc, по одному на каждый исходник. Параметр quiet=1 убрал стандартный дот-репорт компилятора (по точке на файл), чтобы вывод остался читаемым. Когда понадобится перекомпилировать всё принудительно, даже если кэш кажется свежим, — параметр force=True: по умолчанию compileall пропускает файлы, у которых pyc уже актуален.
import compileall
import glob
import os
import shutil
os.makedirs('single_demo', exist_ok=True)
with open('single_demo/report.py', 'w', encoding='utf-8') as f:
f.write('M = 2\n')
ok = compileall.compile_file('single_demo/report.py', quiet=1)
print('одиночный файл скомпилирован:', ok)
print('pyc в кэше модуля:', len(glob.glob('single_demo/__pycache__/*.pyc')))
shutil.rmtree('single_demo')
одиночный файл скомпилирован: True pyc в кэше модуля: 1
У compileall есть и функция для одного файла — compile_file. Она удобна, когда скрипт уже импортирует compileall, а отдельный py_compile ради одного вызова тянуть не хочется. Имя __pycache__ внутри single_demo — то же правило: кэш живёт рядом с исходником.
Командная строка
Оба модуля работают и из консоли — как скрипты через python -m. Это форма, в которой их чаще всего и используют в инструкциях по сборке:
python -m py_compile app.py
python -m compileall мой_проект/
python -m py_compile app.py settings.py utils.py
python -m compileall -q мой_проект/
| Инструмент | Объект | Типичный вызов | Особенность |
|---|---|---|---|
| py_compile | Один файл | py_compile.compile('app.py', doraise=True) | Ошибки — только с doraise |
| compileall | Каталог рекурсивно | compileall.compile_dir('pkg', quiet=1) | Пропускает свежий кэш; force=True пересобирает |
| python -m py_compile | Один файл из консоли | python -m py_compile app.py | Молчит при успехе |
| python -m compileall | Каталог из консоли | python -m compileall . | Печатает дот-репорт |
- py_compile.compile компилирует один файл и возвращает путь к pyc в __pycache__;
- cfile задаёт свой путь результата — удобно для сборки дистрибутивов;
- doraise=True превращает SyntaxError в PyCompileError: без него ошибка печатается в stderr и функция возвращает None;
- compileall.compile_dir компилирует каталог рекурсивно, compile_file — один файл, force=True игнорирует свежий кэш;
- из консоли работают обе формы: python -m py_compile и python -m compileall.
Ручная компиляция — про кэш и проверку синтаксиса. Дальше по разделу: как прочитать байткод глазами — дизассемблер dis в четвёртом уроке, почему pyc строго привязан к версии интерпретатора — пятый урок про magic-число, а про то, как интерпретатор сам управляет этим кэшем — шестой урок про __pycache__.
Сначала предскажи ответ в голове — это главный навык программиста.
import py_compile
with open('quiz_mod.py', 'w', encoding='utf-8') as f:
f.write('x = (2 +\n')
try:
py_compile.compile('quiz_mod.py', doraise=True)
print('ok')
except py_compile.PyCompileError:
print('fail')
import compileall, glob, os
os.makedirs('quiz_pkg', exist_ok=True)
for name in ('a', 'b'):
with open('quiz_pkg/' + name + '.py', 'w', encoding='utf-8') as f:
f.write('N = 1\n')
compileall.compile_dir('quiz_pkg', quiet=1)
print(len(glob.glob('quiz_pkg/__pycache__/*.pyc')))
import shutil
shutil.rmtree('quiz_pkg')
import py_compile
res = py_compile.compile('no_such_file.py', doraise=True)
print('готово')
1. Куда py_compile.compile кладёт результат по умолчанию?
2. Что делает doraise=True в py_compile.compile?
3. Чем compileall.compile_dir отличается от py_compile.compile?
4. compileall пропустил файл при повторном запуске. Почему?
5. Как скомпилировать каталог проекта из консоли?
6. Исполняет ли py_compile.compile код из файла?
7. Зачем прогревают кэш командой python -m compileall перед деплоем?
Скомпилируйте файл notes.py с содержимым TITLE = 8 по своему пути build_notes.pyc (параметр cfile) и напечатайте, что файл собран. За собой уберите и pyc, и исходник.
Как скомпилировать Python-файл в pyc?
Один файл — py_compile.compile('app.py') из стандартной библиотеки или в консоли python -m py_compile app.py. Каталог целиком — compileall.compile_dir('проект/') или python -m compileall. Результат — pyc в __pycache__ рядом с исходником.
Урок 2: устройство pyc-файлаКак проверить синтаксис Python-файла, не запуская его?
Скомпилировать: py_compile.compile('app.py', doraise=True) — код не исполняется, но ошибки синтаксиса поднимутся как PyCompileError. Для всего проекта — compileall.compile_dir с doraise: ни одна строка не выполнится.
Зачем запускать python -m compileall перед выкладкой?
Это прогрев кэша: все модули компилируются один раз на машине сборки, и на сервере импорты сразу идут по готовым pyc. Критично на холодном старте и там, где каталоги read-only и записать pyc на месте невозможно.
py_compile.compile молча вернул None — что не так?
Файл содержит SyntaxError: без doraise=True py_compile печатает ошибку в stderr и возвращает None, не бросая исключений. Добавьте doraise=True — получите PyCompileError, который можно поймать и показать.
Можно ли распространять программу как набор pyc вместо исходников?
Технически да — pyc той же версии Python импортируется без исходника. Но это не защита: декомпиляторы для каждой версии восстанавливают большую часть кода. Для сборки самостоятельных программ есть PyInstaller и Nuitka — им посвящены уроки 10-12.
Урок 10: PyInstallerПонравился урок? Сошлитесь на него
«Компиляция без запуска — это проверка синтаксиса всего проекта за секунды: ни одна строка кода не исполнилась.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 2
pyc-файл: устройство скомпилированного файла Python
pyc — это кодовый объект, упакованный в байты: 16 байт заголовка с magic-числом, временем и размером исходника, дальше marshal-блок с байткодом. Разбираем файл до последнего байта.
Компиляция · Урок 4
dis: дизассемблер байткода Python
dis печатает байткод функции инструкция за инструкцией: какая команда, над какими значениями, куда прыгает условие. Учимся читать колонки вывода и понимать стековую машину.
Компиляция · Урок 6
__pycache__: кэш компиляции Python
Каталог __pycache__ появляется сам и живёт по простым правилам: pyc свеж, пока у исходника прежние время и размер. Разбираем инвалидацию, очистку и отключение кэша.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
pytest · Урок 2
Как pytest находит тесты: конвенции имён файлов и функций
pytest не читает мысли: тестом становится только то, что названо по конвенции — test_*.py, test_-функции, классы Test*. И адрес одного теста: файл::тест.
pytest запуск файла с тестами
pytest · Урок 11
Встроенные фикстуры: capsys и tmp_path
Половину бытовых задач тестирования закрывают две фикстуры из коробки: capsys читает то, что функция напечатала, tmp_path выдаёт чистую временную папку — без декораторов и настроек.
pytest тест файла
Компиляция · Урок 1
Как Python исполняет код: байткод и виртуальная машина
Питон исполняет программу в два этапа: компилятор переводит исходник в байткод, виртуальная машина выполняет его по инструкциям. Достаём оба этапа на поверхность и вызываем компилятор сами.
python компиляция