Skip to main content

pyc_revive

.pyc → .py reconstructor. Core pipeline is pure stdlib — no uncompyle6, no pycdc, no xdis. A bundled disassembler handles foreign versions; Node.js + depyo handles 3.14 and 3.15.

install

pip install pyc-revive

The Python package itself has no third-party dependencies. Node.js and depyo are only needed for the 3.14+ backend, and they're installed on demand.

On first run, pyc-revive checks for Node.js and depyo and installs them if missing. Everything goes under %LOCALAPPDATA%\pyc_revive on Windows, or the equivalent user directory elsewhere. No admin rights, no system changes.

  • Node.js ≥ 20 via winget (user scope) if available, otherwise a portable ZIP extracted into the user directory. No MSI, no registry writes.
  • depyo via npm into a user-writable prefix.
  • optional pip backends (pychd, byteripper, uncompyle6, decompyle3) installed individually with a --user fallback.
# run installer explicitly
pyc-revive --install-deps

# skip first-run bootstrap entirely
pyc-revive --no-bootstrap Backdoor.pyc

From a checkout:

python -m pyc_revive my file.pyc
python -m pyc_revive ./dist/app.pyc --out ./revived.py
python -m pyc_revive --install-deps

Input handling: exact path, spaces with no quotes, or basename search in the current directory, all subfolders, and the parent folder.

versions

The magic-number table covers every CPython release from 2.0 through 3.15, with nearest-lower fallback for unrecognized magics and correct header sizes per version (pymagic.py). Practical testing to date has focused on 3.9 through 3.15.

Three backends, one shared pipeline:

  • native — .pyc magic matches the running interpreter. stdlib marshal loads the code object, stdlib dis disassembles it. No external dependencies at all.
  • pycdas — anything else from 2.x through 3.13. A bundled disassembler emits a text dump; pycdas_parser.py parses it into code trees; shim.py adapts them to look like real code objects. The rest of the pipeline (normalize, segment, translate, CFG, emit, verify ×3) runs unchanged on the result.
  • depyo — 3.14 and 3.15. depyo_parser.py runs depyo --skip-source-gen --asm and parses the .pyasm dump; depyo_postprocess.py repairs depyo's mechanical emitter bugs using the pycdas pipeline's output as a repair source when both ran. When the pipeline's output doesn't compile, a fallback path produces source from depyo's own decompiler.

Routing is decided by magic number before any bytecode is touched. A foreign magic goes straight to the appropriate backend. marshal.loads can succeed on foreign payloads because the marshal format is stable across versions — and then stdlib dis mis-decodes unknown opcodes and can crash the interpreter instead of raising. So "try native, fall back on error" is deliberately not used for foreign files.

benchmark

Reference corpus: 500 randomly sampled .py files drawn from PyPI ecosystem packages — PyTorch internals, sympy, pandas, Django, telegram client libraries, gevent, trio, PyInstaller hooks, CPython's own Compiler/ directory, generated parser tables, and one 918 KB data module. Compiled with Python 3.11.2, which is one of the hardest modern targets: exception tables, PRECALL/CALL split, adaptive specialization, CACHE entries between instructions.

Measured with a byte-exact verification harness: compile source → .pyc → decompile with pyc-revive → recompile the output → recursive instruction-level comparison against the original (opcode, arg, argval, co_names, co_varnames, co_consts, and nested code objects).

result files percent
byte-equivalent 127 25.4%
structurally equivalent (opcode-name match) 126 25.2%
not equivalent 247 49.4%
total runtime 500 files 67 seconds

That's approximately 130 ms per file including process spawn, source compilation, tool invocation, and comparison.

Nothing is skipped. The corpus is the full random sample.

Honest context on the number. This corpus is not representative of typical Python code. It is deliberately weighted toward categories that are hard for any decompiler: code generators, machine-generated tables, metaclass-heavy object systems, async generator machinery, and annotation-dense auto-generated API stubs. A corpus of hand-written application code would score higher. A corpus compiled with Python 3.8 would score higher. The number above is what you get when you stack every hard case next to each other on the worst realistic version target.

Trajectory. The byte-equivalent rate roughly doubled (12.2% → 25.4%) in a single development session with no change in runtime. The structural rate went from 9.4% to 25.2% over the same period. The tool is under active development and the ceiling has not been reached.

To reproduce the number, the harness is pyc_strict_bench.py in the source repository.

3.14 and 3.15 support

3.14 was the first foreign 3.11+ target exercised end-to-end, and it required real work beyond extending the opcode table.

Translator additions:

  • LOAD_SMALL_INT, LOAD_FAST_BORROW, LOAD_FAST_BORROW_LOAD_FAST_BORROW (double 4-bit index, high nibble first), LOAD_SPECIAL, FORMAT_SIMPLE, TO_BOOL (transparent), CALL_KW.
  • BINARY_OP opargs ≥ 26 (3.14 renumbers the table; 26 = NB_SUBSCR, 27 = inplace).
  • KW_NAMES — pycdas 3.13 sometimes prints it as LOAD_CONST <i> // Value: ('a', 'b'), a tuple of key names. shim.py rewrites the opname when the argval is a string tuple immediately followed by a CALL_KW.

Parser additions (pycdas_parser.py):

  • [Locals+Names] sections read into co_varnames.
  • [Exception Table] entries parsed and re-encoded to co_exceptiontable bytes on the shim, so the shared try/with-region machinery runs unchanged.

Shim additions (shim.py):

  • 3.14 conditional-branch targets from the bundled disassembler land 2 bytes low. The disassembler suppresses a hidden CACHE but doesn't account for it in target math. Repaired when the fingerprint is exact: a 3.14+ dump, NOT_TAKEN at branch + 4, and target + 2 a real instruction offset.
  • 3.14 replaced BEFORE_WITH with an inline prologue (COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL). The shim folds it back to one synthetic BEFORE_WITH so the shared with-region machinery applies.
  • LOAD_FAST_BORROW index resolution, including the pair form.

Region detection:

  • with bodies are scoped by their exception-table range, so fallthrough code after the body dedents correctly.
  • try interiors stop at the body end, so post-try joins don't indent under try:.
  • Post-try joins stranded textually before out-of-line handlers are deferred past the except clause.
  • RERAISE-ended handlers that pair with nothing are skipped as implicit-cleanup residue.
  • Duplicated finally bodies are paired by twin match: an unpaired RERAISE-ended handler whose leading statements duplicate a post-try block's leading statements is the exceptional finally copy, and the tails decide the split.

3.15 goes through the same depyo path. depyo's unresolved name masks (##NAME_N##, ##ERROR##, __common_const_N__) are collapsed or substituted in depyo_postprocess.py. stdlib imports that depyo drops are reinserted from a canonical table.

pipeline

  1. find file (finder.py)
  2. detect version via magic (pymagic.py)
  3. load: native marshal, else pycdas or depyo backend (loader.py, pycdas_parser.py, depyo_parser.py, shim.py)
  4. disassemble and abstract (bytecode.py: Inst, EditableBytecode, opcode/oparg/value-stack/metadata tables)
  5. normalize and mask constants and names (normalizer.py)
  6. opcode-aware B/I/E segmentation (segmenter.py)
  7. statement translation (translator.py: stack machine, defaults recovery, inline lambdas, 2.x print/exec/class ops)
  8. control-flow graph from jumps (cfg.py: if/for/while/try/except-as including bare except:/with/finally, clause and cluster detection, 3.11+ exception table, SETUP-region splits)
  9. emit and repair (emitter.py: source-order splicing onto def/class stub lines, base-class recovery, decorator restoration, global declarations, condition chains, ternaries, nested if/else, loop ranges, break/continue recovery, indent and suite-guard passes)
  10. equivalence verification ×3 sweeps (verifier.py: recompile revived source, compare opcode structure, repair, repeat)
  11. self-validation (autofix.py: compile the output, diagnose the first error, apply a targeted repair — orphan handler, empty suite, bad indent, orphan try: — until it compiles or the budget runs out; ships the closest parse)
  12. backend fallback (reconstructor.py: if the pipeline's output doesn't compile and depyo is available, depyo-direct runs with the pipeline's source as a repair companion, then post-processing fixes mechanical bugs)

layout

pyc_revive/
  finder.py            file resolution
  pymagic.py           magic-number table + version detection
  loader.py            native marshal loading
  pycdas_parser.py     bundled-disassembler dump parser
  depyo_parser.py      depyo --asm dump parser
  depyo_postprocess.py repair pass for depyo output
  shim.py              foreign-version code trees behave like code objects
  bytecode.py          Inst + EditableBytecode
  normalizer.py        const/name masking
  segmenter.py         B/I/E opcode segmentation
  translator.py        opcode stream -> statements
  cfg.py               basic blocks + structured control flow
  emitter.py           source emission + structural repairs
  verifier.py          recompile + compare x3
  autofix.py           compile-repair loop
  reconstructor.py     orchestrator
  cli.py               interface
  bootstrap.py         first-run dependency check
  install_deps.py      Node + depyo + pip backend installer
  deps.py              optional-backend reporter
  debug_log.py         verbose logging (PYC_REVIVE_VERBOSE=1)
  __main__.py
  pycdas.exe           bundled disassembler (Windows)

what this is and isn't

What it is: a bytecode-to-source reconstructor. It takes a compiled Python file and produces Python source that, when recompiled, matches the original as closely as the pipeline can get. It runs offline, does not execute the input, and does not require the input to be safe to open.

What it isn't: a malware detector. It does not scan files, does not flag behavior, and does not tell you whether a file is dangerous. It reconstructs source from bytecode you already have. In a malware-analysis workflow it comes after something else extracts the .pyc from an executable or archive — it is the step that turns that .pyc back into readable source.

What it will do to your machine: nothing on its own. The pipeline compiles the revived source to check that it parses (compile()), but never executes it (exec() is not called anywhere in the pipeline). If you run it on a hostile .pyc, it will try to decompile it and will not run any of the code inside it.

What it will not do for you: guarantee a reconstruction is safe to run. If you decompile malware and then run the output yourself, that's on you. The tool never runs the source it produces.

known limits

  • Inline comprehensions and generator expressions render as func(...) placeholders in some cases.
  • try bodies that fall through into always-run handlers keep flat order instead of guarded flow, unless an exception table is available. Native 3.11+ and foreign dumps with [Exception Table] sections scope bodies, pair handlers, and defer stranded post-try joins properly.
  • Exotic decorators reduce to bare defs. Exotic call shapes degrade to commented disassembly rather than failing.
  • Pre-3.8 default-argument recovery is best-effort. Flag-style MAKE_FUNCTION from 3.8 onward recovers exactly.
  • Verifier scores dip across versions: opcode sets drift between releases, so cross-version similarity reads lower than same-version.
  • 3.14 FOR_ITER targets are untested. No loop has appeared in any 3.14 sample so far. If the bundled disassembler hides caches after FOR_ITER the same way it does for conditional branches, loop joins may land 2 bytes off.
  • The bundled pycdas.exe is a Windows binary. On Linux and macOS the native and depyo backends still work, but foreign-version support through pycdas requires a platform-appropriate build of that disassembler, which is not currently shipped.

changelog

1.0.3 — depyo backend wired end-to-end. First-run bootstrap installs Node ≥ 20 and depyo without admin rights. --install-deps and --no-bootstrap flags added. depyo_postprocess.py added: drops unresolved name masks, recovers dropped imports from companion source, strips orphan returns, fixes wrong from X import Y. 3.14 rc1 magic added (rc1 and rc3 have different magic bytes). Byte-exact rate on the reference corpus: 12.2% → 25.4%.

1.0.2 — 3.14 support beyond the opcode table. Exception-table parsing, [Locals+Names] sections, hidden-CACHE target repair, BEFORE_WITH folding, region scoping from exception tables.

1.0.1 — first PyPI release. Native and pycdas backends, versions 2.0–3.13 through the magic table, practical testing focused on 3.9–3.13.

license

MIT

Metadata

Release files for pyc-revive 1.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyc-revive 1.0.3
File Size Uploaded
pyc_revive-1.0.3.tar.gz 119.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyc-revive 1.0.3
File Interpreter ABI Platform
pyc_revive-1.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 241.7 kB

Release files / pyc_revive-1.0.3.tar.gz

Download URL pyc_revive-1.0.3.tar.gz
Size 119.1 kB
Tags Source
SHA-256 checksum
How to use checksums
19954a9ba522096f834f36dd881c1f9c93f4c1829c45790b9f7f64a25532231c
BLAKE2b-256 checksum
How to use checksums
ade7c5200eb9a8fbd1e77cb433b56a927b54d1d0904a1936fdf45e7bc033b1ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.2

Release files / pyc_revive-1.0.3-py3-none-any.whl

Download URL pyc_revive-1.0.3-py3-none-any.whl
Size 122.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4e931b3a346ee6fcb94622ab7aa75e6e670423b241d3126be9daf0b52d4372c8
BLAKE2b-256 checksum
How to use checksums
3cd6382bf303644c18a332f74b087abf0af2d1d56cc4df97d124017d3d744f9e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.2

Release history Release notifications | RSS feed

This release

1.0.3 This release

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page