Python
Python is disrobe's most contested and most developed ecosystem. It ships an in-house Rust decompiler as the product, never a wrapper around pycdc, pylingual, decompyle3, or uncompyle6 (those are benchmark competitors, available only as optional --backend fallbacks).
At a glance
| Layer | Coverage |
|---|---|
| Bytecode disassembly | CPython 1.0-3.15, PyPy, MicroPython .mpy v0-v6, Jython, IronPython, Brython |
| Decompilation | In-house engine across CPython 1.0-3.15 with per-version opcode dispatch; 92.76% per-code-object recompile-equivalence on a pinned 200-module CPython 3.14 stdlib corpus (5831 of 6286, above a 90% CI floor), and the legacy 1.0-3.7 band asserts a CI floor of 152 of 191 proven-correct (67 by recompile-equivalence, the rest by structural token-match) |
| Modern constructs | match, walrus, f-strings and PEP 750 t-strings, exception groups, PEP 695/696/709 |
| Freezers | PyInstaller 2.x-6.20+, Nuitka, cx_Freeze, py2exe, PyOxidizer, shiv, pex, Briefcase, SourceDefender |
| Protectors | PyArmor v6-v9-pro and 18 source obfuscators with an AST-evaluator backend |
Decompiling .pyc
disrobe py decompile module.pyc --out recovered/
disrobe py decompile module.pyc --out recovered/ --backend native # default; deterministic, no external tools
disrobe py decompile module.pyc --out recovered/ --emit source,disasm,ast
The default native backend is the in-tree engine: it runs a frame-tree pre-pass, per-version opcode dispatch, and then round-trip verification. The optional --backend pycdc|decompyle3|uncompyle6 flags shell out to those external tools (which must be on PATH) purely for benchmark comparison; they are never the default.
How the in-house engine works
- Frame-tree pre-pass. Before walking instructions, the engine reconstructs the nested source-construct tree from the 3.11+ exception table. This eliminates the single-pass stack-walker desync that causes other decompilers to mis-nest try/except and with-blocks.
- Provably-inert normalizations. Twelve normalizations (padding, super-instruction fusion, constant-pool ordering, and more) run before the round-trip check, each gated by an adversarial test proving it masks no real bug.
- Round-trip metric. Every emitted file is recompiled on the matching interpreter and compared opcode-for-opcode against the original.
PERFECTis byte-identical;SEMANTICis the same program with a different layout;CODE_DIFFflags a real bug that is fixed before ship. The normalizer preserves jump-condition polarity rather than collapsing all jumps, so an inverted condition reads as aCODE_DIFFinstead of passing silently.
Measured equivalence
The per-code-object figure is measured against an independent oracle, not the tool's own output: each recovered module is recompiled on CPython 3.14.5 and its code objects are diffed against the originals. On a pinned 200-module stdlib corpus (6286 code objects) the rate is 92.76% (5831 of 6286), above a 90% floor a committed CI gate enforces (arbitrary_recompile_gate.rs). uncompyle6 stops near 3.8 and decompyle3 near 3.9; the ML-based decompilers self-flag benchmark contamination, and there is no model here to contaminate.
Disassembling
disrobe py disasm module.pyc --out trace.txt
A faithful per-instruction trace across every supported interpreter dialect. This is the Disasm rung: lossless, offset-preserving, no structural reconstruction.
Deobfuscating source
disrobe py deob obfuscated.py --out clean.py
disrobe py deob obfuscated.py --out clean.py --cleanup
Peels source-level obfuscator wrappers (Kramer/Specter, Berserker, Jawbreaker, BlankOBF, PlusOBF, Wodx, pyobfuscate.com, PyObfuscator (mauricelambert), python-obfuscator (PyPI), ObfuXtreme, Manglify, Oxyry, pyminifier, online obfuscator family, Xindex, pyobfus, Pypacker, Patchwork) with an AST-evaluator backend. --cleanup runs a ruff-AST constant-fold and dead-branch-elimination pass afterward.
Freezers and packagers
disrobe pyinstaller extract onefile.exe --out out/ # PyInstaller 2.1 .. 6.x, AES-CTR/CFB decrypt
disrobe pyinstaller detect onefile.exe # cookie, Python version, TOC offsets, no extract
disrobe pyfreeze extract app.exe --out out/ # cx_Freeze / py2exe / shiv / pex / PyOxidizer / Briefcase
disrobe nuitka detect app.exe # flavor + Python version
disrobe nuitka extract app.exe --out out/ # --onefile payload (zstd)
disrobe nuitka symbols app.exe # impl_* + module-init scan on --standalone builds
disrobe py sourcedefender app.pye --out app.msgpack # SourceDefender .pye decrypt
PyArmor
disrobe pyarmor unpack protected.py --out out/
Unpacks a PyArmor wrapper back to its original .pyc. v8 and v9-pro are handled by a pure-static path (no code execution). v6/v7 can optionally use a dynamic-hook fallback that runs the obfuscated wrapper in a watched subprocess to capture marshal streams; this is opt-in and unsafe on untrusted input:
disrobe pyarmor unpack protected.py --out out/ --allow-dynamic --dynamic-timeout 60
The
--allow-dynamicpath executes the sample. Only enable it on trusted samples or inside an isolated sandbox. See Forensics and malware-safety posture.
Other useful flags: --mode auto|standard|super, --target 3.11 (rewrite emitted .pyc magic), --allow-bcc (BCC native-body lift via Ghidra-headless), --strict (exit non-zero on any partial decode), and --all-emits.
End-to-end
A real-world Python sample is often frozen, then protected, then compiled. disrobe auto chains the whole stack:
disrobe auto suspect.exe --out recovered/ # PyInstaller -> PyArmor -> .pyc decompile