Skip to main content
Reverse Engineering

Can Python .pyc Files Be Decompiled? A Complete Guide

By Mithun··9 min read
Quick Answer

Yes. A .pyc is a compiled bytecode file, and bytecode decompiles back to near-original Python — uncompyle6/decompyle3 for Python ≤3.9, pycdc for 3.10+. Even without a decompiler, a .pyc leaks every string literal in plaintext and reveals its logic via the dis module. What survives: logic, strings, and structure. What's lost: comments and (mostly) exact formatting. How well it decompiles depends heavily on the Python version.

"Is my .pyc safe to ship?" comes up constantly, usually with the hope that compiled bytecode is somehow opaque. It isn't. I compiled the same file across Python 3.8 through 3.14 and ran real decompilers against each to show exactly what comes back — and how much the version matters.

This is the .pyc-specific deep dive; for the broader picture across .py/.pyc/.pyd/server, see can Python code be decompiled?

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 4-byte magic number identifying the exact CPython version, plus flags and a timestamp/hash — followed by a single marshal-serialized code object: the same thing the interpreter runs. It is not encrypted or minified. It's your program in a different shape.

That magic number is why version matters so much: a .pyc built by Python 3.11 declares "I am 3.11 bytecode," and any tool that reads it must understand that exact version's instruction set.

What decompiling recovers (tested)

I compiled a small module with a hard-coded key and a license check, deleted the .py, and went after the .pyc. Two things come out with zero decompiler required. First, the strings — a plain byte search finds them all:

$ strings secret.pyc | grep -E "sk-live|PRO-"
sk-live-9f83kd02n
PRO-2026-XYZ
strings leak straight from the .pyc

Second, the built-in dis module reads the logic directly off the code object — no reconstruction needed:

1 LOAD_CONST 0 ('sk-live-9f83kd02n')
STORE_NAME 0 (API_KEY)
2 LOAD_CONST 1 (<code object check>)
MAKE_FUNCTION
4 LOAD_NAME 1 (check)
LOAD_CONST 2 ('PRO-2026-XYZ')
COMPARE_OP 2 (==)
real dis output from the .pyc

You can read the program back from that: assign API_KEY, define check, compare the argument to "PRO-2026-XYZ". A proper decompiler goes further and rebuilds real .py source — try it in our browser-based .pyc Decompiler.

Version differences: what works on which Python

This is the part most guides skip. Decompiler support is tightly tied to the Python version the .pyc was built for. I compiled the same file with Python 3.8–3.14 and ran our pycdc (Decompyle++) engine against each — real results:

Python versionpycdc resultTool to use
2.x – 3.8Clean decompileuncompyle6 (near-perfect)
3.9Clean decompiledecompyle3 / pycdc
3.10 – 3.12Clean decompilepycdc (Decompyle++)
3.13Partial (some functions incomplete)pycdc + dis fallback
3.14Bad MAGIC — not yet supportedmarshal + dis (always works)

The pattern: decompilers lag new Python releases by months, so the *newest* bytecode is the hardest to fully decompile — for now. But "my version isn't supported yet" is a delay, not a defense: the dis + marshal fallback reads the logic and strings from *any* version, including the 3.14 file pycdc rejected.

What survives, and what's lost

ElementRecoverable from .pyc?
Program logic / control flowYes — fully
String literals (keys, URLs, messages)Yes — plaintext, no decompiler needed
Function & class structureYes
Local variable namesUsually yes (stored in the code object)
CommentsNo — stripped at compile time
Exact whitespace / formattingNo — regenerated by the decompiler
DocstringsYes, unless stripped

So "why do variable names sometimes disappear?" — they don't, normally: CPython keeps local names in the code object, so a decompiler recovers them. Names only vanish if the code was obfuscated before compiling (renamed to meaningless tokens) — which is exactly the point of obfuscation. Comments and formatting are the only things genuinely lost, because the compiler discards them.

Why obfuscation matters — and how to protect a `.pyc`

If a raw .pyc gives up logic, strings, and names, then shipping .pyc "for protection" achieves almost nothing. What actually helps, in order of effort:

  1. Obfuscate before compilingrename identifiers and encrypt strings so that even a perfect decompile yields unreadable code with no plaintext secrets. This is the single highest-value step.
  2. Compile to a native moduleCython to a `.pyd`/`.so` removes the bytecode entirely, so there's nothing for a .pyc decompiler to read.
  3. Keep secrets off the client — a key in a .pyc is a key in plaintext; load it from the environment or a server instead (details).

Bottom line: treat .pyc as a performance cache, never as protection. It decompiles, it leaks strings, and the dis fallback works on every version. If your logic is worth protecting, obfuscate it first.

Make your .pyc worthless to decompile — free

A plain .pyc gives up logic, strings and names. Obfuscate your source first so even a perfect decompile yields unreadable code with no plaintext secrets. In your browser, nothing installed.

Open the Python Obfuscator

Free tools mentioned here

Related guides

Frequently asked questions

Can .pyc files be decompiled back to source?

Yes. A .pyc is compiled bytecode, and decompilers rebuild near-original .py source from it — uncompyle6/decompyle3 for Python 3.9 and earlier, pycdc (Decompyle++) for 3.10+. Even without a decompiler, a .pyc leaks all its string literals in plaintext and reveals its logic through the built-in dis module.

Does the Python version affect whether a .pyc decompiles?

Heavily. Decompilers lag new Python releases: in my tests pycdc cleanly decompiled Python 3.8–3.12, was partial on 3.13, and rejected 3.14 with a 'Bad MAGIC' error. But the marshal + dis fallback reads the logic and strings from any version, including ones no decompiler supports yet.

What is lost when Python compiles to .pyc?

Only comments and exact formatting (whitespace) are truly lost — the compiler discards them. Program logic, string literals, function/class structure, local variable names, and docstrings all survive in the code object and are recoverable. Variable names only disappear if the code was obfuscated before compiling.

Why can I still read strings in a .pyc without decompiling?

String literals are stored verbatim in the code object's constants, so a plain byte search (like the strings command) finds API keys, URLs and messages in a .pyc directly — no decompiler needed. This is why you should never hard-code secrets in code you distribute as .pyc.

How do I protect a .pyc file from being decompiled?

Obfuscate the source before compiling it (rename identifiers and encrypt string literals) so a decompile yields unreadable code with no plaintext secrets; or compile to a native .pyd/.so with Cython so there's no bytecode to decompile at all; and keep real secrets off the client entirely. Shipping a plain .pyc provides essentially no protection.

Keep reading