🔀 v0.43 to v0.44¶
Purpose¶
ngff-zarr v0.44 replaces zarr-python and tensorstore with zarrista –
a Python Zarr implementation built on the Rust zarrs library – as the
engine behind writes and behind reads of paths, URLs, and archives. Reads
from in-memory mappings use a small pure-Python reader instead, described in
In-memory mapping reads cover fewer stores.
After this release no module in ngff_zarr imports zarr, and
pip install ngff-zarr no longer installs zarr-python or tensorstore.
The motivation is a leaner, conflict-free install with one engine instead of three. Reads previously went through zarr-python, the fast write path went through tensorstore, and the two disagreed on codec metadata details. zarr-python was also the heaviest part of the dependency tree and the usual source of environment conflicts; it is now a test-only dependency whose sole job is verifying that zarrista-written stores are readable by the reference implementation.
This guide covers what that swap means for you: what keeps working untouched, what breaks, the errors you will see, and the code change for each one.
Most users need to change nothing. If your code writes to local paths with
to_ome_zarr and reads local paths or remote URLs with from_ome_zarr, it
already works on v0.44. The changes below matter if you pass zarr-python
store objects, write to a remote URL, rely on zarr-python arriving
transitively, or run Python 3.10.
What is compatible¶
Nothing about the OME-Zarr data itself changed. The stores v0.44 writes were verified byte-equivalent to those written by v0.43 – same file sets, same metadata documents, same pixels.
Area |
Status |
|---|---|
Stores written by earlier ngff-zarr releases |
Read unchanged |
Stores written by v0.44 |
Standard OME-Zarr, readable by other implementations. 0.4 is Zarr v2, readable by zarr-python 2 and 3; 0.5+ is Zarr v3, which requires zarr-python 3 |
Path-based |
Unchanged |
Remote reads ( |
Unchanged call surface – fsspec-style option names still accepted |
|
Unchanged, read and write. |
HCS plates and wells |
Unchanged for path, URL, and |
RFC-4 anatomical orientation, RFC-5 coordinate systems, metadata validation |
Unchanged |
Sharding ( |
Unchanged for gzip, zstd and blosc – the compression the engine writes |
CLI flags and the |
Unchanged |
Downsampling methods |
Unchanged |
Installing zarr-python alongside ngff-zarr |
Supported – the two no longer conflict |
What is not compatible¶
Change |
You hit it if… |
Section |
|---|---|---|
Python >= 3.11 required |
You run Python 3.10 |
|
zarr-python is not installed as a dependency |
Your code does |
|
zarr-python store objects rejected |
You pass |
|
Remote writes unsupported |
You write to an |
|
|
You relied on |
|
|
You install |
|
|
You pass |
|
|
You write a codec chain other than |
|
Stricter Zarr metadata parsing |
You read a store with out-of-spec codec metadata |
|
Narrower in-memory mapping reads |
You read a sharded store from a |
Python 3.11 or newer is required¶
What changed and why. requires-python moved from >=3.10 to >=3.11
for both ngff-zarr and ngff-zarr-mcp. zarrista does not support Python
3.10.
Error you will see. pip refuses to install rather than failing at
runtime:
ERROR: Ignored the following versions that require a different python version: 0.44.0 Requires-Python >=3.11
ERROR: Could not find a version that satisfies the requirement ngff-zarr==0.44.0
Or, when resolving in place:
ERROR: Package 'ngff-zarr' requires a different Python: 3.10.14 not in '>=3.11'
Migration. Upgrade the interpreter to 3.11 or newer. Python 3.11, 3.12, 3.13, and 3.14 are tested in CI. If you pin an interpreter in CI or a container image, bump it there too:
# GitHub Actions, before
- uses: actions/setup-python@v5
with:
python-version: "3.10"
# After
- uses: actions/setup-python@v5
with:
python-version: "3.11"
Staying on Python 3.10 means staying on ngff-zarr v0.43.
zarr-python is no longer installed¶
What changed and why. zarr was a required runtime dependency in v0.43
and is not a dependency at all in v0.44. zarrista handles Zarr I/O, and
numcodecs – previously a transitive dependency of zarr-python – is now
declared directly for the pure-Python mapping reader.
This is what removes the environment conflicts: zarr-python and ngff-zarr can be installed side by side, in either version, without ngff-zarr constraining the resolution.
Error you will see. Code that used zarr-python transitively now fails at import:
ModuleNotFoundError: No module named 'zarr'
Migration. If you use zarr-python in your own code, declare it. It is a supported, conflict-free co-installation – ngff-zarr’s own test suite installs it to verify round-trips:
pip install ngff-zarr zarr
# pyproject.toml
dependencies = [
"ngff-zarr",
"zarr",
]
If you only used zarr-python to hand a store object to ngff-zarr, you do not need it at all – see the next section.
zarr-python store objects are no longer accepted¶
What changed and why. to_ome_zarr, from_ome_zarr, to_hcs_zarr,
write_hcs_well_image, and upgrade_ome_zarr no longer accept zarr-python
store instances (LocalStore, MemoryStore, DirectoryStore,
FSStore, …). ngff-zarr has no zarr-python to construct or interpret them,
so it takes the path or URL a store wraps and dispatches on that.
Reading¶
from_ome_zarr accepts:
a local directory path (
str,pathlib.Path, oros.PathLike)a remote URL string (
s3://,gs://,azure://,http://,https://), with theremoteextra installeda
.ozx/.ziparchive pathan in-memory key-to-bytes
MutableMapping– a plaindictworks
Anything else raises TypeError:
TypeError: ngff-zarr reads from local directory paths, remote URL strings, zip
archive paths, and key-to-bytes mappings; got LocalStore. Pass a path instead
of a zarr-python store object.
Migration. Pass the path the store wraps:
# Before
from zarr.storage import LocalStore
from ngff_zarr import from_ome_zarr
multiscales = from_ome_zarr(LocalStore("image.ome.zarr"))
# After
from ngff_zarr import from_ome_zarr
multiscales = from_ome_zarr("image.ome.zarr")
An in-memory MemoryStore becomes a plain mapping of the store’s contents:
# Before
from zarr.storage import MemoryStore
from ngff_zarr import from_ome_zarr
store = MemoryStore()
# ... store populated elsewhere ...
multiscales = from_ome_zarr(store)
# After: any key-to-bytes mapping works, including a plain dict
store = {} # keys are store-relative paths, values are the raw bytes
multiscales = from_ome_zarr(store)
See In-memory mapping reads cover fewer stores for the limits of the mapping reader.
Writing¶
to_ome_zarr writes to a local directory path or a .ozx archive path.
In-memory mappings, remote URLs, and zarr-python store objects raise
TypeError:
TypeError: ngff-zarr writes to local directory paths; got LocalStore. Pass a
str, pathlib.Path, or os.PathLike directory path (write to a local directory
and upload afterwards for remote targets). In-memory mappings and zarr-python
store objects are no longer accepted.
Migration.
# Before
from zarr.storage import LocalStore
from ngff_zarr import to_ome_zarr
to_ome_zarr(LocalStore("image.ome.zarr"), multiscales)
# After
from ngff_zarr import to_ome_zarr
to_ome_zarr("image.ome.zarr", multiscales)
Writing directly into a .zip is a separate error, because zarrista can only
read zip archives:
ValueError: zarrista cannot write into zip archives (.zip/.ozx). Write to a
directory and zip it afterwards (see ngff_zarr.rfc9_zip).
The message names both extensions because it comes from the low-level store
check, but only .zip reaches it. .ozx paths are handled earlier by the
RFC-9 write path, which stages a directory and zips it for you, so they keep
working:
to_ome_zarr("image.ozx", multiscales, version="0.5")
To convert an existing directory store to an archive without recomputing,
use write_store_to_zip:
from ngff_zarr.rfc9_zip import write_store_to_zip
write_store_to_zip("image.ome.zarr", "image.ozx", version="0.5")
The cache store setting¶
config.cache_store is now the cache directory path; large-image cache
arrays are written into it through zarrista. A zarr-python store object
raises:
TypeError: config.cache_store must be a local directory path; got LocalStore.
Migration.
# Before
from zarr.storage import LocalStore
from ngff_zarr import config
config.cache_store = LocalStore("/fast/scratch/ngff-cache")
# After
from pathlib import Path
from ngff_zarr import config
config.cache_store = Path("/fast/scratch/ngff-cache")
The CLI equivalent, --cache-dir, is unchanged.
Remote writes are not supported¶
What changed and why. v0.43 could write to remote stores through fsspec.
The zarrista/obstore backend covers remote reads only, so remote write
targets are rejected. Reads from http(s), S3, Google Cloud Storage, and Azure
Blob Storage are unaffected, including storage_options authentication.
Error you will see. A remote URL passed as a write target is handled by the same check as any other non-directory store:
TypeError: ngff-zarr writes to local directory paths; got str. Pass a str,
pathlib.Path, or os.PathLike directory path (write to a local directory and
upload afterwards for remote targets). In-memory mappings and zarr-python
store objects are no longer accepted.
Migration. Write locally, then upload:
# Before
to_ome_zarr("s3://my-bucket/image.ome.zarr", multiscales, version="0.5")
# After
to_ome_zarr("image.ome.zarr", multiscales, version="0.5")
aws s3 sync image.ome.zarr s3://my-bucket/image.ome.zarr
# or: rclone copy image.ome.zarr remote:my-bucket/image.ome.zarr
A single .ozx archive is often a better upload unit than a directory of many
small objects:
to_ome_zarr("image.ozx", multiscales, version="0.5")
aws s3 cp image.ozx s3://my-bucket/image.ozx
The remote extra no longer pulls in fsspec¶
What changed and why. Remote reads route through zarrista’s async API
backed by obstore instead of fsspec. The remote extra shrank
accordingly:
v0.43 |
v0.44 |
|
|---|---|---|
|
|
|
Migration. Installation is unchanged:
pip install "ngff-zarr[remote]"
Read calls are unchanged too – fsspec-style storage_options names (key,
secret, anon, token, endpoint_url, region_name, …) are translated
to their obstore equivalents, and obstore-native names pass through as-is:
from ngff_zarr import from_ome_zarr
multiscales = from_ome_zarr(
"s3://ome-zarr-scivis/v0.5/96x2/carp.ome.zarr",
storage_options={"anon": True},
)
If your own code imports fsspec, s3fs, gcsfs, or adlfs, declare them
directly – they no longer arrive with ngff-zarr[remote]:
pip install "ngff-zarr[remote]" s3fs
The tensorstore extra is removed¶
What changed and why. The fast direct-write path used to be opt-in via tensorstore. zarrista provides it natively, so the path is always on and the optional dependency is gone. The keyword and CLI flag survive as deprecated no-ops so existing callers keep running.
Errors and warnings you will see. The extra no longer exists:
WARNING: ngff-zarr 0.44.0 does not provide the extra 'tensorstore'
Passing the keyword warns:
DeprecationWarning: use_tensorstore is deprecated; the fast direct-write path
now uses the zarrista backend instead of tensorstore.
And on the command line:
Warning: --use-tensorstore is deprecated; using the zarrista backend.
Migration. Drop the extra and the argument. There is no replacement to opt into – the behavior it selected is now the default.
# Before
pip install "ngff-zarr[tensorstore]"
# After
pip install ngff-zarr
# Before
to_ome_zarr("image.ome.zarr", multiscales, use_tensorstore=True)
# After
to_ome_zarr("image.ome.zarr", multiscales)
# Before
ngff-zarr --use-tensorstore -i input.tif -o output.ome.zarr
# After
ngff-zarr -i input.tif -o output.ome.zarr
The MCP server’s use_tensorstore option behaves the same way; see
MCP server changes.
The chunk store argument is removed¶
What changed and why. to_ome_zarr(..., chunk_store=...) let chunks and
metadata land in different zarr-python stores. The zarrista write path targets
one local directory, so the argument no longer has a meaning and is rejected
explicitly rather than silently ignored.
Error you will see.
TypeError: chunk_store is no longer supported: chunks and metadata are always
written to the same local directory store.
Migration. Remove the argument:
# Before
to_ome_zarr("image.ome.zarr", multiscales, chunk_store="chunks")
# After
to_ome_zarr("image.ome.zarr", multiscales)
Only the bytes codec plus gzip, zstd, or blosc is written¶
What changed and why. zarr-python resolves a codec chain through its codec
entry points, so any registered codec could be written. The zarrista engine
encodes with the bytes codec plus one bytes-to-bytes compressor – gzip,
zstd, or blosc. filters (array-to-array codecs) and a serializer naming
anything but bytes describe chains it cannot write, and are rejected. A
serializer that names bytes asks for what the writer already does and is
accepted – unless it also asks for endian: big, which the engine does not
encode either.
chunks is rejected too, with a TypeError naming where to set it instead:
the stored chunk shape follows the dask chunking of the images, which
to_multiscales(..., chunks=...) sets.
This affects codec chains, not compression settings: compressor and
compressors are unchanged for the three compressors above.
Error you will see.
ValueError: filters are not supported by the zarrista write engine, which
encodes with the bytes codec plus gzip, zstd, or blosc compression. To write
another codec chain, create the store's metadata and empty arrays with
to_ome_zarr(..., metadata_only=True) -- without the codec arguments, which that
call rejects the same way -- then re-create and fill each dataset with a writer
that supports those codecs (for example zarr-python's zarr.create_array with
overwrite=True).
Any other unrecognized array-creation keyword is ignored with a
DeprecationWarning naming it, and will become a TypeError in a later
release. Before v0.46 such a keyword was accepted and silently dropped.
Migration. Write the metadata with ngff-zarr and the arrays with a writer
that implements the codecs. metadata_only=True creates every scale level’s
array – shape, dtype, chunks, shards – and writes no pixels, so each one can
be re-created with its codec chain and filled. Leave the codec arguments off
that call: it validates them exactly like a full write and raises the same
error.
# Before -- silently produced a plain `bytes` array on v0.44 and v0.45
to_ome_zarr(
"image.ome.zarr",
multiscales,
version="0.5",
filters=[...],
serializer=...,
compressors=[...],
)
# After
import dask.array as da
import zarr
to_ome_zarr("image.ome.zarr", multiscales, version="0.5", metadata_only=True)
for dataset, level in zip(multiscales.metadata.datasets, multiscales.images):
skeleton = zarr.open_array("image.ome.zarr", path=dataset.path, mode="r")
array = zarr.create_array(
"image.ome.zarr",
name=dataset.path,
shape=skeleton.shape,
chunks=skeleton.chunks, # keep the layout ngff-zarr chose ...
shards=skeleton.shards, # ... including its sharding
dtype=skeleton.dtype,
filters=[...],
serializer=...,
compressors=[...],
fill_value=0,
dimension_names=list(level.dims),
overwrite=True, # replace the skeleton, codec chain and all
)
# One dask chunk per stored write unit: every chunk (or shard) has a
# single writer and needs no lock, and no level is held in memory whole.
write_shape = skeleton.shards or skeleton.chunks
da.store(level.data.rechunk(write_shape), array, lock=False)
zarr.consolidate_metadata("image.ome.zarr")
Two details are easy to lose. Re-creating the array from skeleton.shape
rather than from a materialized level.data keeps the migration lazy, and
carrying skeleton.chunks and skeleton.shards over keeps the layout
ngff-zarr picked – passing the full shape as chunks would collapse each
level into one chunk and drop the sharding. The consolidation at the end
matters too: ngff-zarr consolidated the skeletons it wrote, and a reader that
trusts the consolidated document would otherwise see the codec chain the
skeletons had rather than the one the arrays carry.
Reading such a store needs the same split – from_ome_zarr decodes through
zarrista and raises on the foreign chain, while ngff_zarr.validate checks the
OME metadata from the group attributes and zarr-python reads the arrays.
Zarr metadata is validated more strictly on read¶
What changed and why. zarrs parses Zarr v3 codec configurations against
the specification. Some tools wrote an nthreads key into blosc codec
configurations – a local decoding hint that is not part of the Zarr v3 blosc
codec spec. zarr-python tolerated it; zarrista rejects the array as
out-of-spec. (The blocksize key often written alongside it is in the spec
and is accepted.)
ngff-zarr tolerates and ignores the key when it appears only in a store’s
consolidated metadata (the root zarr.json), which is read as a plain JSON
document. When it appears in an individual array’s zarr.json, the read
fails.
Error you will see.
ArrayCreateError: configuration is unsupported: data did not match any variant
of untagged enum BloscCodecConfiguration
Migration. Repair the store by removing the offending key from the
blosc codec configuration object in each array-level zarr.json:
import json
from pathlib import Path
for doc_path in Path("image.ome.zarr").rglob("zarr.json"):
doc = json.loads(doc_path.read_text())
changed = False
for codec in doc.get("codecs") or []:
if codec.get("name") == "blosc":
configuration = codec.get("configuration", {})
if "nthreads" in configuration:
del configuration["nthreads"]
changed = True
if changed:
doc_path.write_text(json.dumps(doc))
Re-writing the store with v0.44 also produces spec-conformant metadata.
In-memory mapping reads cover fewer stores¶
What changed and why. Key-to-bytes mappings are still accepted by
from_ome_zarr, but they are read by a pure-Python reader rather than by
zarr-python. zarrista’s SyncStore is a closed union with no hook for a
custom mapping, so ngff-zarr ships its own reader for this case. It covers
Zarr v2 fully, and Zarr v3 for unsharded arrays whose codec chain is the
bytes codec plus gzip, zstd, or blosc compression.
Errors you will see.
ValueError: Unsupported zarr format 3 array-to-bytes codec 'sharding_indexed'
in a mapping store; only the 'bytes' codec is supported. Sharded stores must
be read through zarrista.
ValueError: Unsupported zarr format 3 codec 'lz4' in a mapping store.
Supported bytes-to-bytes codecs: gzip, zstd, blosc; sharded stores must be
read through zarrista.
Migration. Read the store from a path or URL instead of materializing it into a mapping – that route goes through zarrista and handles sharding and the full codec set natively:
# Sharded store as a mapping: unsupported
multiscales = from_ome_zarr(mapping_of_store_contents)
# As a directory path or .ozx archive: fully supported
multiscales = from_ome_zarr("image.ome.zarr")
multiscales = from_ome_zarr("image.ozx")
MCP server changes¶
ngff-zarr-mcp moves to the zarrista-backed core with a backward-compatible
option surface:
Python >= 3.11 is required, as for
ngff-zarr.use_tensorstoreis still accepted byconvert_to_ome_zarrand is a deprecated no-op. ItsDeprecationWarningis suppressed so MCP responses stay clean; remove the option at your convenience.zarr-python is no longer used at runtime. Store inspection reads metadata documents directly.
validate_ome_zarrnow also reports the version of v0.5 stores, andis_zarr_storerecognizes Zarr v3.Output paths must be local. As with the Python API, write locally and upload afterwards.
Migration checklist¶
[ ] Interpreter is Python 3.11 or newer, in dev environments and in CI.
[ ]
zarris declared as a direct dependency if your code imports it.[ ] No zarr-python store objects are passed to
to_ome_zarr,from_ome_zarr,to_hcs_zarr,write_hcs_well_image, orupgrade_ome_zarr– pass paths or URLs.[ ]
config.cache_storeis a directory path, not a store object.[ ] Remote write targets are replaced by a local write plus an upload step.
[ ]
ngff-zarr[tensorstore]is dropped from install commands and requirement files.[ ]
use_tensorstore=/--use-tensorstoreare removed from calls and scripts.[ ]
chunk_store=is removed fromto_ome_zarrcalls.[ ]
fsspec,s3fs,gcsfs, andadlfsare declared directly if your code imports them.
Further reading¶
Frequently Asked Questions – short answers to the errors above
Python Interface – accepted store types and the zarrista backend
Installation – current optional dependency extras
Pull request #654 – the full implementation and rationale