safetensors
safetensors is Hugging Face's file format and library for storing model weights safely. PyTorch's default checkpoint format is pickle, and unpickling a downloaded file can execute arbitrary code; a .safetensors file is just a JSON header plus a raw byte buffer, so loading one can never run code. The format is also fast (memory-mapped, near zero-copy on CPU) and lazy: you can read one tensor, or one slice of one tensor, without touching the rest of the file. The Python package is a thin binding over a small Rust core, with helpers for torch, numpy, JAX, and TensorFlow.
The correct default for storing and sharing model weights in the PyTorch ecosystem: safe by construction, faster to load than pickle, and already the Hub standard. Just know it stores tensors, not checkpoints; training state still needs something else.
Use it if
- You load model weights you did not train yourself: safetensors removes the run-arbitrary-code risk that comes with torch.load on a stranger's pickle file
- You ship or consume Hub models: .safetensors is the default weight format across transformers and diffusers, so you are likely using it already
- You do sharded or distributed loading: lazy per-tensor access means each rank reads only its slice instead of every rank deserializing the full checkpoint
- Load time matters: memory-mapped loading is much faster than unpickling, especially for repeated cold starts of large models
- You need full training checkpoints: the format stores named tensors and string metadata only, so optimizer state dicts, RNG states, and epoch counters need torch.save or a proper checkpointing library alongside it
- Your target runtime is llama.cpp or ollama: that ecosystem runs on GGUF with quantization baked into the format, and safetensors files must be converted first
- You store anything that is not a tensor: arbitrary Python objects, nested configs, and custom classes do not fit; the only escape hatch is a flat string-to-string metadata dict
- Your models have tied weights and you use the raw save_file API: it rejects tensors that share memory, and you must know to use save_model or clone tensors first
Setup reality
pip install safetensors is painless: prebuilt wheels for all mainstream platforms, no dependencies at all in the base install. The gotchas are conceptual rather than build-related. Framework helpers are separate modules (safetensors.torch, safetensors.numpy, safetensors.flax) and the torch extra expects torch>=2.4 already installed. save_file raises on tensors that share memory, which is exactly what tied embeddings in language models do, so real models often need safetensors.torch.save_model instead. Tensors must be contiguous before saving. And safe_open is a context manager whose tensors are only valid inside the with block pattern people expect from files.
Patterns
Save a dict of torch tensorssave-tensors
import torch
from safetensors.torch import save_file
tensors = {
"weight1": torch.zeros((1024, 1024)),
"weight2": torch.zeros((1024, 1024)),
}
save_file(tensors, "model.safetensors")save_file raises if any two tensors share memory (tied weights) or are non-contiguous; call .contiguous() or use save_model for real models.
Load a file back into a dictload-tensors
from safetensors.torch import load_file
tensors = load_file("model.safetensors")
print(tensors["weight1"].shape)load_file materializes every tensor; for large files where you need a subset, safe_open is the lazy alternative.
Read only specific tensors lazilylazy-load-subset
from safetensors import safe_open
with safe_open("model.safetensors", framework="pt", device="cpu") as f:
names = f.keys()
embedding = f.get_tensor("model.embed_tokens.weight")Only the requested tensor's bytes are read from disk; framework is 'pt', 'np', 'tf', 'flax', or 'paddle'.
Load tensors directly onto a GPUload-to-gpu
from safetensors.torch import load_file
tensors = load_file("model.safetensors", device="cuda:0")Skips the CPU staging copy that torch.load plus .to('cuda') would do; safe_open accepts the same device argument.
Read a slice of one tensor without loading it allslice-tensor
from safetensors import safe_open
with safe_open("model.safetensors", framework="pt") as f:
sl = f.get_slice("model.embed_tokens.weight")
print(sl.get_shape())
part = sl[0:1000] # only these rows are read from diskThis is the primitive that makes tensor-parallel loading cheap: each rank slices its own shard straight from the file.
Attach metadata to a checkpointsave-with-metadata
from safetensors.torch import save_file
save_file(tensors, "model.safetensors", metadata={"format": "pt", "step": "12000"})
from safetensors import safe_open
with safe_open("model.safetensors", framework="pt") as f:
print(f.metadata()) # {'format': 'pt', 'step': '12000'}Metadata is strictly string-to-string; numbers and nested structures must be serialized to strings yourself.
Save a model with tied weights correctlysave-whole-model
from safetensors.torch import save_model, load_model
save_model(model, "model.safetensors")
model = MyModel()
load_model(model, "model.safetensors")save_model resolves shared tensors by dropping duplicates that load_model restores; save_file(model.state_dict(), ...) fails on the same model.
Save and load numpy arraysnumpy-roundtrip
import numpy as np
from safetensors.numpy import save_file, load_file
save_file({"embeddings": np.zeros((100, 768), dtype=np.float32)}, "emb.safetensors")
arrays = load_file("emb.safetensors")Files are framework-neutral: something saved from numpy loads fine through safetensors.torch and vice versa, dtypes permitting.
Convert a torch .bin checkpoint to safetensorsconvert-pickle-checkpoint
import torch
from safetensors.torch import save_file
state = torch.load("pytorch_model.bin", map_location="cpu", weights_only=True)
state = {k: v.contiguous() for k, v in state.items()}
save_file(state, "model.safetensors")Keep weights_only=True so the conversion itself does not execute pickle code; this is the one time you still touch torch.load.
List tensor names, shapes, and dtypes without loadinginspect-header
from safetensors import safe_open
with safe_open("model.safetensors", framework="pt") as f:
for name in f.keys():
sl = f.get_slice(name)
print(name, sl.get_shape(), sl.get_dtype())Reads only the JSON header, so it is instant even on multi-gigabyte files; useful for verifying a download before loading.
Serialize to bytes instead of a filein-memory-bytes
from safetensors.torch import save, load
blob = save({"w": torch.ones(4)}) # bytes
tensors = load(blob)Handy for sending weights over a network or stashing them in object storage without a temp file.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| torch | PyPI | You need full checkpoints with optimizer state and arbitrary objects, and you control both ends of the file. |
| gguf | PyPI | You target llama.cpp and friends, where quantized single-file GGUF is the native format. |
| h5py | PyPI | You want hierarchical scientific array storage with compression, not just flat model weights. |
| onnx | PyPI | You need to ship the computation graph together with the weights for portable inference. |