Most Python obfuscation is reversible because it isn't encryption — the code has to run, so the machine can always reach the real logic, and so can you. Four techniques cover almost everything: 1) unpack exec(base64.b64decode(zlib.decompress(...))) packers by decoding the payload instead of running it; 2) re-indent mangled or minified source with ast.parse → ast.unparse; 3) disassemble marshal/.pyc blobs with dis (or decompile them with a .pyc decompiler); 4) pull "hidden" string constants by statically evaluating just the decode expression. The one thing no tool recovers: original variable and function names — once renamed, they're gone for good.
Deobfuscation sounds like code-breaking, but for Python it's mostly bookkeeping. The reason is simple and it's the same reason obfuscation is weak as a security control: your interpreter has to run the code, so somewhere in the file the real logic is always reachable. If CPython can get to it, so can you — you just reverse the same transforms the obfuscator applied, in the opposite order.
There are legitimate reasons to do this: understanding what your *own* obfuscated build actually exposes, auditing a dependency, or analyzing a suspicious script before you run it. I ran every method below on real samples on Python 3.14 — here's exactly what each one recovers, and what it can't.
What deobfuscation can and cannot recover
Set expectations first, because this is where people get confused. Deobfuscation reliably gets you back to working, readable code — correct structure, correct logic, the actual strings and constants. What it does not get you back is the *original author's intent*: meaningful names, comments, and formatting that were thrown away during obfuscation are not encoded anywhere, so nothing can reconstruct them.
Rule of thumb: obfuscation is a one-way hash for readability, not for logic. Logic is fully recoverable; readability is only partly recoverable. That asymmetry is the whole story.
Only deobfuscate code you own or are authorized to analyze. Reversing someone else's protection to bypass a license or republish their work can breach their terms or copyright — this guide is for auditing your own builds and inspecting untrusted code safely.
Method 1 — unpack an exec(base64/zlib) one-liner
The most common "obfuscated" Python you'll meet is a packer: the real source is compressed, base64-encoded, and handed to exec(). It looks impenetrable but it's the easiest to reverse. Here's a real one our engine-style packer produced:
import base64, zlibexec(zlib.decompress(base64.b64decode('eJxdjDsKgDAUBPuc...' # long base64 blob)))
The trick is to never call `exec`. Copy the payload and reverse the exact same steps — base64-decode, then decompress — but print the result instead of executing it:
import base64, zlibpayload = 'eJxdjDsKgDAUBPuc...' # the blob from the filesource = zlib.decompress(base64.b64decode(payload)).decode()print(source)
def login(user, pw):SECRET = "s3cr3t-token"return user == "admin" and pw == SECRETprint(login("admin", "s3cr3t-token"))
I verified recovered == original returned True. Because the packer is just reversible encoding, you get the source back exactly — names, strings, and all. If the layers are nested (base64 inside base64, or multiple execs), you repeat the same peel one layer at a time until plain source falls out.
When you're unsure what a payload does, swap the outer exec(x) for print(x) (or route it through a decoder like above). You statically reveal the next layer without ever running untrusted code — the safe way to open a suspicious sample.
Method 2 — re-indent mangled or minified source (AST)
A lot of obfuscation renames identifiers to noise (_0xa, l1ll1) and crushes everything onto few lines. The logic is intact — it's just unreadable. Python's own ast module fixes the layout for free: parse it, then unparse it back to canonical, correctly-indented source.
def _0xa(_0xb, _0xc):_0xd = _0xb + _0xcreturn _0xd * 2print(_0xa(3, 4))
import astsrc = open('mangled.py').read()print(ast.unparse(ast.parse(src)))
def _0xa(_0xb, _0xc):_0xd = _0xb + _0xcreturn _0xd * 2print(_0xa(3, 4))
This restores readability of structure, but look closely: the names are still _0xa, _0xb, _0xd. ast.unparse cannot invent add_then_double, a, b — that information was destroyed at obfuscation time and is gone forever. You can rename them back by hand once you understand the code, but no tool does it for you.
Want to see the tree an obfuscator works on? Paste code into the AST viewer — it's the same ast.parse step, visualized.
Method 3 — disassemble marshal / .pyc blobs
Some obfuscation ships compiled bytecode instead of source — a marshal.dumps blob, or a shipped .pyc. There's no source to unpack, but the code object still describes every operation. marshal.loads it and dis.dis it:
import marshal, discode = marshal.loads(open('blob.bin', 'rb').read())dis.dis(code)print('constants:', code.co_consts)
1 LOAD_SMALL_INT 41STORE_NAME 0 (x)2 LOAD_NAME 0 (x)LOAD_SMALL_INT 1BINARY_OP 0 (+)...constants: (41, None)
Even without pretty source, the disassembly plus co_consts, co_names, and co_varnames tell you the constants, the names touched, and the exact control flow. To go one step further and turn bytecode back into readable Python, use a decompiler — our free online .pyc decompiler handles every CPython version in the browser. (Full walkthrough: decompile a .pyc file.)
Method 4 — extract "hidden" string constants
Obfuscators love to hide secrets by encoding string literals — base64.b64decode("..."), bytes.fromhex(...), XOR loops. You don't need to run the whole program to reveal them; you evaluate only the decode expression, in isolation, using the AST:
import ast, base64tree = ast.parse(open('mod.py').read())assign = [n for n in tree.body if isinstance(n, ast.Assign)][0]expr = ast.Expression(assign.value)print(eval(compile(expr, '<x>', 'eval'), {'base64': base64}))
prod-api-key-1234
This is exactly why you must never obfuscate an API key or password into client-side code. The decode step is right there in the file; anyone can evaluate it in one line. Encoded ≠ encrypted. See protect API keys in Python for what to do instead (keep secrets server-side).
The lesson: obfuscation slows readers, it doesn't stop them
| Obfuscation technique | How it's reversed | What survives reversal |
|---|---|---|
| exec(base64/zlib) packer | Decode payload, don't run | Everything — exact source |
| Name mangling / minify | ast.parse → ast.unparse | Logic & structure (not names) |
| Marshal / .pyc bytecode | marshal.loads + dis, or a decompiler | Full logic, constants, names used |
| Encoded string constants | Eval the decode expression only | The plaintext secret |
Every row reverses with the standard library in minutes. That's not a knock on obfuscation — it genuinely raises the effort bar, deters casual copying, and protects trivial IP. But it is not a security boundary, and treating it like one is how secrets leak. If you need to actually *control who can run your code* — not just make it annoying to read — you need something obfuscation can't provide: a check the user can't simply reverse.
This is the honest split we cover in is Python obfuscation secure?: obfuscate to slow down casual reading, but pair it with server-side logic for anything sensitive, or license enforcement (device-bound keys, signed responses) when you're selling software. Obfuscation buys time; it doesn't buy control.
See how strong your obfuscation really is
Obfuscate a snippet with our free tool, then try the methods above on it — the best way to understand what protection actually buys you.
Open the Python ObfuscatorFree tools mentioned here
Related guides
Frequently asked questions
Can all obfuscated Python be deobfuscated?
Effectively yes, in the sense that the working logic can always be recovered — Python has to run the code, so the real behavior is always reachable by reversing the same transforms the obfuscator applied. What can't be recovered are the original variable/function names, comments, and formatting, because obfuscation discards those permanently. So you get back functional, readable code, but not a byte-perfect copy of the author's original file (unless it was a simple reversible packer, which does restore the source exactly).
How do I deobfuscate exec(base64...) Python code?
Don't run the exec(). Copy the encoded payload and reverse the same steps the code would — usually base64.b64decode() then zlib.decompress() — but print the result instead of executing it. That reveals the next layer of source safely. If it's nested (base64 inside base64, or multiple exec calls), repeat the peel one layer at a time until plain Python source falls out.
Can I recover the original variable names?
No. When an obfuscator renames variables and functions to things like _0xa or l1ll1, the original names are thrown away and stored nowhere in the file. ast.unparse can restore correct indentation and structure, and disassembly shows the logic, but no tool can turn _0xa back into add_then_double. You can rename them yourself once you understand the code, but it's manual.
How do I deobfuscate a compiled .pyc file?
A .pyc contains a marshalled code object, not source. Load it and disassemble it with Python's dis module to read the operations, constants, and names, or use a decompiler to reconstruct readable Python — pyobfuscate.com's free online .pyc decompiler does this in the browser for every CPython version. See our decompile a .pyc file guide for a step-by-step.
Is deobfuscating Python code legal?
Analyzing code you own or wrote is fine. Reversing third-party software can be a different matter: bypassing license protection, or copying and republishing someone's proprietary code, may violate their license terms or copyright depending on your jurisdiction. Deobfuscating to audit a dependency or inspect a suspicious script for safety is generally defensible; use judgment and stay within the rights you actually have.