Sitemap
Diego Prayudha

Diego Prayudha

Trying to write about AI and other tech things I'm learning along the way.

Safetensor: Is It Real Safe?

Understanding Safetensors and Why So Many AI Models Use It

8 min read12 hours ago

--

Press enter or click to view image in full size
Photo by Markus Spiske on Unsplash

Have you ever downloaded a model from Hugging Face and noticed a file named model.safetensors?

If you’ve worked with machine learning models, you’ve probably encountered different file extensions, such as .pt, .pth, .bin, and .safetensors.

At first glance, they might seem like different ways to store the same thing: model weights.

But have you ever wondered what’s actually inside those files?

When we train a neural network, we produce tensors containing numerical values learned during training. Those values need to be stored somewhere so we can load the model later without training it again.

For large language models containing billions of parameters, the way those tensors are stored and loaded becomes increasingly important. But there’s another question that might be even more interesting.

Can a model file actually be dangerous?

This is where Safetensors comes in.

The Problem with Pickle

Before understanding Safetensors, we need to understand serialization.

Serialization is the process of converting data or objects into a format that can be stored and reconstructed later.

In PyTorch, we commonly save model weights using torch.save().

import torch

weights = {
"weight": torch.tensor([
[1.0, 2.0],
[3.0, 4.0]
])
}
torch.save(weights, "model.pth")

PyTorch’s standard checkpoint format uses Python’s Pickle mechanism for parts of the serialized representation, while tensor storage data is handled separately.

Pickle is powerful because it supports reconstructing complex Python objects.

But that flexibility introduces a security concern.

Why can Pickle be dangerous?

Pickle can invoke Python callables during object reconstruction.

This means that loading a maliciously crafted Pickle file with unrestricted deserialization could execute arbitrary code. Imagine downloading a checkpoint from an unknown source.

You might think you’re simply loading numerical weights, but the deserialization process could perform operations you never intended.

The important distinction is that the danger comes from the deserialization mechanism, not from the numerical tensor values themselves. Does this mean every Pickle file is dangerous?

Of course not.

It means we should be careful when loading files from untrusted sources. Modern PyTorch also provides a restricted loading mechanism. Since PyTorch 2.6, torch.load() defaults to weights_only=True when no custom pickle_module is supplied. This reduces the risk associated with arbitrary object reconstruction, although it isn't a complete security guarantee.

So, what happens if we compare Pickle and Safetensors directly?

Hands-on: Same Tensor, Different Serialization Rules

Let’s run a small experiment.

First, we’ll save and load exactly the same tensor using both formats. Then, we’ll test how each format handles an object that specifies a Python callable during reconstruction.

Install the dependencies:

pip install torch safetensors

Experiment 1: Normal serialization

import pickle
import torch
from safetensors.torch import save_file, load_file

weights = {
"weight": torch.tensor([1.0, 2.0, 3.0])
}
# Save with Pickle
with open("model.pkl", "wb") as f:
pickle.dump(weights, f)

# Save with Safetensors
save_file(weights, "model.safetensors")

# Load both files
with open("model.pkl", "rb") as f:
pickle_weights = pickle.load(f)
safe_weights = load_file("model.safetensors")
print("Pickle:", pickle_weights["weight"])
print("Safetensors:", safe_weights["weight"])
assert torch.equal(
pickle_weights["weight"],
safe_weights["weight"]
)

Expected output:

Pickle: tensor([1., 2., 3.])
Safetensors: tensor([1., 2., 3.])

Both formats successfully store and reconstruct the same tensor values.

So far, nothing unusual.

The interesting difference appears when we examine what else each format can represent.

Experiment 2: Testing serialization behavior

Let’s create a simple Python class using the __reduce__() method.

This method allows a Pickle-compatible object to specify how it should be reconstructed.

For this demonstration, we’ll use a harmless print() function.

class SuspiciousObject:
def __reduce__(self):
return (
print,
("[WARNING] Code executed during loading!",)
)


suspicious_data = {
"weight": weights["weight"],
"extra": SuspiciousObject()
}
# Pickle accepts the object
with open("suspicious.pkl", "wb") as f:
pickle.dump(suspicious_data, f)
print("Loading Pickle...")
with open("suspicious.pkl", "rb") as f:
pickle.load(f)
# Safetensors only accepts tensor values
print("\nTrying Safetensors...")
try:
save_file(
suspicious_data,
"suspicious.safetensors"
)
except (ValueError, TypeError) as e:
print("Safetensors rejected the object.")
print("Error:", type(e).__name__)

Expected output:

Loading Pickle...
[WARNING] Code executed during loading!

Trying Safetensors...
Safetensors rejected the object.
Error: ValueError

The important observation is that Pickle accepts our Python object, and its reconstruction causes the print() function to execute during loading.

Safetensors, on the other hand, rejects the object because the format is designed to store tensors, not arbitrary Python objects.

This isn’t a demonstration of two identical malicious files. The second experiment deliberately tests the difference in what each serialization format is capable of representing.

And that restriction is exactly what makes Safetensors interesting from a security perspective.

Instead of trying to safely reconstruct every possible Python object, Safetensors avoids that functionality entirely.

Note: The Pickle example uses unrestricted pickle.load() directly. It does not imply that PyTorch's restricted torch.load(weights_only=True) would execute the same payload. Never use unrestricted Pickle loading on untrusted files.

What’s Inside a Safetensors File?

Now that we’ve seen the difference in behavior, let’s look at how Safetensors actually stores data.

A Safetensors file has a relatively simple binary structure:

Press enter or click to view image in full size
https://huggingface.co/docs/safetensors/en/index
+---------------------------+
| 8-byte Header Length |
+---------------------------+
| JSON Header |
+---------------------------+
| Raw Tensor Data |
+---------------------------+

There are three main components.

The first 8 bytes: Header length

The first eight bytes contain an unsigned 64-bit integer stored in little-endian format.

This integer tells us the length of the JSON header in bytes.

For example, if the header is 120 bytes long, the raw tensor data begins at:

8 + 120 = 128 bytes

This is the absolute byte offset where the raw tensor data section begins.

The JSON header

The JSON header describes the stored tensors.

For example, a tensor entry might look like this:

{
"weight": {
"dtype": "F32",
"shape": [2, 2],
"data_offsets": [0, 16]
}
}

Let’s break it down.

  • dtype: F32 means each element uses 32 bits, or 4 bytes.
  • shape: [2, 2] means the tensor has two rows and two columns.
  • data_offsets: [0, 16] means its bytes occupy the interval from offset 0 to offset 16 within the raw tensor data buffer.

We can calculate the required storage:

2 × 2 × 4 bytes = 16 bytes

Notice that data_offsets are relative to the beginning of the tensor data buffer, not the beginning of the file.

Get Diego Prayudha’s stories in your inbox

Join Medium for free to get updates from this writer.

The ending offset is exclusive.

The raw tensor data

After the header comes the binary representation of the tensor values.

Unlike Pickle, this section does not contain instructions for reconstructing arbitrary Python objects.

It contains numerical data that the loader interprets according to the metadata.

Hands-on: Inspecting the header

Let’s inspect our actual Safetensors file.

import json
import struct

with open("model.safetensors", "rb") as f:
header_size = struct.unpack(
"<Q", f.read(8)
)[0]
header = json.loads(
f.read(header_size)
)
print("Header size:", header_size)
print(json.dumps(header, indent=2))

The expression "<Q" tells Python to interpret the eight bytes as a little-endian unsigned 64-bit integer.

For the three-element tensor from our first experiment, the header should describe an F32 tensor with shape [3] and data offsets [0, 12].

The exact header length may vary because the JSON representation can contain padding.

With just a few lines of Python, we can inspect the metadata of a real Safetensors file.

No model architecture is required, and no Pickle object reconstruction takes place.

Is Safetensors Really Safe?

Let’s return to the main question.

Is Safetensors really safe?

The answer depends on what we mean by safe. Safetensors is designed to avoid the arbitrary Python object reconstruction behavior associated with unrestricted Pickle deserialization. That removes an important class of security risks.

The format also defines constraints on its metadata and data layout, allowing implementations to reject malformed structures. But this doesn’t mean that everything involving Safetensors is automatically secure.

For example, Safetensors does not protect against:

  • Vulnerabilities in loading libraries : Even a carefully designed file format depends on software implementations that may contain bugs.
  • Excessive resource consumption :Loading large tensors can still require significant memory and computing resources.
  • Untrusted model code : A model repository may contain custom Python code or other executable components. Storing its weights in Safetensors doesn’t make that code safe.
  • Malicious or unreliable model behavior : A model’s weights may still produce unwanted or unsafe outputs. Serialization security and model behavior are different problems.

So the right conclusion isn’t that Safetensors makes every AI model safe.

It’s that Safetensors provides a safer way to serialize tensor data by deliberately limiting what a file can represent.

What About Performance?

Security isn’t the only advantage of Safetensors. Its binary structure also supports efficient tensor access. One important concept here is memory mapping, or mmap.

Memory mapping allows file contents to be mapped into a process’s virtual address space. The operating system can then bring file-backed pages into physical memory as needed.

Because Safetensors stores tensor data at known byte offsets, compatible implementations can locate and access particular tensors without first reconstructing a complex Python object graph.

This enables efficient loading strategies in suitable scenarios.

Selective tensor loading

Let’s see a simple example using safe_open().

from safetensors import safe_open

with safe_open(
"model.safetensors",
framework="pt",
device="cpu"
) as f:
print("Available tensors:", list(f.keys()))
weight = f.get_tensor("weight")
print("Shape:", weight.shape)
print("Values:", weight)

Expected output:

Available tensors: ['weight']
Shape: torch.Size([3])
Values: tensor([1., 2., 3.])

This API lets us request a tensor by name instead of using a high-level operation that returns the entire checkpoint.

For larger tensors, Safetensors also offers get_slice() for accessing selected portions of tensor data. But does that mean Safetensors is always faster than Pickle-based checkpoints?

Not necessarily.

Loading performance depends on storage hardware, operating-system caching, memory allocation, the size of the checkpoint, and the loading implementation.

Memory mapping doesn’t eliminate disk I/O or the memory needed for computation. Without running a controlled benchmark, it would be misleading to claim that Safetensors is always faster.

What we can say is that its structure enables efficient loading strategies that are particularly useful for large model weights.

Why Do So Many AI Models Use Safetensors?

Modern AI models often contain millions or billions of parameters. Distributing those parameters requires a format that is practical, predictable, and suitable for loading large collections of tensors.

Safetensors offers several advantages:

  • A smaller deserialization attack surface : It avoids unrestricted Pickle-style object reconstruction.
  • A simple binary structure : Tensor metadata can be inspected independently of the numerical values.
  • Efficient tensor access : Known offsets make it possible to locate individual tensors and support selective loading.
  • A natural fit for model distribution : When we only need to distribute model weights, storing arbitrary Python objects may be unnecessary.

However, Safetensors isn’t a replacement for every type of checkpoint.

Training workflows may need to preserve optimizer states, scheduler configurations, random-number-generator states, and other information. Some of this can be represented as tensors, while other state requires separate serialization.

Safetensors is especially useful when the main goal is to store and distribute tensor data.

Final Thoughts

When I first encountered .safetensors files, I thought they were simply another way to save model weights. But after looking into the format, I realized that the most interesting part isn’t the file extension.

It’s the design decision behind it.

Pickle provides flexibility by supporting complex Python object reconstruction. Safetensors takes a different approach.

Instead of supporting arbitrary objects, it focuses on tensor metadata and numerical data. That restriction reduces the risks associated with unrestricted object deserialization while supporting efficient tensor access.

So, is Safetensors really safe?

It’s safer by design for tensor serialization, but it isn’t a guarantee that a model or its surrounding software is completely secure.

And perhaps that’s the most interesting lesson from this exploration.

Sometimes, improving security isn’t about adding more features. It’s about carefully deciding what a system should never be allowed to do.

References & Further Reading

  1. Hugging Face — Safetensors Documentation
    https://huggingface.co/docs/safetensors/index
  2. Safetensors — Official GitHub Repository and Format Specification
    https://github.com/huggingface/safetensors
  3. Hugging Face — Safetensors Metadata Parsing
    https://huggingface.co/docs/safetensors/metadata_parsing
  4. PyTorch — Serialization Semantics
    https://docs.pytorch.org/docs/stable/notes/serialization.html
  5. PyTorch — torch.load API Reference
    https://docs.pytorch.org/docs/stable/generated/torch.load.html
  6. Python — pickle Documentation
    https://docs.python.org/3/library/pickle.html

--

--