Zarr Python — Chunked N-D Arrays
Overview
Zarr is a Python library for storing large N-dimensional arrays with chunking, compression, and parallel I/O. It provides NumPy-compatible indexing with pluggable storage backends (local, cloud, in-memory), making it the standard format for cloud-native scientific data pipelines.
When to Use
- Storing arrays too large for memory with chunked access (out-of-core computing)
- Cloud-native data workflows with S3 or GCS storage backends
- Parallel read/write with Dask for large-scale computation
- Hierarchical data organization (groups of named arrays with metadata)
- Converting between formats (HDF5 → Zarr, NetCDF → Zarr)
- Appending time-series data incrementally without rewriting
- For labeled, coordinate-aware arrays (time, lat, lon), use xarray with Zarr backend instead
- For data management, lineage, and ontology validation, use lamindb (which uses Zarr as a storage format)
Prerequisites
pip install zarr
# Cloud storage support
pip install s3fs # Amazon S3
pip install gcsfs # Google Cloud Storage
Requires Python 3.11+.
Quick Start
import zarr
import numpy as np
# Create a chunked, compressed 2D array
z = zarr.create_array(
store="data/my_array.zarr",
shape=(10000, 10000),
chunks=(1000, 1000),
dtype="f4"
)
# Write with NumPy-style indexing
z[:, :] = np.random.random((10000, 10000)).astype("f4")
# Read a slice (only reads needed chunks)
subset = z[0:100, 0:100]
print(f"Shape: {subset.shape}, dtype: {subset.dtype}")
# Shape: (100, 100), dtype: float32
Core API
1. Array Creation
import zarr
import numpy as np
# Empty arrays
z = zarr.zeros(shape=(10000, 10000), chunks=(1000, 1000), dtype="f4", store="data.zarr")
z = zarr.ones((5000, 5000), chunks=(500, 500), dtype="f4")
z = zarr.full((1000, 1000), fill_value=42, chunks=(100, 100), dtype="i4")
# From existing NumPy data
data = np.arange(10000, dtype="f4").reshape(100, 100)
z = zarr.array(data, chunks=(10, 10), store="from_numpy.zarr")
print(f"Created: shape={z.shape}, chunks={z.chunks}, dtype={z.dtype}")
# Create like another array (matches shape, chunks, dtype)
z2 = zarr.zeros_like(z)
# Open existing array
z = zarr.open_array("data.zarr", mode="r+") # Read-write
z = zarr.open_array("data.zarr", mode="r") # Read-only
z = zarr.open("data.zarr") # Auto-detect array vs group
2. Reading, Writing, and Indexing
import zarr
import numpy as np
z = zarr.zeros((10000, 10000), chunks=(1000, 1000), dtype="f4")
# Write slices
z[0, :] = np.arange(10000, dtype="f4")
z[10:20, 50:60] = np.random.random((10, 10)).astype("f4")
z[:] = 42 # Fill entire array
# Read slices (returns NumPy array)
row = z[5, :]
block = z[0:100, 0:100]
print(f"Row shape: {row.shape}, block shape: {block.shape}")
# Advanced indexing
z.vindex[[0, 5, 10], [2, 8, 15]] # Coordinate (fancy) indexing
z.oindex[0:10, [5, 10, 15]] # Orthogonal indexing
z.blocks[0, 0] # Block/chunk indexing
# Resize and append
z.resize(15000, 15000)
z.append(np.random.random((1000, 10000)).astype("f4"), axis=0)
3. Chunking and Sharding
Chunk shape is the most important performance parameter.
import zarr
from zarr.codecs import ShardingCodec
# Chunk aligned with access pattern
# Row-wise access → chunk spans columns
z_row = zarr.zeros((10000, 10000), chunks=(10, 10000), dtype="f4")
# Column-wise access → chunk spans rows
z_col = zarr.zeros((10000, 10000), chunks=(10000, 10), dtype="f4")
# Mixed access → balanced square chunks (~1MB each for float32)
z_bal = zarr.zeros((10000, 10000), chunks=(512, 512), dtype="f4")
# 512*512*4 bytes = ~1MB per chunk
# Sharding: group small chunks into larger storage objects
# Useful when millions of small chunks cause filesystem overhead
z_sharded = zarr.create_array(
store="sharded.zarr",
shape=(100000, 100000),
chunks=(100, 100), # Small chunks for fine-grained access
shards=(1000, 1000), # Groups 100 chunks per shard
dtype="f4"
)
print(f"Chunks: {z_sharded.chunks}, shards reduce file count")
Chunk size guidelines:
- Target 1–10 MB per chunk (minimum 1 MB)
- Align chunk shape with your most common access pattern
- Entire chunks load into memory during read → don't exceed available RAM
- Entire shards load into memory during write
4. Compression
from zarr.codecs.blosc import BloscCodec
from zarr.codecs import GzipCodec, ZstdCodec, BytesCodec
import zarr
# Default: Blosc with Zstandard (good balance)
z = zarr.zeros((1000, 1000), chunks=(100, 100), dtype="f4")
# Explicit Blosc configuration
z = zarr.create_array(
store="compressed.zarr",
shape=(1000, 1000), chunks=(100, 100), dtype="f4",
codecs=[BloscCodec(cname="zstd", clevel=5, shuffle="shuffle")]
)
# Speed-optimized (LZ4)
z_fast = zarr.create_array(
store="fast.zarr",
shape=(1000, 1000), chunks=(100, 100), dtype="f4",
codecs=[BloscCodec(cname="lz4", clevel=1)]
)
# Maximum compression (Gzip level 9)
z_small = zarr.create_array(
store="small.zarr",
shape=(1000, 1000), chunks=(100, 100), dtype="f4",
codecs=[GzipCodec(level=9)]
)
# No compression
z_raw = zarr.create_array(
store="raw.zarr",
shape=(1000, 1000), chunks=(100, 100), dtype="f4",
codecs=[BytesCodec()]
)
Codec selection: Blosc/Zstd (default, balanced) → LZ4 (fastest) → Gzip (smallest). Enable shuffle="shuffle" for numeric data — it reorders bytes for better compression ratios.
5. Storage Backends
import zarr
import numpy as np
from zarr.storage import LocalStore, MemoryStore, ZipStore
# Local filesystem (default — string paths create LocalStore automatically)
z = zarr.open_array("data/array.zarr", mode="w", shape=(1000, 1000),
chunks=(100, 100), dtype="f4")
# In-memory (not persisted)
store = MemoryStore()
z_mem = zarr.open_array(store=store, mode="w", shape=(1000, 1000),
chunks=(100, 100), dtype="f4")
# ZIP file storage
store = ZipStore("data.zip", mode="w")
z_zip = zarr.open_array(store=store, mode="w", shape=(1000, 1000),
chunks=(100, 100), dtype="f4")
z_zip[:] = np.random.random((1000, 1000)).astype("f4")
store.close() # IMPORTANT: must close ZipStore
# Cloud storage: Amazon S3
import s3fs
s3 = s3fs.S3FileSystem(anon=False)
store = s3fs.S3Map(root="my-bucket/path/array.zarr", s3=s3)
z = zarr.open_array(store=store, mode="w", shape=(1000, 1000),
chunks=(500, 500), dtype="f4")
z[:] = np.random.random((1000, 1000)).astype("f4")
# Consolidate metadata for faster subsequent reads
zarr.consolidate_metadata(store)
# Google Cloud Storage
import gcsfs
gcs = gcsfs.GCSFileSystem(project="my-project")
store = gcsfs.GCSMap(root="my-bucket/path/array.zarr", gcs=gcs)
Cloud best practices: consolidate metadata, use 5–100 MB chunks, enable sharding to reduce object count, use Dask for parallel I/O.
6. Groups and Hierarchies
import zarr
# Create hierarchical structure (like HDF5 groups)
root = zarr.group(store="hierarchy.zarr")
# Create sub-groups
temperature = root.create_group("temperature")
precipitation = root.create_group("precipitation")
# Create arrays within groups
temp_arr = temperature.create_array(
name="t2m",
shape=(365, 720, 1440),
chunks=(1, 720, 1440),
dtype="f4"
)
precip_arr = precipitation.create_array(
name="prcp",
shape=(365, 720, 1440),
chunks=(1, 720, 1440),
dtype="f4"
)
# Access by path
arr = root["temperature/t2m"]
print(root.tree())
# /
# ├── temperature
# │ └── t2m (365, 720, 1440) f4
# └── precipitation
# └── prcp (365, 720, 1440) f4
7. Attributes and Metadata
import zarr
z = zarr.zeros((1000, 1000), chunks=(100, 100), dtype="f4")
# Attach metadata (must be JSON-serializable)
z.attrs["description"] = "Temperature data in Kelvin"
z.a