File I/O: save, load, npz and memory-mapped arrays

Persist arrays without losing dtype or shape, read text files robustly, and work with arrays that do not fit in memory.

npy and npz

NumPy's own formats store dtype, shape and order alongside the bytes, so a round trip is exact. CSV cannot do that: it loses dtype and floats come back as an approximation.

a = np.arange(6).reshape(2, 3)

np.save("a.npy", a)              # single array
np.load("a.npy")

np.savez("bundle.npz", x=a, y=a * 2)            # several arrays, uncompressed
np.savez_compressed("bundle.npz", x=a, y=a * 2) # smaller, slower to write
data = np.load("bundle.npz")
sorted(data.keys())              # ['x', 'y']
data["x"]

# the easy route to a huge file: do not read it all
mm = np.load("a.npy", mmap_mode="r")
mm[0]
FormatKeeps dtypeSizeUse for
.npyYesRaw bytesOne array, exact round trip
.npzYesRaw bytesSeveral named arrays in one file
.npz compressedYesSmallerArchiving, slow random access
CSV/textNoLargeInterchange with other tools
memmapYesOn diskArrays larger than RAM

Text files

np.savetxt("data.csv", a, delimiter=",", fmt="%.3f",
           header="a,b,c", comments="")

np.loadtxt("data.csv", delimiter=",", skiprows=1)   # numeric, fast, all-or-nothing

# tolerant version: handles missing values, headers and mixed types
t = np.genfromtxt("data.csv", delimiter=",", names=True,
                  dtype=None, encoding="utf-8")
t["a"]

d = np.loadtxt("data.csv", delimiter=",", skiprows=1,
               usecols=(0, 2), max_rows=100)
  • loadtxt fails on a single bad token; genfromtxt fills it with nan instead.
  • Use usecols and max_rows to avoid reading columns you do not need.
  • Text I/O is a convenience, not a storage strategy: it is slower and roughly twice the size of binary.

Arrays bigger than memory

# create a file-backed array, then write through it in blocks
mm = np.memmap("big.dat", dtype=np.float32, mode="w+", shape=(10000, 10000))
for start in range(0, 10000, 500):
    mm[start:start + 500] = 0.0
mm.flush()

# later, and on a machine without enough RAM to hold it all
mm = np.memmap("big.dat", dtype=np.float32, mode="r", shape=(10000, 10000))
mm[42, :10]          # only the pages touched are read from disk
mm[:, 3].mean()      # a column still walks the whole file: slow but possible
⚠️
Fancy indexing a memmap materialises the result in RAM. mm[rows] with thousands of indices can allocate more memory than reading the file sequentially would — slice, do not gather.

FAQ

Why did my CSV numbers come back slightly different?
Text parsers read decimals as doubles and CSV cannot record the original dtype. For an exact round trip use .npy or .npz.
Is a memmap faster than loading?
Not necessarily. Random access to disk is slower than reading once into RAM. Memmap wins when the file is larger than memory or when you only touch a small part of it.

Views, copies and memory layout Structured arrays, datetimes and string dtypes

Last refreshed 2026-09-18.