Skip to main content
Reverse Engineering

pycdc (Decompyle++) Guide: Install It, Use It, and What It Really Recovers

By Mithun··9 min read
Quick Answer

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.git
cd pycdc
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j4
# -> build/pycdc and build/pycdas
Build pycdc and pycdas from source (Linux) — tested

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
Decompile, and disassemble when the output looks wrong

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.py
Object Name: <module>
Qualified Name: <module>
Arg Count: 0
Pos Only Arg Count: 0
KW Only Arg Count: 0
Stack Size: 2
Flags: 0x00000000
pycdas on a Python 3.12 .pyc (real output, first lines)

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 = 0
while tries < attempts:
if code in VALID:
return "ok"
tries += 1
code = 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()
simple.py — the simple test script

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

Pythonpycdc outputDoes it run correctly?What went wrong
3.821 lines, no warningNo — SyntaxErrorwhile became if, tries += 1 became None += 1, attempts=3 became (3,)
3.921 lines, no warningNo — SyntaxErrorSame loop reconstruction bug
3.107 linesNoStopped with PycBuffer::getByte(): Unexpected end of stream
3.1117 lines, marked incompleteNoUnsupported opcode: POP_JUMP_BACKWARD_IF_TRUE
3.1223 lines, no warningNo — 'continue' not properly in loopLoop rebuilt as an if again
3.137 lines, marked incompleteNoUnsupported opcode: MAKE_FUNCTION
3.14nothing—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 = 0
if tries < attempts:
if code in VALID:
return 'ok'
None += 1
code = code.upper()
continue
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()
pycdc on the Python 3.8 .pyc (real output, unedited)

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

Pythonpycdc outputMain problems reported
3.836 lines, 2 incomplete sectionsdataclass lost (Unsupported Node type: 12), BEGIN_FINALLY / CALL_FINALLY
3.947 lines, 2 incompleteJUMP_IF_NOT_EXC_MATCH, MAP_ADD, dataclass lost
3.1048 lines, 2 incompleteJUMP_IF_NOT_EXC_MATCH, MAP_ADD, dataclass lost
3.1157 lines, 2 incompleteRETURN_GENERATOR (the context manager), MAP_ADD
3.1240 lines, 3 incompleteLOAD_FAST_AND_CLEAR (inlined comprehensions), RETURN_GENERATOR
3.139 lines, 1 incompleteMAKE_FUNCTION — almost nothing recovered
3.14nothingBad 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.

Tool3.8 simple script3.8 realistic module3.9 and newer
pycdc (b428976)Wrong loop, no warningPartial, 2 incomplete sectionsPartial up to 3.13; 3.14 rejected
uncompyle6 3.9.3Ran but crashed: slice became API_KEY[(-4)[:None]], plus a stray for … elseDeparsing stopped due to parse errorUnsupported Python version
decompyle3 3.9.3Printed its internal parse tree instead of codeOutput failed with IndentationErrorUnsupported 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 .pyc is from a Python version it doesn't support yet (3.14 today) or it isn't a plain .pyc at 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 @dataclass class.
  • `# 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 Decompiler

Free 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).

Keep reading