Safeguard
AppSec

Python Pickle: Reading, Writing, and Why It Is a Security Risk

How to pickle an object in Python and read a pickle file back, and the reason the standard library itself warns you never to unpickle data you did not produce.

Aisha Rahman
Security Analyst
7 min read

To pickle an object in Python you call pickle.dump(obj, file) to write it and pickle.load(file) to read it back — but the moment that pickle data comes from anywhere you do not fully control, load becomes arbitrary code execution, which is why Python's own documentation warns you never to unpickle data received from an untrusted source. Pickle is a fast, convenient way to persist Python objects, and it is genuinely useful inside a trust boundary. The trouble is that its power — reconstructing arbitrary objects — is indistinguishable from its danger, and a lot of production incidents trace back to treating a pickle stream like it were JSON.

This guide shows the correct read and write patterns, then explains exactly why unpickling is dangerous and what to reach for instead.

Writing: how to pickle an object in Python

The core API is two functions and their in-memory twins:

import pickle

data = {"model": "griffin", "weights": [0.1, 0.4, 0.5], "epoch": 42}

# Write to a file — note the binary mode "wb"
with open("state.pkl", "wb") as f:
    pickle.dump(data, f)

# Or to a bytes object in memory
blob = pickle.dumps(data)

Points that trip people up:

  • Always open in binary mode (wb / rb). Pickle is a binary protocol; text mode corrupts it.
  • Pass protocol=pickle.HIGHEST_PROTOCOL for smaller, faster output. Protocol 5 (Python 3.8+) added out-of-band buffers for large data like NumPy arrays.
  • Not everything pickles. Open file handles, sockets, database connections, lambdas, and locks raise PicklingError or TypeError. If a class needs custom handling, implement __getstate__ / __setstate__ — but know that __setstate__ runs on load, which is part of the security story below.

Reading: how to read a pickle file in Python

The symmetric read side — this is the python read pickle file pattern you will see everywhere:

import pickle

with open("state.pkl", "rb") as f:
    data = pickle.load(f)

# From bytes
data = pickle.loads(blob)

That is the entire read pickle Python surface. load reads from a file object, loads from a bytes object. And this is precisely where you must stop and ask one question before typing either line: do I know who created this file? If the honest answer is no, you should not be calling pickle.load at all.

Why unpickling untrusted data is remote code execution

Pickle is not a data format in the way JSON is. It is a small stack-based virtual machine, and the bytecode it executes can call arbitrary Python callables during reconstruction. A class can define __reduce__, which pickle honors on load to decide how to rebuild the object — and __reduce__ can return "call this function with these arguments." Nothing stops that function from being os.system.

The proof-of-concept that every security team has seen looks like this, shown here only to make the mechanism concrete:

import pickle, os

class Evil:
    def __reduce__(self):
        return (os.system, ("id",))

payload = pickle.dumps(Evil())
# Anyone who calls pickle.loads(payload) runs `id` on their machine.

There is no exploit craft here — no memory corruption, no race, no clever gadget chain. The attacker is using pickle exactly as designed. pickle.loads sees "call os.system('id')" and complies, because reconstructing objects by calling things is the whole point of the format. Swap id for a reverse shell or a curl … | sh and the untrusted .pkl is a full remote-code-execution primitive. The CPython documentation states the consequence plainly: only unpickle data you trust, because it can execute arbitrary code.

Where this bites in real systems:

  • ML model files. Downloading a .pkl or a pickled PyTorch checkpoint from a public hub and loading it is running someone else's code. This is a live, actively-exploited class of supply-chain risk, which is why safer model formats exist (see below).
  • Caches and queues. Redis or a message broker holding pickled payloads becomes a code-execution channel the instant an attacker can write to it.
  • Session data. Any web framework that pickles session state into a client-visible cookie without integrity protection hands attackers the loader.

What to use instead

Match the tool to the trust boundary:

SituationUse
Data crosses a trust boundary (APIs, user uploads, config)JSON, or a schema format like Protobuf / Avro
ML model weightssafetensors, or a format designed to carry data not code
Internal cache, same process/trust domain, speed mattersPickle is acceptable — with integrity checks
Tabular / numeric persistenceParquet, NumPy .npy, or HDF5

For the internal-but-cautious case, sign the payload so a tampered pickle is rejected before it ever reaches loads:

import hmac, hashlib, pickle

KEY = b"...from your secrets manager..."

def dump_signed(obj):
    body = pickle.dumps(obj)
    sig = hmac.new(KEY, body, hashlib.sha256).digest()
    return sig + body

def load_signed(blob):
    sig, body = blob[:32], blob[32:]
    expected = hmac.new(KEY, body, hashlib.sha256).digest()
    if not hmac.compare_digest(sig, expected):
        raise ValueError("pickle signature mismatch — refusing to load")
    return pickle.loads(body)

This does not make pickle safe against a source you distrust — an attacker with the HMAC key still wins. It only guarantees the bytes were not altered in transit between two components you control. Restricting globals via a custom Unpickler.find_class is sometimes suggested as a sandbox, but treat it as defense in depth, not a boundary; the safe posture remains "do not unpickle untrusted input."

Catching it before it ships

Static analyzers flag pickle.load/loads on tainted input as a high-severity finding, and rightly so — Bandit reports it out of the box (its B301/B403 checks). But the harder version of the problem is transitive: a dependency deep in your tree unpickles something you never see. Cache libraries, ML serving frameworks, and task queues have all shipped pickle-based defaults. Dependency-aware scanning helps here — an SCA tool such as Safeguard can surface a transitive package whose known-vulnerable version deserializes untrusted pickle data, which no amount of reviewing your own code would reveal. The general deserialization risk class extends well past Python; the same "reconstruction executes code" pattern underlies advisories across ecosystems.

FAQ

Is pickle safe for saving my own data locally?

Yes — within a trust boundary, pickling your own objects to your own disk and loading them back is fine and fast. The danger is exclusively about the source of the data at load time. If a file could have been written or modified by anyone you do not trust, do not pickle.load it.

How do I read a pickle file in Python?

Open it in binary mode and call pickle.load: with open("f.pkl", "rb") as f: obj = pickle.load(f). For bytes already in memory, use pickle.loads(blob). Only do this for data you produced or fully trust.

Why is pickle a security risk when JSON is not?

JSON describes data; a parser reads values and stops. Pickle describes how to reconstruct objects, which includes calling functions — so a malicious pickle can invoke os.system during load. That capability is the format's design, not a bug, which is why it cannot be patched away.

What should I use instead of pickle for untrusted data?

JSON for general interchange, Protobuf or Avro when you want a schema, and safetensors for machine-learning weights. Each of these carries data without the ability to execute code on load.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.