Skip to main content
Obfuscation

Python `marshal` Explained: Bytecode Loaders and Why They’re Weak Protection

Pyobfuscate Team··8 min read
Quick Answer

Python's marshal module serializes a compiled code object into raw bytecode. Wrapping it as exec(marshal.loads(base64.b64decode(...))) lets you ship bytecode instead of readable source. But it is weak protection: the bytecode decompiles back to near-original code, it is version-locked to one CPython release, and antivirus often flags the loader. Use our free Marshal Encryptor, and obfuscate first for real protection.

A popular way to "hide" Python is to compile it, marshal.dumps() the code object, base64 it, and ship a tiny loader that execs the result. It looks impressive — a wall of base64 instead of your source. This guide explains exactly what marshal does, how to build such a loader, and the two things people always get wrong: it is version-locked, and it is not real protection. Every claim was tested on CPython.

TL;DR

  • marshal.dumps(compile(src)) turns source into a code object → bytecode blob.
  • A loader runs it with exec(marshal.loads(base64.b64decode(b"..."))) — no .py shipped.
  • The blob is version-locked: bytecode magic changes each minor version, so a 3.12 blob won't run on 3.11/3.13.
  • It is weak: decompilers (decompyle3, pycdc) rebuild near-original source, and AV flags exec(marshal.loads(...)).
  • Build it free with the Marshal Encryptor (choose Python 3.10–3.13) — and obfuscate first for meaningful protection.

What is `marshal` in Python?

marshal is a standard-library module that serializes internal Python objects — most usefully compiled code objects — into bytes. It's what CPython itself uses to write .pyc files. Unlike pickle, it isn't meant for general data and its format isn't guaranteed stable across versions, which matters a lot here.

import marshal, base64

src = 'def greet(name):\n    return f"Hello, {name}!"\nprint(greet("World"))'
code = compile(src, "<pyd>", "exec")
blob = base64.b64encode(marshal.dumps(code)).decode()

loader = f'import marshal, base64\nexec(marshal.loads(base64.b64decode("{blob}")))'
exec(loader)   # ->  Hello, World!
Compile → marshal → base64 → a self-contained loader (tested)

The loader is the whole "encrypted" artifact: a couple of imports and one exec. It runs your original program — but there's no readable source in it.

Why marshaled bytecode is version-locked

CPython stamps every bytecode format with a magic number that changes each minor version, and the opcodes themselves change too. A code object marshaled by one version won't reliably load — or will crash on execution — under another. That's why you can't build a "3.12 blob" on a 3.11 interpreter.

PythonBytecode magicLoader built on it runs on
3.103439Python 3.10.x only
3.113495Python 3.11.x only
3.123531Python 3.12.x only
3.133571Python 3.13.x only

This is exactly why our Marshal Encryptor makes you pick a target version — it loads the matching CPython runtime and marshals with it, so the bytecode magic is always correct for the Python you ship to.

How to create a marshal loader (free, in your browser)

  1. Open the Marshal Encryptor.
  2. Paste your Python source.
  3. Pick your target Python version (3.10, 3.11, 3.12, or 3.13) — it must match where you'll run the output.
  4. Click Marshal Encrypt and copy the loader.
  5. Run it only on that Python version. For real protection, run your code through the Python Obfuscator first, then marshal.

Is `marshal` secure? (No — here’s why)

Marshaling changes the *representation* of your code, not its recoverability. Three hard limits:

  • It decompiles. Tools like decompyle3 and pycdc reconstruct readable .py from marshaled bytecode — names and logic mostly intact.
  • Strings are in the clear. Your literals (API keys, URLs) sit inside the bytecode as plain text.
  • Antivirus hates it. exec(marshal.loads(base64.b64decode(...))) is a classic malware pattern, so legitimate apps can get quarantined.

Treat marshal as packaging, not protection. The strong move is to obfuscate first (rename + encrypt strings at the AST level), *then* marshal — so even decompiled bytecode is unreadable.

`marshal` vs `.pyc` vs `pickle`

PurposeCross-version?Safe on untrusted input?
marshalSerialize code objects (bytecode)No — version-lockedNo
.pycCached bytecode + a magic/headerNo — version-lockedNo
pickleSerialize general Python objectsMostly, with careNo — can execute code

For shipping code, marshal and .pyc are two views of the same thing (bytecode); neither protects source. pickle is for data, not code, and is unsafe on untrusted input.

Common mistakes

  • Shipping a blob for the wrong Python version — it'll fail to load or crash. Match the target exactly.
  • Assuming it's secure — it decompiles; obfuscate first.
  • Leaving secrets in the code — they survive in the bytecode verbatim.
  • Ignoring antivirus — test your build against AV before distributing.

Conclusion

marshal is a neat way to ship Python as bytecode, and it's genuinely useful for packaging — but it's a light deterrent, not a lock. Pick the right target version, don't rely on it alone, and pair it with real AST obfuscation. Build a loader in seconds with the free Marshal Encryptor.

Build a marshal bytecode loader — free

Compile Python to version-locked marshaled bytecode (3.10–3.13) in your browser. No install, no signup.

Open the Marshal Encryptor

Free tools mentioned here

Frequently asked questions

What does Python marshal do?

marshal serializes internal Python objects — most usefully compiled code objects (bytecode) — into bytes. It is what CPython uses internally to write .pyc files. It is not intended for general data serialization or untrusted input.

How do I turn Python code into a marshal loader?

Compile the source to a code object, marshal.dumps() it, base64-encode the bytes, and emit exec(marshal.loads(base64.b64decode(b"..."))). Our free Marshal Encryptor does this in the browser for Python 3.10–3.13.

Does marshaled bytecode run on any Python version?

No. It is version-locked — CPython bytecode magic and opcodes change each minor version, so a blob built for 3.12 will not load or run on 3.11 or 3.13. Build it for the exact version you will run.

Is marshal secure for protecting Python code?

No. Marshaled bytecode decompiles back to near-original source with tools like decompyle3 or pycdc, string literals stay in the clear, and exec(marshal.loads(...)) trips antivirus. Obfuscate first for real protection.

What is the difference between marshal and pickle?

marshal serializes code objects/bytecode and is version-locked; pickle serializes general Python objects and is more portable. Both are unsafe on untrusted input, but for shipping code you use marshal (or .pyc), not pickle.

Can antivirus flag a marshal loader?

Yes. exec(marshal.loads(base64.b64decode(...))) is a common malware obfuscation pattern, so AV engines may flag or quarantine even legitimate apps that ship this way. Test against antivirus before distributing.

Should I use marshal or obfuscation?

Use both, in order: obfuscate the source first (rename identifiers, encrypt strings) so decompiled bytecode is unreadable, then marshal to ship bytecode. Marshal alone is packaging, not protection.

Keep reading