pycdc (Decompyle++) Guide: Install It, Use It, and What It Really Recovers
pycdc (Decompyle++) is a free, open-source C++ decompiler that turns `.pyc` bytecode back into Python. You build it from source with CMake, then run pycdc file.pyc; its sibling pycdas prints the disassembly. I tested the latest pycdc (commit b428976, April 2026) on the same scripts compiled with every CPython from 3.8 to 3.14: it recovered names, strings and most of the structure on 3.8–3.13, but its output didn't run on any version — a simple while loop and a dataclass were enough to trip it — and it rejects 3.14 files outright with Bad MAGIC!. Use it to read code, not to get a working file back.
If you have a .pyc and no source, pycdc is usually the first tool people reach for: it's fast, it's free, and unlike the pure-Python decompilers it keeps up with newer bytecode. What most guides don't tell you is how good the output actually is. So instead of repeating the README, I built the current pycdc from source and put it through a real test.
The method is simple and strict. I wrote two scripts, compiled each one with CPython 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 and 3.14 (14 .pyc files), decompiled every file with pycdc, and then ran the recovered code on the same Python version to see if it does what the original did. For comparison I ran uncompyle6 and decompyle3 (both 3.9.3) on the same files.
What pycdc is (and what pycdas is)
pycdc — short for Python C++ Decompiler, also called Decompyle++ — is an open-source project by Michael Hansen (zrax) on GitHub, licensed under the GPL-3.0. It reads the code objects inside a .pyc and rebuilds Python statements from the bytecode. It isn't on PyPI: there's no pip install pycdc, you compile it yourself.
The build gives you two programs. `pycdc` is the decompiler: it prints recovered source. `pycdas` is a disassembler: it prints every code object, its constants, names and raw instructions. When pycdc gets something wrong, pycdas is how you check what the bytecode really says.
How to install pycdc (built and tested)
I built it on Debian 12 (bookworm) from a clean container. You need Git, CMake and a C++ compiler; nothing else. On a 4-core machine the whole build took 63 seconds.
sudo apt install git cmake g++git clone https://github.com/zrax/pycdc.gitcd pycdccmake -S . -B build -DCMAKE_BUILD_TYPE=Releasecmake --build build -j4# -> build/pycdc and build/pycdas
On macOS the same CMake commands apply after installing CMake (brew install cmake) and Xcode's command-line tools. On Windows, install CMake and Visual Studio's C++ Build Tools, run the same two cmake commands, and you get pycdc.exe and pycdas.exe under build\Release. I verified the Linux build above; I didn't re-test the macOS and Windows paths for this article.
pycdc is developed on the main branch without formal releases, so the commit you build matters. Note it (git log -1) when you compare results — mine was b428976 from 2026-04-06.
How to use pycdc and pycdas
./build/pycdc module.pyc > recovered.py./build/pycdas module.pyc > module.dis.txt
pycdc writes the source to standard output and its problems to standard error — so don't redirect stderr away. Lines like Unsupported opcode: … tell you exactly where the output stops being trustworthy. Here is the start of real pycdas output for one of my test files:
s-3.12.pyc (Python 3.12)[Code]File Name: simple.pyObject Name: <module>Qualified Name: <module>Arg Count: 0Pos Only Arg Count: 0KW Only Arg Count: 0Stack Size: 2Flags: 0x00000000
The test: two scripts, seven Python versions
The first script is deliberately simple: a hard-coded key, a set of license codes and a while loop that retries — the kind of thing people ship as a .pyc hoping it hides the logic.
API_KEY = "sk-demo-7c1f2a"VALID = {"PRO-2026-XYZ", "TRIAL-7788"}def check(code, attempts=3):tries = 0while tries < attempts:if code in VALID:return "ok"tries += 1code = code.upper()return "denied"def main():for code in ["pro-2026-xyz", "nope"]:print(code, "->", check(code))print("key suffix:", API_KEY[-4:])if __name__ == "__main__":main()
The second is a realistic 60-line module: a @dataclass, a @contextmanager, JSON parsing with try/except, a list and a dict comprehension, a for … else, a sorted(key=lambda …) and formatted f-strings. Every recovered file was run on the same CPython version as the original and its output compared line by line.
Results: the simple script
| Python | pycdc output | Does it run correctly? | What went wrong |
|---|---|---|---|
| 3.8 | 21 lines, no warning | No — SyntaxError | while became if, tries += 1 became None += 1, attempts=3 became (3,) |
| 3.9 | 21 lines, no warning | No — SyntaxError | Same loop reconstruction bug |
| 3.10 | 7 lines | No | Stopped with PycBuffer::getByte(): Unexpected end of stream |
| 3.11 | 17 lines, marked incomplete | No | Unsupported opcode: POP_JUMP_BACKWARD_IF_TRUE |
| 3.12 | 23 lines, no warning | No — 'continue' not properly in loop | Loop rebuilt as an if again |
| 3.13 | 7 lines, marked incomplete | No | Unsupported opcode: MAKE_FUNCTION |
| 3.14 | nothing | — | Bad MAGIC! — the version isn't supported |
This is what pycdc returned for the 3.8 file — note there's no warning anywhere, yet the function is broken:
API_KEY = 'sk-demo-7c1f2a'VALID = {'PRO-2026-XYZ','TRIAL-7788'}def check(code, attempts = (3,)):tries = 0if tries < attempts:if code in VALID:return 'ok'None += 1code = code.upper()continuereturn 'denied'def main():for code in ('pro-2026-xyz', 'nope'):print(code, '->', check(code))print('key suffix:', API_KEY[-4:])if __name__ == '__main__':main()
The dangerous cases are 3.8, 3.9 and 3.12: pycdc printed plausible-looking code with no warning at all, and it was wrong. Always run (or at least syntax-check) recovered code before trusting it, and compare against pycdas where it matters.
Results: the realistic module
| Python | pycdc output | Main problems reported |
|---|---|---|
| 3.8 | 36 lines, 2 incomplete sections | dataclass lost (Unsupported Node type: 12), BEGIN_FINALLY / CALL_FINALLY |
| 3.9 | 47 lines, 2 incomplete | JUMP_IF_NOT_EXC_MATCH, MAP_ADD, dataclass lost |
| 3.10 | 48 lines, 2 incomplete | JUMP_IF_NOT_EXC_MATCH, MAP_ADD, dataclass lost |
| 3.11 | 57 lines, 2 incomplete | RETURN_GENERATOR (the context manager), MAP_ADD |
| 3.12 | 40 lines, 3 incomplete | LOAD_FAST_AND_CLEAR (inlined comprehensions), RETURN_GENERATOR |
| 3.13 | 9 lines, 1 incomplete | MAKE_FUNCTION — almost nothing recovered |
| 3.14 | nothing | Bad MAGIC! |
None of the seven outputs ran. But pycdc isn't useless here — on 3.11 it correctly recovered the constants (TAX = 0.18 and the full JSON string), the function signatures, the json.loads call, the for … else in first_over, sorted(items, key=lambda i: -i.total()) and even the formatted f-string {it.name:<8}{it.total():>8.2f}. What it lost were the dataclass, the generator-based context manager and the dict comprehension, which came out as 'tags:'(<dictcomp>, sorted(by_tag.items())()).
That's the honest summary of pycdc today: very good at showing you what a file contains, unreliable at giving you a file that runs. For reading secrets and logic out of a .pyc, that's often enough — and it's a good reminder that shipping .pyc hides almost nothing.
pycdc vs uncompyle6 and decompyle3
The pure-Python decompilers are often recommended as the more accurate option for older bytecode, so I ran both (version 3.9.3, on Python 3.8) against the same files.
| Tool | 3.8 simple script | 3.8 realistic module | 3.9 and newer |
|---|---|---|---|
| pycdc (b428976) | Wrong loop, no warning | Partial, 2 incomplete sections | Partial up to 3.13; 3.14 rejected |
| uncompyle6 3.9.3 | Ran but crashed: slice became API_KEY[(-4)[:None]], plus a stray for … else | Deparsing stopped due to parse error | Unsupported Python version |
| decompyle3 3.9.3 | Printed its internal parse tree instead of code | Output failed with IndentationError | Unsupported Python version |
So on modern bytecode there isn't really a contest: uncompyle6 and decompyle3 stop at 3.8, and even there they stumbled on these files. pycdc is the only one of the three that reads 3.9–3.13 at all.
Reading pycdc's error messages
- `Bad MAGIC!` — the first four bytes of the file are a magic number pycdc doesn't know. Either the
.pycis from a Python version it doesn't support yet (3.14 today) or it isn't a plain.pycat all (encrypted or packed). See how to read a .pyc's magic number. - `Unsupported opcode: NAME (n)` — a bytecode instruction pycdc can't translate yet. Everything around it is suspect; check that area with
pycdas. - `Unsupported Node type: 12` / `<NODE:12>` in the output — a construct it couldn't rebuild. In my tests this was the
@dataclassclass. - `# WARNING: Decompyle incomplete` inside the source — pycdc gave up on that block and left a stub (often just
pass). - `PycBuffer::getByte(): Unexpected end of stream` — it lost track of the bytecode and read past the end; the output is truncated.
- No message at all — not proof the output is right. Three of my seven simple-script outputs were wrong without any warning.
When pycdc is the right tool
- Auditing what a `.pyc` or a PyInstaller build exposes — strings, names, imports and the rough logic come through well.
- Quick triage on 3.8–3.13 files before spending time on a deeper analysis.
- Pairing with `pycdas` or `dis` to verify the exact logic of the parts that matter.
If you want readable source without building anything — including Python 3.14 files — use our free online .pyc Decompiler. And if you're on the other side, trying to protect your own code, the results above are the argument: a .pyc gives away its strings and structure in seconds, so obfuscate the source before you ship it.
Skip the build — decompile a .pyc online
Our free .pyc Decompiler returns readable source for Python 2.x through 3.14. No install.
Open the .pyc DecompilerFree tools mentioned here
Related guides
Frequently asked questions
What is pycdc?
pycdc (Decompyle++) is a free, open-source C++ decompiler that rebuilds Python source from .pyc bytecode. It comes with pycdas, a disassembler. You build both from source with CMake — there's no pip package.
How do I install pycdc on Windows?
Install CMake and Visual Studio's C++ Build Tools, clone https://github.com/zrax/pycdc, then run cmake -S . -B build and cmake --build build --config Release. The programs appear as pycdc.exe and pycdas.exe under build\Release. I tested the Linux build for this guide, not the Windows one.
Does pycdc support Python 3.12, 3.13 and 3.14?
Partly. In my tests the latest pycdc (April 2026) read 3.12 and 3.13 files but produced output that didn't run — several opcodes are still unsupported — and it rejected 3.14 files with 'Bad MAGIC!'.
Is there a pycdc online?
You don't need to build anything to decompile a .pyc: our free online .pyc Decompiler takes a file from Python 2.x through 3.14 and returns readable source, with nothing to install.
Is pycdc better than uncompyle6?
For anything newer than Python 3.8, yes — uncompyle6 and decompyle3 simply refuse 3.9+ bytecode. On 3.8 none of the three produced fully correct code for my test files, so verify whatever you use.
Why does pycdc say 'Bad MAGIC!'?
The .pyc starts with a magic number for the exact Python version that compiled it. 'Bad MAGIC!' means pycdc doesn't recognise it — usually because the file is from a newer Python (3.14 today) or isn't a plain .pyc (packed or encrypted).