skip to content

Is marshal.loads a safer alternative to pickle.loads for untrusted bytes?

level: middleimportance: nice to knowfreq 12%

answer

  1. Lower level does not mean better defended
  2. It is the format behind compiled bytecode caches
  3. Only built-in types, but code objects included
  4. The reader assumes the interpreter wrote the bytes
  5. allow_code is portability, not a sandbox

basics

~20 s

No. marshal is CPython's internal format for compiled bytecode, and its documentation says it is not secure against maliciously constructed data. It carries code objects, and its reader offers no promise of failing cleanly on hostile input.

solid answer

~50 s

It is a lateral move, not a fix. `marshal` handles only a fixed set of built-in types plus code objects, so unlike `pickle` it never imports a module or calls a constructor the payload chose — which is why people mistake it for the safe option. But it is CPython's internal format for `.pyc` bodies: undocumented, unstable between releases, and explicitly documented as not secure against erroneous or maliciously constructed data. The hazards are that a payload can hand you a code object, which is harmless until the program does the natural thing and executes it, and that the reader assumes it is parsing bytes the interpreter wrote, so crafted input can crash rather than raise. Python 3.13 added `allow_code=False` to `marshal.loads` for cross-version portability; it refuses code objects but does not harden the parser.

code

python · 10 lines
python
import marshal

blob = marshal.dumps(compile("print('from a marshalled code object')", "<payload>", "exec"))
code = marshal.loads(blob)
exec(code)

try:
    marshal.loads(blob, allow_code=False)
except ValueError as exc:
    print("refused:", exc)

go deeper

for a junior

You will not be marked down for missing this, but remember the shape of the answer: swapping one binary object format for another does not make untrusted input safe, and JSON is the boring correct choice at a trust boundary.

for a middle

Be able to say what marshal is for — the body of compiled bytecode caches — and why that origin means its reader was written for a trusted producer. Know that allow_code=False, added in 3.13, is a portability guard rather than a sandbox.

for a senior

Recognise the pattern in a codebase: unmarshal-then-execute is how bytecode caches load, so a writable cache directory or a shared build artefact is a code-execution path. Argue the fix as a format change, not a flag.

for a principal

Push back on the instinct that a lower-level format is a safer one, and set the rule that formats crossing trust boundaries need an independent hardened parser and an owned schema. Decide where authentication substitutes while formats are migrated.

## Why anyone asks `marshal` looks like the safe cousin of `pickle`. It has the same `dumps` / `loads` shape, it is fast, and — unlike pickle — it genuinely cannot be made to import a module of the payload's choosing or call a constructor you did not expect. It handles a fixed set of built-in types: `None`, booleans, integers, floats, complex numbers, `str`, `bytes`, tuples, lists, dictionaries, sets, frozensets, and code objects. There is no `REDUCE`-style opcode and no `__reduce__` hook. So the reasoning goes: no arbitrary calls, therefore safe for untrusted input. The answer is still no, for reasons that have nothing to do with pickle's opcode machine. ## What marshal is for `marshal` is CPython's *internal* serialization format. Its main job is the body of a `.pyc` file: when the interpreter caches compiled bytecode, the code object is written with marshal and read back with marshal. The documentation states plainly that the format is undocumented, that it is not intended to be stable between Python versions, and that it is **not intended to be secure against erroneous or maliciously constructed data**, with an explicit instruction never to unmarshal data from an untrusted or unauthenticated source. That first pair of properties alone disqualifies it as a data-interchange format even ignoring security. Bytes written by one release may be unreadable by the next, so anything you persist with it is coupled to the interpreter that wrote it. `marshal.version` reflects the current format revision, and it has changed across releases. ## The two real hazards **Code objects.** Marshal can carry a code object, so a payload can hand you compiled bytecode. Loading it does not by itself run anything — this is the nuance to get right, because it is routinely stated wrongly in both directions. What makes it dangerous is what a program naturally does next: an object of that kind exists to be executed, and any code path that passes it to `exec`, wraps it in a function, or hands it to an import machinery hook runs whatever the producer compiled. A loader-shaped program that unmarshals and executes is the entire mechanism of `.pyc` loading, which is why a writable bytecode cache is a code-execution path on any interpreter. **The reader itself.** The parser is written on the assumption that it is reading bytes CPython wrote. Type codes, lengths and back-references are taken largely at face value, and hostile input aimed at that reader has historically produced crashes rather than clean exceptions. There is no promise of a well-behaved failure, which is exactly what the documentation's warning is telling you. ## What `allow_code` did and did not add Python 3.13 added an `allow_code` parameter to `marshal.dumps` and `marshal.loads`, still present in 3.14. It defaults to true; passing `allow_code=False` makes the functions refuse to serialize or deserialize code objects, raising `ValueError`. Understand what motivated it: cross-version portability. If you use marshal for simple data structures, refusing code objects means you cannot accidentally embed something whose format is tied to one interpreter version. It is a compatibility guard that happens to remove the most obviously dangerous type, and it is worth setting on any load you cannot avoid. It does not make the module a hardened parser, it does not address malformed input aimed at the reader, and it does not turn marshal into a format you should accept from strangers. ## What to say instead For data crossing a trust boundary, use a data-only format with an independent, hardened parser — JSON is the usual answer — and construct your objects from the parsed primitives with a function you wrote. If you need compactness, a schema-checked binary format with a real parser is the right category. Neither pickle nor marshal belongs on that boundary, and swapping one for the other is a lateral move, not a fix. For data moving between components you control, the question is authentication rather than parsing: sign the bytes with an HMAC keyed from a secret store and verify before loading. That is the same answer as for pickle, because it is the same problem. The interview point is smaller than the explanation: candidates reach for marshal because it is "lower level", assuming lower level means simpler and simpler means safer. Here it means the opposite. Lower level means the parser was written for a trusted producer — the interpreter itself — and inherits none of the defensive posture you would expect from a format designed to be handed strangers' bytes.

  • Does marshal.loads execute a code object that appears in the payload?
    No — loading builds the object without running it. The danger is what a program does next, because such an object exists to be executed: anything that passes it to exec, wraps it in a function, or feeds it to import machinery runs whatever the producer compiled. That pattern is exactly how a bytecode cache is loaded, which is why a writable cache directory is a code-execution path.
  • If not marshal, what should carry data across a trust boundary?
    A data-only format with a hardened, independent parser — JSON in most cases — with your own constructor mapping parsed primitives to objects, so a hostile document produces a validation error rather than a call. Where compactness matters, use a schema-checked binary format with a real parser. Between components you control, sign the bytes with an HMAC and verify before loading.

saying these in an interview costs you the question

  • Calls marshal safe because it has no __reduce__ hook
  • Assumes a lower-level format is a better-defended one
  • Treats allow_code=False as a security sandbox
  • Says marshal.loads runs the code object it decodes
  • Uses marshal as a durable interchange format across versions

context