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-9f83kd02nPRO-2026-XYZ
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_FUNCTION4 LOAD_NAME 1 (check)LOAD_CONST 2 ('PRO-2026-XYZ')COMPARE_OP 2 (==)
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 version | pycdc result | Tool to use |
|---|---|---|
| 2.x – 3.8 | Clean decompile | uncompyle6 (near-perfect) |
| 3.9 | Clean decompile | decompyle3 / pycdc |
| 3.10 – 3.12 | Clean decompile | pycdc (Decompyle++) |
| 3.13 | Partial (some functions incomplete) | pycdc + dis fallback |
| 3.14 | Bad MAGIC — not yet supported | marshal + 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
| Element | Recoverable from .pyc? |
|---|---|
| Program logic / control flow | Yes — fully |
| String literals (keys, URLs, messages) | Yes — plaintext, no decompiler needed |
| Function & class structure | Yes |
| Local variable names | Usually yes (stored in the code object) |
| Comments | No — stripped at compile time |
| Exact whitespace / formatting | No — regenerated by the decompiler |
| Docstrings | Yes, 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:
- Obfuscate before compiling — rename identifiers and encrypt strings so that even a perfect decompile yields unreadable code with no plaintext secrets. This is the single highest-value step.
- Compile to a native module — Cython to a `.pyd`/`.so` removes the bytecode entirely, so there's nothing for a
.pycdecompiler to read. - Keep secrets off the client — a key in a
.pycis 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 ObfuscatorFree 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.