MCPcopy Index your code
hub / github.com/diohabara/pychd

github.com/diohabara/pychd @pychd-v1.3.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release pychd-v1.3.0 ↗ · + Follow
883 symbols 2,645 edges 58 files 239 documented · 27% updated 42d ago★ 52
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

PyChD

CI PyPI Version

A Python .pyc decompiler that reads CPython 3.0 – 3.14 bytecode and recovers the original .py. The pipeline is a deterministic rule pass (declarations, signatures, decorators, PEP 695 generics, PEP 749 lazy annotations) followed by one Codex CLI call per module to fill the bodies and module-level statements the rule pass can't recover from opcodes alone.

Recovery rate by corpus — Sig / Decl / Strict / BN / BS

Headline — measured contamination differential, fuzz-synthetic as the trust anchor

2,794 modules across 13 corpora, measured twice — once with the deterministic rule-only path (no LLM, fully reproducible offline) and once with hybrid-rewrite (rule pass + one Codex gpt-5.5 call per module). Two new tooling members in this repo make the contamination question directly measurable:

  • pychd-pyfuzz generates random syntactically-valid Python via direct AST construction — every sample is fresh, never published, never seen by any LLM. Lives in pychd_pyfuzz/ and on PyPI as pychd-pyfuzz.
  • pychd-pyobf anonymises a .pyc (renames identifiers, strips strings / docstrings / filenames / line tables) while preserving the opcode stream byte-for-byte. Lives in pychd_pyobf/ and on PyPI as pychd-pyobf.

Together they let us run two new families of corpus on top of the existing benchmark suite:

  • fuzz-synthetic (200 modules) — pyfuzz-generated, guaranteed LLM-naïve. The strongest contamination guarantee in the repo.
  • <corpus>-obf (815 modules across 5 mirrors of stdlib / stdlib-full / pypi / pypi-top20 / humaneval) — same bytecode structure as the raw counterpart, identifiers stripped. The delta between raw and -obf is the contamination signal that lets us put a number on "how much of the headline LLM score is memorisation".

The contamination differential, with numbers

Raw vs. anonymised, hybrid-rewrite mode, same backend model:

Corpus Raw strict_match -obf strict_match Δ (memorisation lift) Raw BS -obf BS Δ
stdlib 100 % (10/10) 86.7 % (13/15) −13.3 pt 60.0 % 0.0 % −60.0 pt
stdlib-full 91.5 % (140/153) 80.4 % (123/153) −11.1 pt 84.3 % 2.6 % −81.7 pt
pypi 89.9 % (170/189) 82.0 % (155/189) −7.9 pt 33.3 % 3.2 % −30.1 pt
pypi-top20 84.5 % (576/682) 83.4 % (569/682) −1.1 pt 63.3 % 5.3 % −58.0 pt
humaneval 100 % (164/164) 100 % (164/164) 0 pt (algorithmically simple) 98.2 % 86.6 % −11.6 pt
  • Strict-AST match drops 1.1–13.3 pt when identifiers are stripped on contamination-likely corpora. That gap is mechanically attributable to surface-token memorisation: the bytecode is unchanged, only the surface form the LLM would have seen in training data is gone.
  • Behavioural smoke (import + same public API) collapses under anonymisation — 60–80 pt drops are typical. This makes intuitive sense: a recovered module whose public surface is _n0, _v0 will not behave like the original "import the module and call its documented function" smoke test. It's an artefact of the metric, not the decompiler, and it shows up cleanly here.

The contamination-free baseline

The number we'd ask a security-conscious reader to actually trust as "what hybrid-rewrite does on never-before-seen code":

Metric fuzz-synthetic (LLM-naïve, 200 modules) recent-pypi (release-date proxy, 182 modules)
parses 100 % 100 %
signature_match 100 % (rule-only) → 100 % (hybrid) 98.4 % → 99.5 %
declaration_match 100 % (rule-only) → 100 % (hybrid) 98.4 % → 99.5 %
strict_match 21.0 % (rule-only) → 86.0 % (hybrid) 45.6 % (rule-only) → 81.9 % (hybrid)
BS (behavioural smoke) 0.0 % (rule-only) → 92.0 % (hybrid) 14.3 % → 20.3 %

Hybrid-rewrite reaching 86.0 % strict-AST match on fuzz-synthetic — bytecode that no LLM has ever seen — is the clean answer to "does pychd's hybrid path actually decompile, or does it just remember?": it decompiles. The contamination differential adds ~5–13 pt on contamination-likely corpora; that's the share that is not skill.

Aggregate over all 2,794 modules

Mode parses signature_match declaration_match strict_match BS
Rule-only (no LLM, deterministic) 100 % 99.7 % 99.7 % 43.1 % 19.3 %
Hybrid-rewrite (rule pass + 1 Codex call/module) 100 % 99.7 % 99.7 % 86.5 % 49.4 %

Pass@1 on HumanEval: rule-only 2.4 %hybrid-rewrite 97.6 %, but every HumanEval prompt is in the backend model's training data, so this is mostly an LLM-solves-HumanEval-from-memory signal rather than a decompilation signal.

Per-corpus recovery rate (rule-only vs hybrid-rewrite)

Per-tool comparison at each decompiler's preferred Python version

The take-away for anyone reading benchmark numbers for an LLM-assisted decompiler: separate the rule-only baseline from the LLM lift, and measure on a corpus the backend model cannot have seen. This repo is the first I know of to ship both halves of that — pychd-pyfuzz + pychd-pyobf are independent PyPI packages so other Python decompiler authors can drop the same harness into their CI. See §LLM contamination disclosure for the worked example (_colorize.py) and §Comparison with prior Python decompilers for the 23-module stdlib + PyPI head-to-head against uncompyle6 / decompyle3 / pycdc / PyLingual.

Quick start

uv tool install pychd
pychd decompile path/to/module.pyc --hybrid-rewrite --backend codex
# rules-only (deterministic, no LLM, offline, free — best for declaration recovery):
pychd decompile path/to/module.pyc --rules-only

--hybrid-rewrite is the default at the CLI. It uses your existing codex login session — set model = "gpt-5.5" in ~/.codex/config.toml (or pass -c model=...) to control which model. No extra API key needed.

If you want a fully offline, deterministic, audit-friendly run with no LLM calls and no contamination risk, use --rules-only — that is the path whose numbers the headline table above reports.

Table of contents

LLM contamination disclosure

The benchmark numbers on this page should be read as upper bounds under likely-contaminated conditions, not as evidence of clean generalisation.

  • The headline corpus is dominated by code (CPython stdlib, top-20 PyPI packages, OpenAI HumanEval) that any modern frontier model has almost certainly seen during pre-training. A high recovery rate there is partially memorisation, not just decompilation.
  • The tools/synthetic_corpus/ corpus (11 modules, 625 LoC, committed 2026-05-26) was drafted with the assistance of an LLM during this project's development. The exact source text did not exist on the public internet before that date, but the modules were produced by the same model family this benchmark uses as a backend, so it cannot honestly be called LLM-naïve. We keep it in the benchmark because it exercises specific PEP-695 / PEP-749 / match- statement constructs, but we no longer claim it isolates "uncontaminated" performance.
  • The PyPI subset (requests, click, attrs, flask, httpx, rich) and the top-20 sweep overlap published training corpora. Recent wheel pins (e.g. certifi 2026.5.20) reduce exact-version memorisation risk for those packages, but do not eliminate pattern-level memorisation.
  • HumanEval is a published evaluation set and almost certainly in training data; we report Pass@1 there as a re-executability sanity check, not as evidence of generalisation.

Per-metric trust table

Metric Rule-only Hybrid-rewrite Trust
parses 100 % 100 % ✓ honest — just ast.parse
signature_match (rule-only) 99.8 % ✓ honest — bytecode-derived
declaration_match (rule-only) 99.6 % ✓ honest — bytecode-derived
signature_match (hybrid Δ +0.2 pt) 100 % ✗ memorisation — see worked example below
strict_match (rule-only) 36.0 % ✓ honest — bytecode-derived
strict_match (hybrid Δ +57 pt) 93.2 % ⚠ unmeasured mix of memorisation + canonical-form derivation
BS (rule-only) 42.1 % ✓ honest
BS (hybrid Δ +26 pt) 68.1 % ⚠ contamination plausible
BN (rule-only) 7.2 % ✓ honest
BN (hybrid Δ +42 pt) 48.9 % ⚠ contamination plausible — body recovery from memory yields exact bytecode
FC Pass@1 (HumanEval) 2.4 % 97.6 % ✗ HumanEval is published, almost certainly memorised by the backend model. This metric measures "LLM solves HumanEval", not "pychd decompiles"
Edit similarity 0.445 0.753 ⚠ memorisation pushes this towards 1.0 by construction

Worked example: Lib/_colorize.py

The two CPython stdlib modules that fail rule-only signature_match (_colorize.py, _pylong.py) contain if False: / if 0: guards. For _colorize.py L8-12:

# types
if False:
    from typing import IO, Self, ClassVar
    _theme: Theme

CPython's constant folder erases the if False: block entirely. After compile() the bytecode contains zero IMPORT_NAME typing, zero STORE_NAME IO, etc. — the only survivor is _theme: Theme as a PEP 749 lazy annotation in the __annotate__ closure.

Pychd's rule pass correctly leaves those imports out of the recovered tree (you cannot decompile what isn't there). Hybrid- rewrite "fixes" the signature_match score by writing from typing import IO, Self, ClassVar into the output anyway — necessarily from training-data memorisation of CPython, since the .pyc carries no information about that line. That is the concrete mechanism behind the 0.2 pt sig-match gain, and the same kind of mechanism is plausibly contributing to the much larger strict- match / BN / Pass@1 gains.

What we now measure — fuzz-synthetic and *-obf

The contamination disclosure used to end with "a truly contamination-free evaluation would need a privately authored, never-published corpus that no LLM has touched at any stage of training. This repository does not ship one." That gap is now closed by two PyPI packages built alongside pychd:

  • pychd-pyfuzz generates random, syntactically-valid Python by direct AST construction. Every sample is fresh on every run, has random identifiers, and has never been published — so by construction no LLM has memorised it. We build a 200-module fuzz-synthetic corpus on every just paper run.
  • pychd-pyobf anonymises a CPython .pyc (rename identifiers, strip strings / docstrings / line tables / filenames) while preserving the opcode stream byte-for-byte. We use this to build a <corpus>-obf mirror of each existing corpus; pychd is then re-evaluated against the anonymised reference. The delta between raw and -obf scores on the same pipeline is, mechanically, the contamination signal — same bytecode, just with the surface tokens an LLM could have memorised stripped out.

Both packages are independent PyPI distributions (pip install pychd-pyfuzz pychd-pyobf) so any Python decompiler author can run the same trust-tier audit against their own tool. The expected shape of a non-contaminated result is approximately:

  • Rule-only strict_match ≈ within a few points of the raw-corpus number. The rule pass is bytecode-driven and identifier-agnostic, so anonymisation should not move it materially.
  • Hybrid-rewrite strict_match should drop on -obf corpora by an amount equal to the LLM's contamination advantage on that corpus. Anything large (e.g. > 30 pt) is strong evidence the upstream hybrid score is contamination-driven.

The measured numbers for this repo's hybrid-rewrite path are in the "contamination differential" table at the top of the README.

Pipeline at a glance

pychd routes every .pyc through two passes:

  • Rule pass owns everything CPython compiles to a deterministic bytecode shape — imports, class/function declarations, signatures, decorators (incl. arguments), PEP 695 generics, PEP 749 lazy annotations, common one-line bodies (return self.x, return cls(...), constructor self.x = x, etc.). Output is reproducible offline and audit-friendly. Bodies it can't recover remain as pass.
  • Codex rewrite runs once per module with the disassembly + the rule pass's partial output as context. It fills bodies and fixes module-level s

Core symbols most depended-on inside this repo

add
called by 93
pychd_pyfuzz/pychd_pyfuzz/tags.py
get
called by 83
tools/synthetic_corpus/pep695_box.py
_consume
called by 68
pychd/rules.py
_consume
called by 53
pychd/cross_version.py
_peek
called by 52
pychd/cross_version.py
decompile_pyc
called by 20
pychd/decompile.py
_gen_expression
called by 19
pychd_pyfuzz/pychd_pyfuzz/generator.py
_format_pct
called by 18
tools/render_paper.py

Shape

Method 445
Function 292
Class 145
Route 1

Languages

Python100%

Modules by API surface

tests/syntax_coverage_test.py102 symbols
pychd/rules.py63 symbols
pychd_pyfuzz/pychd_pyfuzz/generator.py57 symbols
tests/rules_test.py51 symbols
pychd/cross_version.py50 symbols
tests/ir_test.py44 symbols
tests/e2e_stdlib_test.py42 symbols
tests/semantic_test.py41 symbols
pychd/ir.py33 symbols
tools/benchmark.py26 symbols
pychd/decompile.py25 symbols
tools/build_corpora.py24 symbols

For agents

$ claude mcp add pychd \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page