PyTorch Model Formats: .pt, .pth & safetensors
The PyTorch model formats you can save to look more confusing than they are: .pt, .pth, .ckpt, .safetensors, .pt2, .gguf. Some of those are the same thing wearing a different name. One of them can run malicious code on your machine the moment you load it. This guide explains what each format actually stores, why the ecosystem moved away from raw pickle files, and which format to reach for depending on whether you are training, sharing, or deploying. For broader context, our PyTorch hub covers the fundamentals, from what PyTorch is to how it compares in PyTorch vs TensorFlow.
What "Saving a Model" Actually Means
A trained neural network is, underneath, a large collection of numbers: the learned weights and biases of every layer. Saving a model means writing those numbers to disk in a way you can read back later. The formats differ in what else they store alongside the numbers and how they store them.
Put governance around how your team uses AI. The AI Acceptable Use Policy: a deploy-ready template that sets the rules for AI use.
Your purchase helps keep our hubs free to read.
PyTorch's native mechanism is torch.save(), which uses Python's pickle utility to serialize objects (to serialize is to convert an object to bytes for storage). The documentation is explicit that "models, tensors, and dictionaries of all kinds of objects can be saved using this function." That flexibility is the source of both its convenience and its biggest risk. Since PyTorch 1.6, the file torch.save() produces is an uncompressed ZIP64 archive containing the pickled data, a byte-order marker, the raw storage tensors (tensors are the multi-dimensional arrays of numbers that make up a model), and a version stamp.
Three broad categories cover almost everything you will encounter:
- Native PyTorch pickle files (
.pt,.pth,.ckpt) hold a state_dict or a full checkpoint. Maximum flexibility, but they run pickle on load. - safetensors (
.safetensors) holds only tensors plus a metadata header. No executable code, fast to load. - Deployment graphs (TorchScript
.pt, torch.export.pt2,.gguf) capture the model so it can run outside your Python training script.
Mental model: a state_dict is the numbers. A checkpoint is the numbers plus training bookkeeping. A deployment graph is the numbers plus the computation, frozen for inference (inference is actually running the trained model to get outputs, as opposed to training it).
.pt vs .pth: The Same File, Different Letter
This is the question that sends newcomers in circles, so here is the short answer: there is no technical difference. The PyTorch documentation states that "a common PyTorch convention is to save models using either a .pt or .pth file extension." Both are produced by the same torch.save() call, both contain the same ZIP64 pickle archive, and torch.load() reads them identically. You could name the file model.banana and it would still load.
The only practical wrinkle is unrelated to PyTorch. Python's own packaging tooling historically treats .pth files in site-packages as path-configuration files. That collision is why some teams prefer .pt for model weights, purely to avoid confusion. It has no effect on how the model loads.
You will also see .ckpt in the wild, especially from PyTorch Lightning and diffusion-model communities. It is still a pickle archive under the hood, just a naming convention signalling "this is a checkpoint with training state," not only weights.
Practical rule: pick one extension and be consistent. .pt for inference weights, .ckpt for resumable training checkpoints. The bytes inside are the same kind of file.
state_dict vs Saving the Whole Model
Inside a pickle file you can store one of two things, and the choice matters more than the extension ever will. A state_dict is a plain Python dictionary mapping each layer to its tensor of learned parameters. Saving the whole model pickles the entire Python object, including the class definition's reference.
PyTorch's guidance is unambiguous: "saving the model's state_dict with the torch.save() function will give you the most flexibility for restoring the model later, which is why it is the recommended method for saving models." Saving the entire model has a real drawback. The documentation warns that "the serialized data is bound to the specific classes and the exact directory structure used when the model is saved," so moving or refactoring your code can break the load.
The standard pattern saves only the parameters:
torch.save(model.state_dict(), "model.pth")
To load it, you re-create the model object from its class, then pour the saved numbers back in. Note that load_state_dict() takes a dictionary, not a path, so you deserialize with torch.load() first. Always call model.eval() before inference to put dropout and batch-norm layers into evaluation mode, or your results will be inconsistent.
model = TheModelClass(*args, **kwargs)
model.load_state_dict(torch.load("model.pth", weights_only=True))
model.eval()
Why this wins: a state_dict is just data. It survives code refactors, transfers cleanly between projects, and is exactly what safetensors stores too. The whole-model shortcut couples your saved file to one snapshot of your source tree.
Checkpoints: Saving More Than Weights
If a multi-day training run crashes, weights alone are not enough to resume cleanly. You also need the optimizer's state, the epoch counter, and the current loss. A checkpoint is simply a dictionary that bundles all of that and hands it to torch.save(). PyTorch recommends including optimizer state because it "contains buffers and parameters that are updated as the model trains."
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss,
}, "checkpoint.pth")
Resuming reverses the process. You load the dictionary, then restore each component into the object it belongs to before continuing training.
# weights_only=True with full checkpoints requires PyTorch 2.6+
# For PyTorch < 2.6, use weights_only=False for checkpoints you own and trust
checkpoint = torch.load("checkpoint.pth", weights_only=True)
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
This is the one place where safetensors is not a drop-in replacement. safetensors stores tensors only, so optimizer state and Python scalars like the epoch number do not fit its model. For resumable training, a pickle checkpoint is still the practical choice. For sharing finished weights, safetensors is the better target, which is the next section.
The Pickle Security Problem
Here is the part that turns a file-format question into a security question. Because torch.save() uses pickle, the load step does not just read numbers. The full pickle path can execute arbitrary Python code while it reconstructs objects. A model file downloaded from a stranger can, in principle, run commands on your machine the instant you call torch.load() on it. This is not a hypothetical with pickle in general; it is the documented behavior of the format.
PyTorch's answer is the weights_only argument. With weights_only=True, unpickling is restricted to "only executing functions/building classes required for state_dicts of plain torch.Tensors as well as some other primitive types," and the unpickler "is not allowed to dynamically import anything during unpickling." Starting in PyTorch 2.6, this is the default when you do not pass a custom pickle module.
weights_only=False path, a malicious file can execute arbitrary code. Treat downloaded .pt/.pth/.ckpt files like you would treat any executable: only run them from sources you trust.weights_only=True "narrows the surface of remote code execution attacks" but does not guard against denial-of-service attacks, and memory corruption may still be possible. It is a strong default, not a guarantee of safety on hostile input.torch.load() defaulted to weights_only=False. If you run an older install, pass weights_only=True explicitly when the file contains only a state_dict. Verify your version before assuming the safe default applies.Bottom line: the cleanest way to sidestep the pickle problem entirely is to share weights in a format that cannot execute code in the first place. That format is safetensors.
safetensors: The Safer, Faster Default
safetensors is a format from Hugging Face designed, in its own words, "for storing tensors safely (as opposed to pickle) and that is still fast (zero-copy)." It has become the default weight format across the Hugging Face ecosystem, including Transformers, Diffusers, and tools like ComfyUI and text-generation-webui.
The design is deliberately boring, and that is the point. A safetensors file stores a JSON header describing each tensor's name, data type, shape, and byte offsets, followed by the raw tensor bytes. There is no executable code anywhere in the format, so loading one cannot run arbitrary commands. It stores tensors only: no Python objects, no class definitions, no optimizer state.
Safety is the headline, but speed is the reason it stuck. Because the loader memory-maps the file and skips unnecessary copies, it is far faster on a cold load.
Saving and loading from PyTorch takes two functions. You hand save_file a state_dict and get back a .safetensors file; load_file returns the state_dict to feed into load_state_dict().
from safetensors.torch import save_file, load_file
save_file(model.state_dict(), "model.safetensors")
state_dict = load_file("model.safetensors")
Already have pickle weights you want to convert? Hugging Face recommends its Convert Space, which downloads the pickled weights, converts them, and opens a pull request with the new .safetensors file. The same conversion script can be run locally for larger models.
Deployment Formats: TorchScript, torch.export, GGUF
A state_dict needs your Python class to mean anything. That is fine for training, but a production server, a mobile app, or a C++ runtime may not have your code or even a Python interpreter. Deployment formats solve this by capturing the computation graph alongside the weights, producing a self-contained artifact.
TorchScript (.pt)
TorchScript is, per the docs, a way to create serializable models from PyTorch code that can run in a high-performance environment such as C++ with no Python dependency. You convert a model with torch.jit.script() (or torch.jit.trace()) and persist it with torch.jit.save(). The result is self-contained, usually with a .pt extension.
scripted = torch.jit.script(model)
scripted.save("model.pt")
loaded = torch.jit.load("model.pt")
torch.export (.pt2)
torch.export is the newer path. It captures the model as an ExportedProgram, a full graph representation, saved and loaded with torch.export.save() and torch.export.load() using the .pt2 extension. It is the direction PyTorch is steering deployment toward, ahead of legacy TorchScript.
ep = torch.export.export(model, (example_input,))
torch.export.save(ep, "model.pt2")
loaded = torch.export.load("model.pt2")
GGUF (.gguf)
GGUF sits slightly outside the PyTorch family but you will meet it constantly when running local LLMs. It is a binary format built for quick loading and saving of models, developed by the author of llama.cpp. Unlike tensor-only safetensors, GGUF encodes both the tensors and a standardized set of metadata in one file, and it supports a wide range of quantization types (quantization means reducing the numeric precision of the weights to shrink the model) for running large models on modest hardware. Models built in PyTorch are commonly converted to GGUF for use with engines like llama.cpp, LM Studio, GPT4All, and Ollama. It is an inference format, not a way to resume PyTorch training.
Which Format Should You Use?
The right format depends on the job. Saving for yourself mid-training, sharing finished weights publicly, and shipping to production are three different problems with three different answers.
| Format | Stores | Runs code on load? | Load speed | Best for |
|---|---|---|---|---|
| .pt / .pth (state_dict) | Layer weights only | Yes (pickle; use weights_only=True) | Slower (full deserialization) | Your own inference weights |
| .pt / .ckpt (checkpoint) | Weights + optimizer + epoch/loss | Yes (pickle; use weights_only=True) | Slower (full deserialization) | Resuming your own training |
| .safetensors | Tensors + JSON metadata header | No | Fastest (zero-copy / mmap) | Sharing and distributing weights |
| TorchScript .pt | Weights + graph (legacy) | Loads a graph, no arbitrary code | Moderate | C++ / no-Python serving |
| torch.export .pt2 | Weights + ExportedProgram graph | Loads a graph, no arbitrary code | Moderate | Modern PyTorch deployment |
| .gguf | Tensors + metadata (quantized) | No | Fast (mmap) | Local LLM inference (llama.cpp) |
Note: a .pt file may be either a pickled state_dict or a TorchScript graph. They share the extension but load differently (torch.load vs torch.jit.load).
model.state_dict() for finished weights and full checkpoints during long runs. Load with weights_only=True by habit.weights_only=True and confirm the source. Treat strangers' pickle files as untrusted executables.Frequently Asked Questions
Video Resources
Go Deeper
Resources from across Tech Jacks Solutions