
Декомпилирует и дизассемблирует байт-код Python (.pyc/.pyo) обратно в читаемый исходный код. Поддерживает Python 2.0 - 3.15 (CPython, PyPy и другие реализации). Написан на Rust.
Английский | 简体中文
Декомпилируйте и дизассемблируйте байт-код Python (.pyc) от Python 2.0 до 3.15 обратно в читаемый исходный код — написан на Rust, три зависимости, Python не требуется во время выполнения.
pycdc program.pyc # decompile to stdout
pycdas program.pyc # disassemble (exception tables, line numbers, nested code)
--opcodesHomebrew (macOS и Linux):
brew install ejfkdev/tap/pycdc
cargo, из crates.io:
cargo install pycdc
Готовые бинарные файлы: необработанные исполняемые файлы (без обёртки архива) на странице Releases — Linux / Windows x86_64 + arm64, macOS arm64 + x86_64; сборки для Linux и Windows сжаты UPX, для macOS поставляются без сжатия.
Из исходного кода:
cargo build --release # -> target/release/pycdc + pycdas
cargo install --path . # or install both binaries from a checkout
Каждый метод предоставляет как pycdc, так и pycdas.
# single file -> stdout, a file, or a directory
pycdc program.pyc > program.py
pycdc program.pyc -o program.py
pycdc program.pyc -o ./outdir/
# stdin (pipelines): - reads the pyc from stdin
cat program.pyc | pycdc -
# batch: mirror a directory tree (parallel by default, -j N to control,
# -q suppresses the per-file src -> dst lines)
pycdc ./pyc-corpus -o ./src-out
pycdc ./pyc-corpus # -> ./pyc-corpus-decompiled/
pycdc a.pyc b.pyc # -> a.py, b.py next to the inputs
pycdc ./pyc-corpus -o ./src-out -q -j 8
# raw marshal (no pyc header) with an explicit version
pycdc -v 3.8 payload.marshal
# disassembly — same CLI surface (files, dirs, stdin, -o/-j/-q);
# also reachable as the `pycdc dis` subcommand (identical output)
pycdas program.pyc
pycdc dis program.pyc
pycdas ./pyc-corpus -o ./dis-out # -> mirrored .dis files
pycdc --help, pycdc dis --help и pycdas --help документируют каждый
флаг. Коды выхода: 0 — успех, 1 — не удалось обработать файл, 2 — ошибка
использования.
Apple Silicon, 526 реальных файлов .pyc (~9,5 МБ, stdlib Python 2.6–3.14):
| Сценарий | Время | Пиковый RSS |
|---|---|---|
| Пакетный, параллельный (по умолчанию) | ≈0,05 с | ≈25 МБ |
Пакетный, последовательный (-j 1) | ≈0,34 с | ≈13 МБ |
| Крупнейший одиночный файл (74 КБ, включая запуск) | ≈5,5 мс | ≈4,4 МБ |
| Запуск процесса | ≈2 мс | — |
tools/fetch_realworld.sh выполняет sparse-checkout одного каталога из каждого из
семи проектов на его последнем теге релиза, компилирует каждый .py в
.pyc (compileall -b) и размещает дерево в tests/realworld/
(игнорируется git — скрипт является способом его воссоздать):
| Проект (тег) | модули | последовательный -j 1 | параллельный (по умолчанию) | перекомпиляции | AST-идентичные¹ |
|---|---|---|---|---|---|
| yt-dlp (2026.08.19) | 1045 | 0,46 с | 0,08 с | 1045/1045 | 501/1045 |
| matplotlib (v3.11.2) | 253 | 0,33 с | 0,06 с | 253/253 | 60/253 |
| pandas (v3.0.5) | 1420 | 0,90 с | 0,14 с | 1420/1420 | 584/1420 |
| django (6.1.1) | 907 | 0,38 с | 0,10 с | 907/907 | 468/907 |
| sympy (1.14.0) | 1532 | 3,03 с | 1,63 с | 1532/1532 | 479/1532 |
| scikit-learn (1.9.1) | 671 | 0,48 с | 0,07 с | 671/671 | 216/671 |
| ansible (v2.21.4) | 583 | 0,27 с | 0,05 с | 583/583 | 185/583 |
| итого | 6411 | 5,85 с | 2,13 с | 6411/6411 (100 %) | 2493/6411 |
¹ строго: AST с удалёнными docstring, равное исходному коду —
эквивалентные нормализации (if/else вместо тернарного оператора, избыточные
*()/**{} вокруг распаковки, переупорядоченные именованные аргументы) считаются
различиями, поэтому рассматривайте это как нижнюю границу точности формы, а не как
частоту ошибок. Столбец parse/recompile — это критерий приёмки.
Один процесс pycdc на проект: ~0,9 мс на модуль последовательно, пиковый RSS
≈81 МБ для всего пакета из 6,4 тыс. модулей. Все 6411 модулей перекомпилируются;
последнее семейство (словарное включение, вложенное в элемент генераторного
выражения, включая аргументы вызова) было закрыто пакетом PR от сообщества. Сырые данные: benchmarks/realworld.json,
стенд: tools/bench_realworld.py.
Три независимых стенда, все запускаются локально (нужны интерпретаторы pyenv/uv 2.6–3.15):
| Стенд | Что он доказывает | Текущий результат |
|---|---|---|
tools/verify_corpus.py | 528 модулей stdlib: декомпиляция → перекомпиляция тем же интерпретатором → сравнение сигнатур байткода, с откатом к diff нормализованного AST | 528/528 семантически (406 сигнатурно точных, 0 синтаксических ошибок, 0 заглушек) |
tools/run_behavior.py | 72 поведенческих случая × 14 интерпретаторов: запуск оригинала и декомпилированного, сравнение stdout + кода выхода | 663/663 = 100% (2.6–3.15) |
tests/roundtrip.py | матрица фикстур: компиляция → декомпиляция → перекомпиляция → строгое сравнение байткода | 49/54 строго (фикстуры исключений структурно отличаются, семантически эквивалентны) |
Результат для 3.15 отражает CPython 3.15.0/3.15.1, в которых изменились
кодировки inline-включений, вызовов и импортов; его таблица опкодов генерируется из
работающего интерпретатора. Все поведенческие случаи проходят, а pandas+scikit-learn перекомпилируются
2091/2091 под 3.15; оставшийся крайний случай с возвратом в теле with задокументирован
в docs/LIMITATIONS.md.