Skill Tracer
Skills › Scientific Agent Skills › zarr-python › integration.md

integration.md

133 lines · skills/zarr-python/references/integration.md at 5f2bc03

Open on GitHub
1# Integration, parallelism, and consolidated metadata
2
3Tested with Zarr 3.4.0, NumPy 2.5.3, Dask 2026.8.0, and Xarray 2026.9.0.
4

Integration, parallelism, and consolidated metadata

Tested with Zarr 3.4.0, NumPy 2.5.3, Dask 2026.8.0, and Xarray 2026.9.0.

Raw lines 1 to 4
5## NumPy and Dask
6
7NumPy conversion is eager. Dask can read and reduce by chunks, but `.compute()` still
8materializes its **result**; a full 100000-by-100000 float64 result needs about 80 GB.
9Write large results directly to a new store. Align Dask task boundaries to whole stored
10chunks/shards and budget all concurrent task/codec buffers.
12```python
13import numpy as np
14import zarr
15import dask.array as da
17expected = np.arange(96, dtype="f4").reshape(12, 8)
18z = zarr.create_array("dask-input.zarr", data=expected, chunks=(4, 4))
19lazy = da.from_zarr("dask-input.zarr")
20np.testing.assert_allclose(lazy.mean(axis=0).compute(), expected.mean(axis=0))
21da.to_zarr(lazy * 2, "dask-output.zarr", mode="w-", zarr_format=3)
22np.testing.assert_array_equal(zarr.open_array("dask-output.zarr", mode="r")[:], expected * 2)
23```
25Dask `to_zarr` defaults to `mode="a"`; choose `"w-"` for new outputs. Known regular chunk
26sizes are required for this path. Rechunking may involve large intermediate transfers;
27benchmark it separately and don't assume a lazy graph is memory-free.

NumPy and Dask

NumPy conversion is eager. Dask can read and reduce by chunks, but .compute() still materializes its result; a full 100000-by-100000 float64 result needs about 80 GB. Write large results directly to a new store. Align Dask task boundaries to whole stored chunks/shards and budget all concurrent task/codec buffers.

import numpy as np
import zarr
import dask.array as da

expected = np.arange(96, dtype="f4").reshape(12, 8)
z = zarr.create_array("dask-input.zarr", data=expected, chunks=(4, 4))
lazy = da.from_zarr("dask-input.zarr")
np.testing.assert_allclose(lazy.mean(axis=0).compute(), expected.mean(axis=0))
da.to_zarr(lazy * 2, "dask-output.zarr", mode="w-", zarr_format=3)
np.testing.assert_array_equal(zarr.open_array("dask-output.zarr", mode="r")[:], expected * 2)

Dask to_zarr defaults to mode="a"; choose "w-" for new outputs. Known regular chunk sizes are required for this path. Rechunking may involve large intermediate transfers; benchmark it separately and don't assume a lazy graph is memory-free.

Raw lines 5 to 28
29## Xarray: labeled data and missingness
31`xr.open_zarr` opens a **group** with dimension metadata, not an arbitrary bare Zarr
32array. Format 3 uses native `dimension_names`; format 2 conventionally uses the
33`_ARRAY_DIMENSIONS` attribute. Prefer Xarray's writer to create a labeled dataset and
34preserve coordinates, units, calendar and missing-value encodings together.
36```python
37import numpy as np
38import xarray as xr
40values = np.arange(24, dtype="f4").reshape(3, 2, 4)
41values[1, 0, 1] = np.nan
42expected = xr.Dataset(
43 {"temperature": (("time", "lat", "lon"), values, {"units": "K"})},
44 coords={
45 "time": np.array(["2026-01-01", "2026-01-02", "2026-01-03"], dtype="datetime64[ns]"),
46 "lat": [30.0, 60.0], "lon": [0.0, 90.0, 180.0, 270.0],
47 },
48 attrs={"source": "synthetic example"},
50expected.to_zarr(
51 "climate.zarr", mode="w-", zarr_format=3, consolidated=False,
52 encoding={"temperature": {"chunks": (1, 2, 4), "_FillValue": np.nan}},
54with xr.open_zarr("climate.zarr", consolidated=False) as actual:
55 xr.testing.assert_identical(actual.compute(), expected)
56 subset = actual.sel(time="2026-01-02", lat=slice(30, 60)).compute()
57 assert subset.temperature.shape == (2, 4)
58```
60Storage `fill_value` and scientific `_FillValue` are different concepts in format 3.
61With default Xarray behavior, a format-3 storage fill of zero is not automatically
62missing; `_FillValue` controls masking. Format 2 normally interprets storage fill as
63missing. Check `use_zarr_fill_value_as_mask`, decoding and dtype when opening; never
64silently convert valid zeros to NaNs. A missing chunk can look like valid fill data,
65so validate written coverage independently. Ascending versus descending coordinate
66order changes which slice bounds select data; calendars and longitude conventions must
67be explicit. Labels alone do not make a CF-compliant climate product.
69For append/region writes, validate coordinates and existing schema first. Xarray's
70`safe_chunks=True` checks a subset of chunk alignment constraints; it does not provide
71locks or a multi-array transaction. Do not disable it merely to silence a warning.

Xarray: labeled data and missingness

xr.open_zarr opens a group with dimension metadata, not an arbitrary bare Zarr array. Format 3 uses native dimension_names; format 2 conventionally uses the _ARRAY_DIMENSIONS attribute. Prefer Xarray's writer to create a labeled dataset and preserve coordinates, units, calendar and missing-value encodings together.

import numpy as np
import xarray as xr

values = np.arange(24, dtype="f4").reshape(3, 2, 4)
values[1, 0, 1] = np.nan
expected = xr.Dataset(
    {"temperature": (("time", "lat", "lon"), values, {"units": "K"})},
    coords={
        "time": np.array(["2026-01-01", "2026-01-02", "2026-01-03"], dtype="datetime64[ns]"),
        "lat": [30.0, 60.0], "lon": [0.0, 90.0, 180.0, 270.0],
    },
    attrs={"source": "synthetic example"},
)
expected.to_zarr(
    "climate.zarr", mode="w-", zarr_format=3, consolidated=False,
    encoding={"temperature": {"chunks": (1, 2, 4), "_FillValue": np.nan}},
)
with xr.open_zarr("climate.zarr", consolidated=False) as actual:
    xr.testing.assert_identical(actual.compute(), expected)
    subset = actual.sel(time="2026-01-02", lat=slice(30, 60)).compute()
    assert subset.temperature.shape == (2, 4)

Storage fill_value and scientific _FillValue are different concepts in format 3. With default Xarray behavior, a format-3 storage fill of zero is not automatically missing; _FillValue controls masking. Format 2 normally interprets storage fill as missing. Check use_zarr_fill_value_as_mask, decoding and dtype when opening; never silently convert valid zeros to NaNs. A missing chunk can look like valid fill data, so validate written coverage independently. Ascending versus descending coordinate order changes which slice bounds select data; calendars and longitude conventions must be explicit. Labels alone do not make a CF-compliant climate product.

For append/region writes, validate coordinates and existing schema first. Xarray's safe_chunks=True checks a subset of chunk alignment constraints; it does not provide locks or a multi-array transaction. Do not disable it merely to silence a warning.

Raw lines 29 to 72
73## Parallel writes and concurrency
75Concurrent reads of a stable store are supported; readers are not promised a snapshot
76during writes. Assign disjoint stored objects to writers: chunks for unsharded arrays,
77**shards** for sharded arrays. Independent inner chunks can share one shard. Serialize
78append/resize/attribute changes and external publication. ZIP is not a concurrent store.
79Zarr-Python 3 has no functioning `ThreadSynchronizer` or `ProcessSynchronizer`; an old
80`synchronizer=` argument may warn and be ignored, not protect writes.
82```python
83from concurrent.futures import ThreadPoolExecutor
84import numpy as np
85import zarr
87z = zarr.create_array("parallel.zarr", shape=(16, 8), dtype="i4", chunks=(2, 4), shards=(8, 8))
88def write_shard(start):
89 target = zarr.open_array("parallel.zarr", mode="r+")
90 target[start:start + 8, :] = start + 1
92with zarr.config.set({"async.concurrency": 4, "threading.max_workers": 4}):
93 with ThreadPoolExecutor(max_workers=2) as pool:
94 list(pool.map(write_shard, [0, 8]))
95read = zarr.open_array("parallel.zarr", mode="r")[:]
96np.testing.assert_array_equal(read[:8], np.full((8, 8), 1))
97np.testing.assert_array_equal(read[8:], np.full((8, 8), 9))
98```
100This is a local two-writer smoke test, not a distributed failure-recovery guarantee.
101Dask worker threads multiply pressure on the store; tune internal concurrency alongside
102worker count, memory and backend rate limits. The 3.4 default async concurrency is 10.

Parallel writes and concurrency

Concurrent reads of a stable store are supported; readers are not promised a snapshot during writes. Assign disjoint stored objects to writers: chunks for unsharded arrays, shards for sharded arrays. Independent inner chunks can share one shard. Serialize append/resize/attribute changes and external publication. ZIP is not a concurrent store. Zarr-Python 3 has no functioning ThreadSynchronizer or ProcessSynchronizer; an old synchronizer= argument may warn and be ignored, not protect writes.

from concurrent.futures import ThreadPoolExecutor
import numpy as np
import zarr

z = zarr.create_array("parallel.zarr", shape=(16, 8), dtype="i4", chunks=(2, 4), shards=(8, 8))
def write_shard(start):
    target = zarr.open_array("parallel.zarr", mode="r+")
    target[start:start + 8, :] = start + 1

with zarr.config.set({"async.concurrency": 4, "threading.max_workers": 4}):
    with ThreadPoolExecutor(max_workers=2) as pool:
        list(pool.map(write_shard, [0, 8]))
read = zarr.open_array("parallel.zarr", mode="r")[:]
np.testing.assert_array_equal(read[:8], np.full((8, 8), 1))
np.testing.assert_array_equal(read[8:], np.full((8, 8), 9))

This is a local two-writer smoke test, not a distributed failure-recovery guarantee. Dask worker threads multiply pressure on the store; tune internal concurrency alongside worker count, memory and backend rate limits. The 3.4 default async concurrency is 10.

Raw lines 73 to 103
104## Consolidation is group metadata caching
106Format 2 writes `.zmetadata`; format 3 embeds consolidated metadata in the root group's
107`zarr.json` and remains experimental. `tree()` shows logical nodes, not those files.
108Do not consolidate a standalone array root or assume all later opens see fresh metadata.
110```python
111import numpy as np
112import zarr
114root = zarr.open_group("consolidated.zarr", mode="w-", zarr_format=2)
115root.create_array("a", data=np.arange(8), chunks=(4,))
116zarr.consolidate_metadata(root.store)
117read = zarr.open_consolidated(root.store, mode="r")
118np.testing.assert_array_equal(read["a"][:], np.arange(8))
119# When freshness matters, explicitly bypass cached hierarchy metadata:
120fresh = zarr.open_group(root.store, mode="r", use_consolidated=False)
121assert fresh["a"].shape == (8,)
122```
124Consolidate only after metadata writers finish. Use `use_consolidated=False` for mutable
125hierarchies and reconsolidate completed revisions. A cached hierarchy may omit newly
126created children or stale array shapes/attributes; actual chunk data is not cached there.
127Pass a configured FsspecStore for cloud consolidation: `consolidate_metadata` has no
128`storage_options` argument. Reopen with the same backend authorization.
130Sources: [Dask writer](https://docs.dask.org/en/stable/generated/dask.array.to_zarr.html),
131[Xarray writer](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.to_zarr.html),
132[Xarray reader](https://docs.xarray.dev/en/stable/generated/xarray.open_zarr.html),
133[consolidated metadata](https://zarr.readthedocs.io/en/stable/user-guide/consolidated_metadata/).

Consolidation is group metadata caching

Format 2 writes .zmetadata; format 3 embeds consolidated metadata in the root group's zarr.json and remains experimental. tree() shows logical nodes, not those files. Do not consolidate a standalone array root or assume all later opens see fresh metadata.

import numpy as np
import zarr

root = zarr.open_group("consolidated.zarr", mode="w-", zarr_format=2)
root.create_array("a", data=np.arange(8), chunks=(4,))
zarr.consolidate_metadata(root.store)
read = zarr.open_consolidated(root.store, mode="r")
np.testing.assert_array_equal(read["a"][:], np.arange(8))
# When freshness matters, explicitly bypass cached hierarchy metadata:
fresh = zarr.open_group(root.store, mode="r", use_consolidated=False)
assert fresh["a"].shape == (8,)

Consolidate only after metadata writers finish. Use use_consolidated=False for mutable hierarchies and reconsolidate completed revisions. A cached hierarchy may omit newly created children or stale array shapes/attributes; actual chunk data is not cached there. Pass a configured FsspecStore for cloud consolidation: consolidate_metadata has no storage_options argument. Reopen with the same backend authorization.

Sources: Dask writer, Xarray writer, Xarray reader, consolidated metadata.

Raw lines 104 to 133