What is pysafe-pickle?
Python's built-in pickle module is the standard way to serialize and deserialize arbitrary Python objects. However, it has one fatal flaw: it is fundamentally insecure.
# WARNING: Standard pickle allows arbitrary code execution!
import pickle
# A malicious payload can execute any shell command upon unpickling:
class Malicious:
def __reduce__(self):
import os
return (os.system, ("echo 'System compromised!'",))
pickle.loads(pickle.dumps(Malicious()))According to the official Python documentation:
"Warning: The
picklemodule is not secure. Only unpickle data you trust."
In modern distributed computing, microservices, RPC architectures, and AI model caching, data often travels over networks or originates from untrusted sources.
Why pysafe-pickle?
pysafe-pickle was built to give you Python object graph serialization without the code-execution hazard of pickle.
1. No Code Execution on Load
During deserialization, pysafe-pickle never calls __reduce__, __reduce_ex__, __setstate__, __init__, __post_init__, exec, or eval, and it never imports a module named in the data. Objects are built only by fixed reconstructors for the supported types. Your own classes are used only when you pass them in classes=, and dataclass instances are created without calling __init__.
2. A Fixed Set of Supported Types
Only the following types can be serialized and deserialized:
None,bool,int(arbitrary size),float,complexstr(including strings with lone surrogates),bytes,bytearraylist,tuple,dict,set,frozensetdatetime.datetime,date,time,timedelta,timezone,zoneinfo.ZoneInfodecimal.Decimal,uuid.UUID,fractions.Fractionrange,slice,Ellipsis,NotImplementedpathlibpaths (PurePosixPath,PureWindowsPath,PosixPath,WindowsPath)collections.OrderedDict,Counter,deque(withmaxlen),defaultdict(factoryNoneor a builtin type such aslist,int,set)@dataclassinstances, namedtuples, andEnummembers (IntEnum,StrEnum,Flag, ...)
Anything else (subclasses of the types above other than enums and namedtuples, custom tzinfo classes, non-dataclass custom classes, functions, NumPy arrays, PyTorch tensors, ...) raises UnsafeTypeError: Unsupported type: ... at dumps time. You can additionally pass an allowlist to loads to restrict which dataclass, enum and namedtuple names may appear; see Security & Allowlist.
3. Rust Core, Honest Performance
Graph traversal, cycle detection, binary encoding, and decoding are implemented in Rust via PyO3. Compared with stdlib pickle (protocol 5):
dumpsis faster on most data shapes, and much faster on dataclass-heavy payloads.loadsis faster on dataclass-heavy data, roughly on par on many shapes, and slower on some (large dicts, int lists, shared-reference graphs).- Serialized payloads are larger than pickle's (typically 1.0x to 2.8x), since there is no compression and each payload carries a string table and a type table.
Results depend on the data shape. See Benchmarks and measure with your own data.
4. Schema Versions and Migrations
Dataclasses can declare __pysafe_pickle_version__, which is stored in the payload, and you register migration functions with @psp.migrate. When you load with classes=[YourClass] and the stored version differs from the class's version, the migrations run automatically. See Schema Evolution.
5. Integrity
dumps(obj, hmac_key=key) appends an HMAC-SHA256 tag, and loads(data, hmac_key=key) verifies it before decoding anything. See Security & Allowlist.
6. A Way Out of Existing Pickle Files
psp.load_pickle reads standard .pkl files with a restricted interpreter that only calls known-safe constructors, and psp.convert_pickle turns them into pysafe-pickle payloads. See Reading Existing Pickle Files.
Things to Know
- Pass
classes=to get your classes back. Without it, a loaded dataclass is an instance of a generated class with the same__name__and fields, andisinstance(obj, User)isFalse. Enum members needclasses=to load at all. __init__and__post_init__are not called on load, so validation in them does not run. Missing fields get their dataclass defaults.ClassVarandInitVarpseudo-fields are not serialized.- Protocol 5 only.
dumps(..., protocol=4)raisesValueError. - No NumPy / PyTorch / zero-copy support, and classes that need
__reduce__or a custom constructor cannot be serialized. - Payloads are larger than pickle's, and
loadsis slower than pickle on some shapes. - Free-threaded Python builds are not supported.
Comparison Table
| Feature | Standard pickle | JSON / msgpack | pysafe-pickle |
|---|---|---|---|
| Arbitrary Code Execution Risk on Load | ⚠️ High | ✅ None | ✅ None |
| Circular Reference Handling | ✅ Yes | ❌ No | ✅ Yes |
| Shared Object References | ✅ Yes | ❌ No | ✅ Yes |
Python @dataclass Support | ✅ Yes | ⚠️ Manual | ✅ Yes (your class with classes=) |
datetime, Decimal, UUID, enums | ✅ Yes | ⚠️ Manual | ✅ Yes |
| Arbitrary Custom Classes | ✅ Yes | ❌ No | ❌ No (dataclasses, enums, namedtuples) |
| Schema Versions / Migrations | ❌ No | ❌ No | ✅ Yes (automatic with classes=) |
| PEP 574 Out-of-band Buffers | ✅ Yes | ❌ No | ❌ No |
| Tamper Detection (HMAC) | ❌ No | ❌ No | ✅ Yes (HMAC-SHA256) |
Reads Existing .pkl Files | ✅ (unsafely) | ❌ No | ✅ Safely, allowlisted types only |
| Payload Size | Smaller | Varies | Larger than pickle |
| Speed / Language Core | C / Python | C / Rust | Rust (PyO3) |