Skillversitas

Reading and Writing Files

NumPy has its own binary file format (.npy) and a compressed archive format (.npz) for arrays - fast, lossless, and dtype-preserving - plus text readers and writers (save/load for the binary side, savetxt/loadtxt for the text side) for the cases where the data must be human-readable or shared with non-Python tools. Choosing the right format is a one-line decision: binary for your own pipeline, text only when something outside NumPy must read the file.

The .npy Binary Format

import numpy as np

a = np.arange(12).reshape(3, 4)
b = np.array([1.5, 2.5, 3.5])

np.save("matrix.npy", a)
np.save("vectors.npy", b)

a2 = np.load("matrix.npy")
b2 = np.load("vectors.npy")
print(np.array_equal(a, a2))    # True - shape and dtype round-trip exactly
print(a2.dtype, a2.shape)       # int64 (3, 4)

save/load preserve dtype, shape, and layout exactly - a float32 stays float32, a structured array stays structured. The file carries a small header (dtype, shape, Fortran-order flag) followed by the raw buffer, which is why the round trip is both lossless and fast. A path ending in .npy is appended automatically when omitted. For a quick in-process cache of an expensive intermediate, save/load is the entire tool you need.

Bundling Many Arrays: .npz

import numpy as np

model = np.random.default_rng(0).normal(size=(5, 5))
bias = np.random.default_rng(1).normal(size=5)
meta = np.array([42, 7])

# a zip archive: one file, many named arrays
np.savez("bundle.npz", weights=model, b=bias, seed=meta)

z = np.load("bundle.npz")
print(list(z.keys()))                 # ['weights', 'b', 'seed']
W = z["weights"]                       # lazy: read only what you need
print(np.array_equal(W, model))        # True

# unpack everything into a dict at once
parts = dict(np.load("bundle.npz"))

savez writes a zip archive of named arrays - the standard way to ship a checkpoint (weights plus biases plus metadata) as one file. Loading is lazy: z["weights"] reads only that member, so a bundle holding several GB can be opened cheaply and paged through by name. allow_pickle=True extends the format to arbitrary Python objects, at the cost of pickling's security caveats - use it deliberately, not by reflex.

Text Formats: savetxt and loadtxt

import numpy as np

m = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])

# CSV-ish: comma delimiter, no header
np.savetxt("rows.csv", m, delimiter=",")

# with a header line describing the columns
np.savetxt("labeled.txt", m,
           header="x,y,z", comments="# ")

back = np.loadtxt("rows.csv", delimiter=",")
print(np.array_equal(m, back))       # True

# skipping the header when loading
back2 = np.loadtxt("labeled.txt", comments="# ")
print(np.array_equal(m, back2))      # True

save/load is the human-readable path: space- or comma-delimited numbers that any tool can open. The header and comments parameters control the description lines, and the matching comments argument on load tells the reader which lines to skip. The trade is explicit - text is slower and stores every value with a fixed decimal format (default %.18e, so precision is preserved but files balloon) - so reach for text only when the consumer is not NumPy.

Messy Text: genfromtxt

import numpy as np

# a file with a junk header and missing values (NaN placeholders)
from io import StringIO
text = """colA colB colC
1.0 2.0 3.0
4.0 NaN 6.0
7.0 8.0 9.0
"""
data = np.genfromtxt(StringIO(text),
                     skip_header=1,
                     names=True)
print(data.dtype.names)              # ('colA', 'colB', 'colC')
print(data["colB"])                   # [2. NaN 8.] - missing became NaN

# the replacement: np.loadtxt is faster for clean, fixed-width tables;
# for ragged or annotated data, pandas.read_csv is the better tool
# genfromtxt remains for the one-off legacy dumps in the wild

genfromtxt is the tolerant reader: it handles named columns, skipped lines, and missing values (which become NaN) in a structured-array result. It is slower than loadtxt because it is more general - use it for legacy or annotated dumps, and for new tabular work prefer loadtxt (clean numbers) or pandas (real tables with labels). The decision rule: if the file has a schema a human wrote, you probably want pandas; if it is a clean numeric grid, loadtxt.

Choosing a Format

FormatUse when
.npysingle array, your own pipeline, speed and fidelity matter
.npzseveral named arrays as one artifact (a checkpoint)
CSV/texta non-Python tool must read it, or humans will inspect it

Summary

Persist arrays with np.save/np.load for lossless, dtype-preserving single arrays, and np.savez/np.load to bundle several named arrays into one lazy-loading archive - the checkpoint pattern. Use save/load only when the consumer is outside NumPy or human, accepting the slower, fixed-format text for that portability. Reach for genfromtxt on legacy or annotated dumps, and prefer pandas for real tabular data. The default for anything internal: binary, always.