Skip to main content
Obfuscation

Is Python Obfuscation Secure? I Tried to Break 5 Common Methods

Pyobfuscate Team··10 min read
Quick Answer

It depends entirely on the method. The DIY tricks people reach for first — base64/zlib encoding, marshaling bytecode, stacking encode layers, or just minifying/renaming — are *not* secure: I reversed all five back to working source in well under a second each, and every one leaked its API key and license string in plaintext. Encoding is not encryption. Real protection starts when string literals are actually encrypted (so a text search and dis show nothing) and secrets live server-side or in a compiled native module — not in any of the copy-paste one-liners below.

"Just obfuscate it" is the most common answer to "how do I protect my Python?" — and most of the time it means one of a handful of copy-paste tricks off Stack Overflow or a random online tool. I wanted to know how much protection those actually buy, so I stopped theorizing and tested them.

I wrote one tiny script with a fake API key and a license check, obfuscated it five different common ways, and then timed how long it took me to get the original source (and the secrets) back out of each. Every method, the real obfuscated form, the exact reversal, and the clock. Spoiler: the slowest one took me about three and a half seconds, and that was only because it had bloated the file to 300 MB.

Everything below was run on Python 3.14 on Windows. You can reproduce all of it with the standard library — no special tools.

The victim script

Same script for every test. It has the two things people obfuscate to hide: a hard-coded credential and a license comparison.

API_KEY = "sk-live-4a91f7c2e8d0"

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

print(check_license("PRO-2026-ZQX"))
victim.py

For each method I checked two things: did I recover byte-identical source, and are the secret strings (sk-live-4a91f7c2e8d0, PRO-2026-ZQX) sitting there in plaintext once I do?

Method 1 — base64 + exec (the one everyone posts)

The single most-recommended "obfuscation" is to base64-encode the source and exec it. It *looks* unreadable, which is the whole appeal:

import base64
exec(base64.b64decode('QVBJX0tFWSA9ICJzay1saXZlLTRhOTFmN2MyZThkMCIK...'))
The obfuscated file

But base64 isn't a cipher — there's no key. It's a reversible alphabet. The exact same call that runs it, decodes it. The reversal *is* one line:

>>> import base64
>>> print(base64.b64decode('QVBJX0tFWSA9...').decode())
API_KEY = "sk-live-4a91f7c2e8d0"

def check_license(key):
    if key == "PRO-2026-ZQX":
        ...

# byte-identical to the original — measured at 0.004 ms
Reversal (real output)

Recovered in 0.004 ms, byte-for-byte, with all four secrets in plaintext. Anything the interpreter can decode to run, an attacker decodes to read. This buys you exactly nothing.

Method 2 — zlib + base64 ("but it's compressed")

The common upgrade is to zlib.compress first, then base64. Now the blob is shorter and looks even more random. Same fatal flaw: compression is not encryption, and the decompress key is, again, nothing.

import base64,zlib
exec(zlib.decompress(base64.b64decode('eJxzDPCM93aNVLBVUCrO1s3JLEvVNUm0...')))
The obfuscated file
>>> zlib.decompress(base64.b64decode(blob)).decode()
# byte-identical original source, measured at 0.010 ms
Reversal (real output)

0.010 ms. You read the decompress call in the payload; you *are* the decompress call. Adding a second reversible transform just means pressing undo twice.

Method 3 — stacking 50 encode layers (the "layered" online tools)

Some online obfuscators lean into the illusion by encoding over and over — base64 of base64 of base64, dozens of times. The output is a genuinely huge wall of noise, which *feels* like security. It isn't; it's the same undo, in a loop.

I encoded the source 50 times. The payload ballooned to 313,469,352 bytes — nearly 300 MB — to hide seven lines. Then I peeled it with a five-line loop that just keeps decoding until it sees Python:

data = payload
while True:
    try:
        data = base64.b64decode(data, validate=True)
    except Exception:
        break

# stripped all 50 layers in 50 rounds, ~3.5 s
# (almost all of that is just shuffling 300 MB of bytes)
# result: byte-identical original source, secrets in plaintext
Auto-peel — no knowledge of how many layers there were

Layering doesn't multiply the difficulty — it just adds mechanical, self-announcing steps. The only thing 50 layers reliably produces is a 300 MB file that's slower to *ship* than to crack.

Method 4 — marshal bytecode (exec(marshal.loads(...)))

This one *feels* more serious because the payload is real binary: a compiled code object, marshaled to bytes. No readable text in sight. But the code object is exactly what the interpreter runs, and Python ships the tools to read it in the standard library.

import marshal
exec(marshal.loads(b'\xe3\x00\x00\x00...'))   # 357 opaque bytes
The obfuscated file

Two standard-library calls undo it. marshal.loads gives back the code object; dis disassembles it. And every string constant is readable straight off the object — no decompiler needed:

>>> co = marshal.loads(blob)
>>> [c for c in walk_consts(co) if isinstance(c, str)]
'sk-live-4a91f7c2e8d0'
'PRO-2026-ZQX'
'unlocked'
'locked'
Constants pulled from the code object (real output)
LOAD_CONST     2 ('PRO-2026-ZQX')
COMPARE_OP    88 (bool(==))
LOAD_CONST     1 ('unlocked')
dis output — the license check, verbatim

Total time from opaque binary to "here is the key and here is the exact comparison": 0.417 ms. If you want full .py source rather than bytecode, a decompiler like pycdc rebuilds it — that's the same engine behind our free .pyc Decompiler. But as with a raw .pyc, I never needed it; the constants and the disassembly already told me everything. (More on why in Python `marshal` explained.)

Method 5 — minify + rename (a real tool this time)

The encoding tricks all share one flaw — they're reversible with no key — so let's test the other popular approach: an actual minifier that renames identifiers. I ran the script through python-minifier with global and local renaming turned on. This is the real, unedited output:

A='PRO-2026-ZQX'
C='sk-live-4a91f7c2e8d0'
def B(key):
	if key==A:return'unlocked'
	return'locked'
print(B(A))
python-minifier output (rename_globals + rename_locals)

Renaming did something real — check_license became B, API_KEY became C. But look at what it *didn't* touch: the API key is right there as 'sk-live-4a91f7c2e8d0'. The license code is right there as 'PRO-2026-ZQX'. The control flow is intact and trivially readable. No reversal step at all — I just *read* it.

Renaming-only obfuscation hides your *variable names*, which were never the secret. Your string literals — keys, tokens, URLs, license codes — stay in plaintext. This is the single most common gap in "obfuscated" Python I see.

The scoreboard

MethodReversalTimeSecrets exposed?
base64 + execone b64decode call0.004 msYes — all, plaintext
zlib + base64decompress + decode0.010 msYes — all, plaintext
50 nested layersa 5-line decode loop~3.5 s*Yes — all, plaintext
marshal bytecodemarshal.loads + dis0.417 msYes — read off the code object
minify + renamenone — just read itinstantYes — strings untouched

*The 3.5 seconds for 50 layers is almost entirely the cost of moving a needlessly-created 300 MB file around — not any actual difficulty. Every method returned working, byte-accurate source, and every method left the API key and license string in the clear.

So is Python obfuscation secure? The honest answer

Python is compiled and run on the user's machine, so the code — in *some* form — has to be present for the interpreter to execute it. That means no obfuscation is truly unbreakable; a determined attacker with enough time can always recover behavior. Anyone who tells you otherwise is selling something.

But that's not the same as "obfuscation is pointless." The realistic goal isn't *impossible*, it's *not worth it* — raising the cost of reversing your code far above the value of doing so. The five methods above fail because they raise the cost by roughly zero. What actually moves the needle:

  1. Encrypt the string literals, don't just encode the file. The reason every test above leaked its secrets is that the strings were never protected. Real obfuscation transforms literals so a text search and a dis dump turn up nothing usable.
  2. Rename at the AST level, together with that encryption — so the disassembly is both anonymous *and* stripped of readable constants, not just short-named. Renaming alone (Method 5) is the trap.
  3. Keep true secrets off the client entirely. An API key or license authority that lives on your server can't be extracted from a download, no matter how it's wrapped. Validate licenses server-side.
  4. For the strongest bar, compile to a native module — a Cython .pyd/.so or a Nuitka binary — so there's no bytecode to marshal.loads or dis in the first place.

Our free Python Obfuscator does the first two — AST-level renaming *with* string encryption — so a decompile or disassembly of the result doesn't hand over your logic and keys the way every method on this page did. It's the difference between "looks scrambled" and "is actually scrambled." Pair it with server-side secrets and, for critical code, compiling to a binary, and you've moved from "cracked in 0.004 ms" to "not worth anyone's afternoon."

Rule of thumb: if the technique has no key, it's encoding, not protection. If your secret strings survive a plain text search of the output, you haven't obfuscated the thing that mattered.

Obfuscate so it survives this test — free

AST-level renaming plus real string encryption, so a decompile or dis dump gives up nothing. In your browser, no signup.

Open the Python Obfuscator

Free tools mentioned here

Frequently asked questions

Is Python obfuscation secure?

No obfuscation is unbreakable, because the code must be present on the machine that runs it. But the common DIY methods (base64/zlib encoding, marshal bytecode, stacking encode layers, minify-only renaming) are effectively no protection — each reverses to working source in well under a second and leaks string literals in plaintext. Real protection comes from encrypting string literals, keeping secrets server-side, and compiling to a native module — which raises the cost of reversing far above its value.

Can base64-obfuscated Python be reversed?

Instantly. base64 is a reversible encoding with no key, so the same base64.b64decode call that the exec() runs also recovers the original source — I measured it at 0.004 ms, byte-identical, with every secret in plaintext. The same goes for zlib+base64 and for stacking dozens of encode layers.

Does exec(marshal.loads(...)) protect my code?

No. The marshaled payload is a compiled code object — exactly what the interpreter runs. marshal.loads returns it, Python’s built-in dis disassembles it, and its string constants (keys, license codes) are readable directly off the object. In my test that took 0.417 ms with no decompiler. A decompiler like pycdc can rebuild full .py source if you want it.

Is minifying or renaming variables enough to obfuscate Python?

No. Renaming identifiers hides variable names, which were never the secret. Your string literals — API keys, tokens, license strings — remain in plaintext, and the control flow stays readable. In my test, python-minifier renamed check_license to B but left the API key and license code sitting in the output verbatim.

What actually protects Python source code?

Encrypt the string literals (not just encode the file) so a text search and disassembly reveal nothing; rename identifiers at the AST level alongside that encryption; keep real secrets off the client and validate licenses server-side; and for the strongest bar compile to a native .pyd/.so or a Nuitka binary so there is no bytecode to disassemble.

Can any Python obfuscation be truly unbreakable?

No. Because the interpreter needs the code to run it, a determined attacker with enough time can always recover behavior. The realistic goal is economic: make reversing cost far more effort than the code is worth. Strong obfuscation plus server-side secrets plus native compilation achieves that; encoding tricks do not.

Keep reading