Serialization and memory-mapped loading#

Use case#

joblib.dump() and joblib.load() provide numpy-aware serialization optimized for memory-efficient loading in multiprocess environments. Large numpy arrays are stored as raw binary buffers that can be memory-mapped on load, so multiple worker processes share a single copy of the data instead of each holding a full in-memory duplicate.

The canonical use case is serving a scikit-learn model under a multiprocess server (e.g. gunicorn): dump the fitted model once, then each worker memory-maps the large coefficient arrays rather than pickling them across a pipe. This avoids duplicating potentially gigabytes of data per worker.

Security considerations#

Because joblib.load() is designed for controlled, internal pipelines — not for distributing models to end users — it only makes safety guarantees for files you produced yourself. It is intended for use cases such as caching intermediate results or sharing objects between worker processes, where the file was written by your own code or by collaborators you control.

Warning

joblib.dump() and joblib.load() are built on Python’s pickle protocol, which can execute arbitrary Python code during deserialization. Loading a file from an untrusted source is equivalent to running untrusted code. This is a fundamental property of pickle, not a joblib-specific bug.

Never call joblib.load() on a file you did not produce yourself or that came from a source you do not fully trust.

Scope of joblib’s security guarantees#

Joblib focuses on parallelism, caching, and distributed computation. It does not aim to be a safe model-exchange format. Consequently:

  • Joblib will not attempt to sandbox or restrict what pickle can deserialize — such restrictions are fragile by design and cannot be made reliable.

  • Security reports that require an attacker-controlled joblib file as their starting point are outside joblib’s threat model: if the file is malicious, the user should not have loaded it.

  • We may still fix behaviors that are surprising or unnecessarily dangerous (e.g. inconsistencies in how companion files are resolved), but these fixes aim for correctness, not for making untrusted-file loading safe.

Sharing models securely#

If you need to distribute or receive serialized models across a trust boundary (e.g. from Hugging Face, a third-party vendor, or an end user), use a format that was designed for safe loading. We recommend skops.io, which provides a pickle-free serialization format for scikit-learn compatible models with an explicit audit step before loading.

If you must load a joblib file of uncertain provenance, inspect it first with a tool such as fickling or skops.io before loading, then load it only if it is clean.

Note

As of Python 3.8 and numpy 1.16, pickle protocol 5 introduced in PEP 574 supports efficient serialization and de-serialization for large data buffers natively using the standard library:

pickle.dump(large_object, fileobj, protocol=5)

A simple example#

First create a temporary directory:

>>> from tempfile import mkdtemp
>>> savedir = mkdtemp()
>>> import os
>>> filename = os.path.join(savedir, 'test.joblib')

Then create an object to be persisted:

>>> import numpy as np
>>> to_persist = [('a', [1, 2, 3]), ('b', np.arange(10))]

which is saved into filename:

>>> import joblib
>>> joblib.dump(to_persist, filename)
['...test.joblib']

The object can then be reloaded from the file:

>>> joblib.load(filename)
[('a', [1, 2, 3]), ('b', array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))]

Persistence in file objects#

Instead of filenames, joblib.dump() and joblib.load() functions also accept file objects:

>>> with open(filename, 'wb') as fo:
...    joblib.dump(to_persist, fo)
>>> with open(filename, 'rb') as fo:
...    joblib.load(fo)
[('a', [1, 2, 3]), ('b', array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))]

Compressed joblib pickles#

Setting the compress argument to True in joblib.dump() will allow to save space on disk:

>>> joblib.dump(to_persist, filename + '.compressed', compress=True)
['...test.joblib.compressed']

If the filename extension corresponds to one of the supported compression methods, the compressor will be used automatically:

>>> joblib.dump(to_persist, filename + '.z')
['...test.joblib.z']

By default, joblib.dump() uses the zlib compression method as it gives the best tradeoff between speed and disk space. The other supported compression methods are ‘gzip’, ‘bz2’, ‘lzma’ and ‘xz’:

>>> # Dumping in a gzip compressed file using a compress level of 3.
>>> joblib.dump(to_persist, filename + '.gz', compress=('gzip', 3))
['...test.joblib.gz']
>>> joblib.load(filename + '.gz')
[('a', [1, 2, 3]), ('b', array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))]
>>> joblib.dump(to_persist, filename + '.bz2', compress=('bz2', 3))
['...test.joblib.bz2']
>>> joblib.load(filename + '.bz2')
[('a', [1, 2, 3]), ('b', array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))]

The compress parameter of the joblib.dump() function also accepts a string corresponding to the name of the compressor used. When using this, the default compression level is used by the compressor:

>>> joblib.dump(to_persist, filename + '.gz', compress='gzip')
['...test.joblib.gz']

Note

Lzma and Xz compression methods are only available for python versions >= 3.3.

Compressor files provided by the python standard library can also be used to compress pickle, e.g gzip.GzipFile, bz2.BZ2File, lzma.LZMAFile:

>>> # Dumping in a gzip.GzipFile object using a compression level of 3.
>>> import gzip
>>> with gzip.GzipFile(filename + '.gz', 'wb', compresslevel=3) as fo:
...    joblib.dump(to_persist, fo)
>>> with gzip.GzipFile(filename + '.gz', 'rb') as fo:
...    joblib.load(fo)
[('a', [1, 2, 3]), ('b', array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))]

If the lz4 package is installed, this compression method is automatically available with the dump function.

>>> joblib.dump(to_persist, filename + '.lz4')
['...test.joblib.lz4']
>>> joblib.load(filename + '.lz4')
[('a', [1, 2, 3]), ('b', array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))]

Note

LZ4 compression is only available with python major versions >= 3

More details can be found in the joblib.dump() and joblib.load() documentation.

Registering extra compressors#

Joblib provides joblib.register_compressor() in order to extend the list of default compressors available. To fit with Joblib internal implementation and features, such as joblib.load() and joblib.Memory, the registered compressor should implement the Python file object interface.

Compatibility across python versions#

Compatibility of joblib pickles across python versions is not fully supported. Note that, for a very restricted set of objects, this may appear to work when saving a pickle with python 2 and loading it with python 3 but relying on it is strongly discouraged.

If you are switching between python versions, you will need to save a different joblib pickle for each python version.

Here are a few examples or exceptions:

  • Saving joblib pickle with python 2, trying to load it with python 3:

    Traceback (most recent call last):
      File "/home/lesteve/dev/joblib/joblib/numpy_pickle.py", line 453, in load
        obj = unpickler.load()
      File "/home/lesteve/miniconda3/lib/python3.4/pickle.py", line 1038, in load
        dispatch[key[0]](self)
      File "/home/lesteve/miniconda3/lib/python3.4/pickle.py", line 1176, in load_binstring
        self.append(self._decode_string(data))
      File "/home/lesteve/miniconda3/lib/python3.4/pickle.py", line 1158, in _decode_string
        return value.decode(self.encoding, self.errors)
    UnicodeDecodeError: 'ascii' codec can't decode byte 0x80 in position 1024: ordinal not in range(128)
    
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/home/lesteve/dev/joblib/joblib/numpy_pickle.py", line 462, in load
        raise new_exc
      ValueError: You may be trying to read with python 3 a joblib pickle generated with python 2. This is not feature supported by joblib.
    
  • Saving joblib pickle with python 3, trying to load it with python 2:

    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "joblib/numpy_pickle.py", line 453, in load
        obj = unpickler.load()
      File "/home/lesteve/miniconda3/envs/py27/lib/python2.7/pickle.py", line 858, in load
        dispatch[key](self)
      File "/home/lesteve/miniconda3/envs/py27/lib/python2.7/pickle.py", line 886, in load_proto
        raise ValueError, "unsupported pickle protocol: %d" % proto
    ValueError: unsupported pickle protocol: 3