Skip to main content

pyc_revive

.pyc -> .py reconstructor. no uncompyle6. no pycdc. no xdis.

use

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

input handling: exact path, spaces with no quotes, basename search in cwd + all subfolders + parent folder.

versions

every CPython from 2.0 through 3.15 resolves by magic number (pymagic.py, full table + nearest fallback + header sizes).

two backends, one pipeline:

  • native: .pyc matches the running interpreter. stdlib marshal loads the code object, stdlib dis disassembles. zero dependencies.
  • pycdas: anything else. the bundled pycdas.exe disassembles the foreign version, pycdas_parser.py parses the dump into code trees, shim.py adapts them to the pipeline. everything downstream (normalize, segment, translate, CFG, emit, verify x3) runs unchanged.

routing is by magic number, decided before any bytecode is touched: a foreign magic goes straight to pycdas. marshal can load foreign payloads without complaint (the format is stable), and letting stdlib dis chew on foreign opcodes mis-decodes jumps and can kill the process instead of raising -- so "try native, fall back on error" is not used for foreign files. verified live on 3.14 (see below).

3.14 support

3.14 was the first foreign 3.11+ target exercised end to end, and it needed real work beyond the opcode table:

  • new ops in the translator: 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 (kwargs split like CALL_FUNCTION_KW, NULL-skipped).
  • pycdas_parser.py reads [Locals+Names] sections into varnames and parses [Exception Table] entries (S to E -> T [depth] (lasti?)), which are re-encoded to co_exceptiontable bytes on the shim so the shared try/with-region machinery runs unchanged.
  • the bundled pycdas prints 3.14 conditional-branch targets 2 bytes low (hidden CACHE it suppresses but forgets in target math -- proven three ways: dead else bodies, inverted with-cleanup, stack-underflow RETURN). shim.py repairs them when the fingerprint is exact (3.14+ dump, NOT_TAKEN at branch+4, target+2 is a real offset).
  • 3.14 dropped BEFORE_WITH for an inline prologue (COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL); the shim folds it back to one synthetic BEFORE_WITH.
  • with-bodies are scoped by their table range (fallthrough code after the body dedents instead of hiding under with), try interiors stop at the body end (post-try joins don't indent under try:), and 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 pair by twin match: an unpaired RERAISE-ended handler whose leading statements duplicate a post-try block's leading statements is the exceptional finally copy (tails decide the split -- [.., return] vs [.., raise] means the return is post-statement). The normal copy splits into finally: + rest. the body dedents instead of hiding under with), try interiors stop at the body end (post-try joins don't indent under try:), and 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.

routing is by magic number, decided before any bytecode is touched: a foreign magic goes straight to pycdas. marshal can load foreign payloads without complaint (the format is stable), and letting stdlib dis chew on foreign opcodes mis-decodes jumps and can kill the process instead of raising -- so "try native, fall back on error" is not used for foreign files. verified live on 3.14 (see below).

3.14 support

3.14 was the first foreign 3.11+ target exercised end to end, and it needed real work beyond the opcode table:

  • new ops in the translator: 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 (kwargs split like CALL_FUNCTION_KW, NULL-skipped).
  • pycdas_parser.py reads [Locals+Names] sections into varnames and parses [Exception Table] entries (S to E -> T [depth] (lasti?)), which are re-encoded to co_exceptiontable bytes on the shim so the shared try/with-region machinery runs unchanged.
  • the bundled pycdas prints 3.14 conditional-branch targets 2 bytes low (hidden CACHE it suppresses but forgets in target math -- proven three ways: dead else bodies, inverted with-cleanup, stack-underflow RETURN). shim.py repairs them when the fingerprint is exact (3.14+ dump, NOT_TAKEN at branch+4, target+2 is a real offset).
  • 3.14 dropped BEFORE_WITH for an inline prologue (COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL); the shim folds it back to one synthetic BEFORE_WITH.
  • with-bodies are scoped by their table range (fallthrough code after the body dedents instead of hiding under with), try interiors stop at the body end (post-try joins don't indent under try:), and 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 pair by twin match: an unpaired RERAISE-ended handler whose leading statements duplicate a post-try block's leading statements is the exceptional finally copy (tails decide the split -- [.., return] vs [.., raise] means the return is post-statement). The normal copy splits into finally: + rest. the body dedents instead of hiding under with), try interiors stop at the body end (post-try joins don't indent under try:), and 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.

pipeline

  1. find file (finder.py)
  2. detect version via magic (pymagic.py)
  3. load: native marshal, else pycdas backend (loader.py, pycdas_parser.py, shim.py)
  4. disassemble + abstract (bytecode.py: Inst, EditableBytecode, opcode/oparg/value-stack/metadata tables)
  5. normalize / mask consts + names (normalizer.py)
  6. opcode-aware B/I/E segmentation (segmenter.py)
  7. statement translation (translator.py: stack machine, defaults recovery, lambdas inline, 2.x print/exec/class ops)
  8. control-flow graph from jumps (cfg.py: if/for/while/try/except-as (bare except: too)/with/finally, clauses + clusters, 3.11+ exception table, SETUP-region splits)
  9. emit + repairs (emitter.py: source-order splicing onto def/class stub lines, bases recovery, @decorator restore, global decls, cond chains, ternaries, nested if/else, loop ranges, break/continue recovery, indent + suite-guard passes)
  10. equivalence verify, 3 sweeps (verifier.py: recompile revived source, compare opcode structure, repair, repeat x3)

layout

pyc_revive/
  finder.py  pymagic.py  loader.py  pycdas_parser.py  shim.py
  bytecode.py  normalizer.py  segmenter.py  translator.py  cfg.py
  emitter.py  verifier.py  autofix.py  reconstructor.py  cli.py
  deps.py  __main__.py  pycdas.exe

known limits

  • inline comprehensions/genexps render as func(...) placeholders.

  • 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 from 3.8+ 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 in any 3.14 sample yet); if the bundled pycdas hides caches after it the same way it does for conditional branches, loop joins may land 2 bytes off.

Metadata

Release files for pyc-revive 1.0.1

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.1
File Size Uploaded
pyc_revive-1.0.1.tar.gz 122.7 kB Details

Built distribution (wheel)

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

Total release size: 247.6 kB

Release files / pyc_revive-1.0.1.tar.gz

Download URL pyc_revive-1.0.1.tar.gz
Size 122.7 kB
Tags Source
SHA-256 checksum
How to use checksums
c8068f46fcec8c926ffdcc9af5536eb09764da795d04bdd40ef73866c6e6aeab
BLAKE2b-256 checksum
How to use checksums
cc5a6119b52b3c83923e015adb8735d6aec77807e16215d8fd48cee8d9fbe82e
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.1-py3-none-any.whl

Download URL pyc_revive-1.0.1-py3-none-any.whl
Size 125.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
50b04db29a79a254c3a5cf2a6e85b7dec320adca7236d793a55059d22987d2df
BLAKE2b-256 checksum
How to use checksums
2a863d243cf94eeb95112759383a01723b1ac4247ccf56a293ebef4b06b640ba
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

1.0.3

2 release files

1.0.2

2 release files

This release

1.0.1 This release

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