Python pickletools: Analyze Pickles

Published on: August 5, 2026
Reading time: 6 minutes
Monitor with binary code representing pickle opcode analysis with Python pickletools

The pickle format represents Python objects as a stream of instructions. Loading a file with pickle.load() executes those instructions and can import functions or call constructors, so untrusted data creates a code-execution risk. The Python pickletools module disassembles the stream into readable opcodes without executing the pickle, helping developers study protocols, investigate files, and optimize serialized data.

This guide covers the command-line interface, dis(), genops(), and optimize(), including their security limits. It complements our articles about Python pickle, bytecode with dis, shelve, tracebacks, and file comparison.

Why pickle requires caution

A pickle is not a passive data container. Its stream contains operations used to reconstruct objects and may reference globals, callables, and reduction mechanisms.

import pickle

with open("data.pickle", "rb") as file:
    obj = pickle.load(file)

This is safe only when the file comes from a fully trusted source and its integrity is protected. Never load a random upload, attachment, or downloaded file.

The role of pickletools

Pickletools interprets the format structure and displays opcodes without executing object reconstruction.

The official pickletools documentation says the module is primarily useful to pickle and Python core developers, but its functions also support auditing and education.

Create a test pickle

import pickle

content = {
    "name": "Ana",
    "scores": [10, 20, 30],
}

data = pickle.dumps(
    content,
    protocol=pickle.HIGHEST_PROTOCOL,
)

with open("example.pickle", "wb") as file:
    file.write(data)

Use locally generated data while learning. Do not call pickle.loads() merely to discover the content of an unknown file.

Disassemble from the command line

python -m pickletools example.pickle

The output shows position, opcode byte, name, argument, and other information. A summary reports the highest protocol required by the encountered instructions.

pickletools versus python -m pickle

python -m pickle loads the object to display its representation. It executes the pickle and is inappropriate for untrusted files.

python -m pickletools disassembles the format and is the safer choice for structural inspection. Resource limits are still necessary for hostile inputs.

Annotate opcodes

The -a option adds a short description to each line.

python -m pickletools -a example.pickle

Annotations help explain operations such as PROTO, FRAME, MARK, MEMOIZE, BINUNICODE, and STOP.

Write disassembly to a file

python -m pickletools \
  -o report.txt \
  example.pickle

Do not treat the text report as sanitized data. It can still contain strings, names, and sensitive values present in the original stream.

Control indentation

-l sets the number of spaces used for each level introduced by a MARK opcode.

python -m pickletools -l 2 example.pickle

Indentation is visual only and does not modify the pickle.

Several files and preambles

The -p option prints text before each file.

python -m pickletools \
  -p '=== next pickle ===' \
  a.pickle b.pickle

This makes concatenated reports easier to read.

Preserve memo across files

-m keeps the memo while disassembling several streams.

python -m pickletools -m part1.pickle part2.pickle

Use it only when the files were produced by a compatible process sharing memo state. Independent files should normally be analyzed separately.

Read from standard input

cat example.pickle | python -m pickletools -

Do not mix binary data and log messages on the same stream. Enforce input-size limits before forwarding uploaded data.

Use dis() programmatically

pickletools.dis() writes symbolic disassembly to a file-like object.

import io
import pickletools

output = io.StringIO()
pickletools.dis(
    data,
    out=output,
    annotate=40,
)

print(output.getvalue())

The pickle argument may be bytes or a file-like object. Output defaults to sys.stdout.

Programmatic memo

The memo parameter accepts a dictionary shared among disassemblies.

memo = {}

pickletools.dis(data_a, memo=memo)
pickletools.dis(data_b, memo=memo)

The memo represents objects stored by protocol operations. Sharing it among unrelated streams can produce incorrect interpretation.

Iterate opcodes with genops()

genops() yields (opcode, argument, position) triples.

for opcode, argument, position in pickletools.genops(data):
    print(
        position,
        opcode.name,
        argument,
    )

Each OpcodeInfo contains a name, code, documentation, stack behavior, minimum protocol, and other low-level metadata.

Create a structured report

def pickle_report(data: bytes):
    for opcode, argument, position in pickletools.genops(data):
        yield {
            "position": position,
            "opcode": opcode.name,
            "protocol": opcode.proto,
            "argument": repr(argument),
        }

Limit the length of repr(argument) because strings and byte blobs may be enormous.

Flag sensitive operations

An audit can highlight opcodes associated with globals and reconstruction calls.

WARNINGS = {
    "GLOBAL",
    "STACK_GLOBAL",
    "REDUCE",
    "BUILD",
    "OBJ",
    "INST",
    "NEWOBJ",
    "NEWOBJ_EX",
}

for opcode, argument, position in pickletools.genops(data):
    if opcode.name in WARNINGS:
        print("Warning:", position, opcode.name, argument)

An opcode allowlist or blocklist does not make unpickling safe. Combinations, extension codes, and seemingly permitted objects can still produce dangerous behavior.

Protocols

Pickle supports several protocol versions. Newer protocols may add frames, more efficient memoization, and better support for large objects.

import pickle

for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
    data = pickle.dumps({"x": 1}, protocol=protocol)
    names = [op.name for op, _, _ in pickletools.genops(data)]
    print(protocol, names)

A protocol number is not the same as a Python version, although interpreter releases determine which protocols are available.

PROTO and highest used protocol

The PROTO opcode declares a version, but disassembly also calculates the highest protocol required by actual operations. Older streams may not start with PROTO.

Tools should inspect the full sequence rather than relying only on the first bytes.

Frames

Modern protocols may divide the stream with FRAME. Frames help the unpickler process blocks and reduce read calls.

A declared frame size must not cause an analyzer to allocate unlimited memory. Process unknown inputs with file-size and memory limits.

The memo

The memo avoids serializing one object repeatedly and preserves shared references.

items = []
obj = [items, items]
data = pickle.dumps(obj, protocol=4)

pickletools.dis(data)

Disassembly shows storage and retrieval of the reference, explaining why the two list elements refer to the same object after unpickling.

Optimize with optimize()

pickletools.optimize() removes unused PUT opcodes and returns an equivalent stream.

optimized = pickletools.optimize(data)

print(len(data), len(optimized))

The result may use less storage and transmission time and may unpickle more efficiently. Optimize is not a sanitizer and still needs resource limits for unknown data.

Verify trusted data only

original = pickle.loads(data)
optimized_obj = pickle.loads(optimized)
assert original == optimized_obj

This test executes pickle and is acceptable only for data generated or authenticated by the same trusted system.

Authenticate internal pickles

When an application genuinely requires pickle persistence, protect integrity and authenticity with HMAC or a digital signature and keep keys separate.

A signature detects modification but does not make third-party content trustworthy. It only proves that a holder of the signing key approved the stream.

Prefer data-only formats at boundaries

For external input and interoperability, choose JSON, MessagePack, Protocol Buffers, or a database schema. These formats represent data rather than arbitrary Python reconstruction instructions.

Pickle is most appropriate for internal communication and persistence among trusted, compatible components.

Resource limits

Even without executing opcodes, analysis can consume resources through huge files, large arguments, or artificial opcode sequences.

Set a maximum file size, timeout, opcode count, and output length. Analyze uploads inside a subprocess with CPU and memory restrictions.

Isolated scanner example

from pathlib import Path


def analyze(path: Path, limit=10_000_000):
    if path.stat().st_size > limit:
        raise ValueError("File is too large")

    with path.open("rb") as file:
        for index, (op, arg, pos) in enumerate(
            pickletools.genops(file)
        ):
            if index > 100_000:
                raise ValueError("Too many opcodes")
            yield pos, op.name, arg

Accept only regular files under an authorized root and avoid unexpected symbolic links.

pickletools does not certify safety

Disassembly helps analysts understand a stream, but it does not prove that the stream is harmless. Final behavior depends on imported objects, reducers, extension registries, and code available in the environment.

The rule remains: never unpickle untrusted data.

Common mistakes

  • Using python -m pickle for an unknown file.
  • Loading an object after disassembly to confirm its content.
  • Trusting a small opcode blocklist.
  • Sharing memo state among independent files.
  • Producing unbounded argument output.
  • Treating optimize as sanitization.
  • Ignoring size and opcode-count limits.
  • Assuming permanent cross-version compatibility.

Best practices

  • Use pickletools for non-executing inspection.
  • Keep unknown files away from pickle.load.
  • Limit size, time, opcodes, and output.
  • Analyze hostile files in an isolated subprocess.
  • Use data formats for external interfaces.
  • Authenticate only internally generated pickles.
  • Record protocol and Python version.
  • Test optimization only with trusted data.

Conclusion

The Python pickletools module reveals the instructions inside a pickle without executing its bytecode. The command-line interface and dis() provide readable disassembly, genops() enables structured analysis, and optimize() removes unnecessary memo operations.

This visibility helps developers learn the format and investigate files, but it does not make pickle safe. Real protection comes from refusing to load unknown data, applying resource limits, and preferring declarative data formats at system boundaries.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Asynchronous Python code representing asyncio.eager_task_factory
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    asyncio.eager_task_factory: Reduce Task Overhead

    Learn Python asyncio.eager_task_factory to reduce scheduling overhead, understand ordering changes, and optimize short coroutines safely.

    Ler mais

    Tempo de leitura: 4 minutos
    14/09/2026
    Developer working with UTC timestamps and Python calendar.timegm
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    calendar.timegm: Convert UTC to Unix Timestamps

    Learn Python calendar.timegm to convert UTC date tuples into Unix timestamps safely and avoid local-time conversion bugs.

    Ler mais

    Tempo de leitura: 5 minutos
    14/09/2026
    Programmer analyzing code to identify file MIME types with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    mimetypes.guess_file_type: Detect MIME Types

    Learn Python mimetypes.guess_file_type for MIME detection in paths, URLs, uploads, and HTTP responses with safe fallbacks.

    Ler mais

    Tempo de leitura: 5 minutos
    13/09/2026
    Server components representing isolated Python interpreters running in parallel
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    InterpreterPoolExecutor: True Parallelism in Python

    Learn Python InterpreterPoolExecutor for CPU-bound tasks, isolated interpreters, true parallelism, and safer concurrency design.

    Ler mais

    Tempo de leitura: 6 minutos
    13/09/2026
    Microprocessor representing CPUs available to a Python process
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    os.process_cpu_count: Count Available CPUs

    Learn Python os.process_cpu_count to size workers according to the CPUs actually available to the process.

    Ler mais

    Tempo de leitura: 4 minutos
    12/09/2026
    Laptop with digital code representing SQLite BLOB data
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    sqlite3.Blob: Incremental BLOB Reads and Writes

    Learn Python sqlite3.Blob for incremental BLOB reads and writes, lower memory use, and safer binary data handling in SQLite.

    Ler mais

    Tempo de leitura: 5 minutos
    12/09/2026