Skip to main content
Reverse Engineering

How to Decompile a Python `.pyc` File (and Why `.pyc` Isn’t Protection)

Pyobfuscate Team··9 min read
Quick Answer

Yes — a Python .pyc decompiles. For Python 3.9 and earlier, uncompyle6 or decompyle3 return near-original source in one command; for 3.10+ you use pycdc (Decompyle++). And even without a decompiler, a .pyc leaks all its string literals in plaintext and its bytecode reveals the logic, so it runs with no .py and hides almost nothing. Treat .pyc as a cache, not protection.

People ship .pyc files all the time thinking the source is safe now. It isn't, and I wanted to show exactly how unsafe — not with hand-waving, but by actually trying it. So I wrote a tiny script with a fake API key and a license check, compiled it with Python 3.11, deleted the `.py`, and spent ten minutes seeing how much I could get back.

Short version: I got everything. Here's the walk-through, with the real output at each step.

What a `.pyc` file actually is

When Python imports a module, it compiles the source to bytecode and caches it in a .pyc under __pycache__/. The file is a small header (a version magic number + some flags) followed by a single marshal-serialized code object — the same thing the interpreter runs. Crucially, it's not encrypted or minified. It's your program in a slightly different shape.

That matters because "different shape" is not "hidden." Here's the script I compiled:

API_KEY = "sk-live-8f21b0c9d4"

def check_license(key):
    if key == "PRO-2026-XYZ":
        return "unlocked"
    return "locked"

print(check_license("PRO-2026-XYZ"))
secret.py — compiled to secret.cpython-311.pyc, then the .py deleted

First surprise: the secrets are just… sitting there

Before decompiling anything, I searched the raw .pyc bytes for my "secret" strings. Every one of them is stored verbatim — no decompiler required:

FOUND  sk-live-8f21b0c9d4
FOUND  PRO-2026-XYZ
FOUND  API_KEY
FOUND  check_license
FOUND  unlocked
FOUND  locked
Searching the compiled .pyc for the literals (real output)

If you've ever hard-coded an API key, token, or license string and shipped the .pyc, it's readable with a plain text search. Compiling changed nothing about that.

Second: the bytecode hands you the logic

Python ships a disassembler in the standard library (dis). I loaded the code object straight out of the .pyc and disassembled it. You don't need to "decompile" — you can just read the license check:

  1   LOAD_CONST   0 ('sk-live-8f21b0c9d4')
      STORE_NAME   0 (API_KEY)
  3   LOAD_CONST   1 (<code object check_license>)
      MAKE_FUNCTION
      STORE_NAME   1 (check_license)

Disassembly of <code object check_license>:
      LOAD_CONST   1 ('PRO-2026-XYZ')
      COMPARE_OP   2 (==)
      LOAD_CONST   2 ('unlocked')
dis output from the .pyc (trimmed to the interesting bits — real)

Read it back to yourself: assign API_KEY, define check_license, and inside it compare the argument to "PRO-2026-XYZ" and return "unlocked". That's the original program, minus the whitespace. Anyone who wants to patch the check just flips that comparison.

Third: it runs with no source at all

To be sure the .pyc was fully self-contained, I renamed it and ran it directly — the .py was already gone:

$ python runme.pyc
unlocked
Real terminal output

So the .pyc is a complete, runnable copy of your program that also happens to leak your strings and expose your logic. That's the opposite of protection.

Getting the actual source back (the decompilers)

If reading bytecode isn't your idea of fun, decompilers rebuild real .py source. Which one you use depends entirely on the Python version the .pyc was built for:

ToolPython versionsNotes
uncompyle62.x – 3.8The classic; near-perfect output on supported versions
decompyle33.7 – 3.9Fork focused on later 3.x
pycdc (Decompyle++)2.x – 3.12+C++ tool; the go-to for newer bytecode

I'll be honest about my own run: my .pyc was Python 3.11, and both uncompyle6 and decompyle3 currently top out around 3.8–3.9 — on 3.11 they wouldn't even start (their bytecode tables don't know that version yet). For 3.10 and up you reach for pycdc — which is exactly what powers our free, in-browser .pyc Decompiler (it runs pycdc compiled to WebAssembly, so nothing is uploaded). But notice the point of this whole exercise: I never actually needed a decompiler. The strings and the dis output already gave me everything.

Decompiler version-support is a moving target — tools catch up to each new Python release over time. "My version isn't supported yet" is a delay, not a defense.

So, is `.pyc` protection? No.

Shipping .pyc instead of .py stops exactly one thing: a colleague double-clicking the file to read it in Notepad. It does not stop anyone who spends five minutes. If your goal is to protect an algorithm, hide a key, or make a license check hard to bypass, .pyc does none of it.

What actually helps, in order of effort:

  1. Obfuscate the source first — rename everything and encrypt string literals so even the disassembly is unreadable. Our free Python Obfuscator does this at the AST level.
  2. Move secrets off the client — load API keys from the environment or a server, and validate licenses server-side. Anything on the user's machine can be extracted.
  3. Compile to a native module (.pyd/.so via Cython, or a Nuitka binary) so there's no bytecode to disassemble in the first place. See Python to EXE and Obfuscator PRO.

And if you *do* want to ship bytecode, at least obfuscate before you marshal it — a decompiled blob of already-mangled names buys you far more than a raw .pyc.

Make your bytecode unreadable — free

Obfuscate your Python at the AST level so a decompiled .pyc gives up nothing. In your browser, no signup.

Open the Python Obfuscator

Free tools mentioned here

Frequently asked questions

Can you decompile a Python .pyc file back to source?

Yes. For Python 3.9 and earlier, uncompyle6 or decompyle3 usually return near-identical source with a single command. For 3.10 and newer, pycdc (Decompyle++) handles the bytecode. Even without a decompiler, a .pyc exposes its string literals and its logic via the built-in dis module.

How do I decompile a .pyc file?

Install a decompiler that matches the .pyc’s Python version (uncompyle6 for ≤3.8, decompyle3 for ≤3.9, pycdc for 3.10+), then run it against the file, e.g. `uncompyle6 module.pyc`. To just read the logic without full source, load the code object and run Python’s dis disassembler on it.

Which Python version was a .pyc compiled for?

The first bytes of every .pyc are a magic number identifying the CPython version, and the filename usually encodes it (e.g. module.cpython-311.pyc = Python 3.11). You must decompile with a tool that supports that exact version.

Does shipping .pyc instead of .py protect my code?

No. A .pyc is cached bytecode: it runs without the source, stores string literals (including keys) in plaintext, and decompiles back to near-original code. It only stops someone from casually opening the file in a text editor.

Why won’t uncompyle6 decompile my newer .pyc?

uncompyle6 supports up to about Python 3.8 and decompyle3 up to 3.9; on newer interpreters they often fail to even load. Use pycdc (Decompyle++) for 3.10+, or read the bytecode directly with dis while the tools catch up.

How do I actually protect Python code then?

Obfuscate the source at the AST level (rename identifiers, encrypt strings) so the bytecode is unreadable, keep secrets off the client and validate licenses server-side, and for the strongest option compile to a native .pyd/.so or a Nuitka binary so there is no bytecode to decompile.

Is decompiling someone else’s .pyc legal?

It depends on jurisdiction, the software license, and your purpose. Decompiling your own code, or for interoperability/security research where local law and the license permit, is common; reverse-engineering third-party software you don’t have rights to can violate its license or the law. Check before you do it.

Keep reading