🔀 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 to_ome_zarr / from_ome_zarr / to_multiscales calls

Unchanged

Remote reads (s3://, gs://, azure://, http(s)://) and storage_options

Unchanged call surface – fsspec-style option names still accepted

.ozx archives (RFC-9)

Unchanged, read and write. .zip archives are read-only

HCS plates and wells

Unchanged for path, URL, and .ozx stores

RFC-4 anatomical orientation, RFC-5 coordinate systems, metadata validation

Unchanged

Sharding (chunks_per_shard), chunking, and the compressor / compressors arguments

Unchanged for gzip, zstd and blosc – the compression the engine writes

CLI flags and the ngff-zarr command

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

Python 3.11 or newer is required

zarr-python is not installed as a dependency

Your code does import zarr without declaring it

zarr-python is no longer installed

zarr-python store objects rejected

You pass LocalStore, MemoryStore, DirectoryStore, …

zarr-python store objects are no longer accepted

Remote writes unsupported

You write to an s3:// / gs:// / azure:// URL

Remote writes are not supported

remote extra is now just obstore

You relied on ngff-zarr[remote] to install fsspec, s3fs, …

The remote extra no longer pulls in fsspec

tensorstore extra removed

You install ngff-zarr[tensorstore] or pass use_tensorstore=True

The tensorstore extra is removed

chunk_store removed

You pass chunk_store= to to_ome_zarr

The chunk_store argument is removed

filters and a non-bytes serializer rejected

You write a codec chain other than bytes plus gzip/zstd/blosc

Only the bytes codec plus gzip, zstd, or blosc is written

Stricter Zarr metadata parsing

You read a store with out-of-spec codec metadata

Zarr metadata is validated more strictly on read

Narrower in-memory mapping reads

You read a sharded store from a dict

In-memory mapping reads cover fewer stores

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, or os.PathLike)

  • a remote URL string (s3://, gs://, azure://, http://, https://), with the remote extra installed

  • a .ozx / .zip archive path

  • an in-memory key-to-bytes MutableMapping – a plain dict works

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

remote extra

fsspec, aiohttp, requests, s3fs, gcsfs, adlfs

obstore

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_tensorstore is still accepted by convert_to_ome_zarr and is a deprecated no-op. Its DeprecationWarning is 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_zarr now also reports the version of v0.5 stores, and is_zarr_store recognizes 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.

  • [ ] zarr is 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, or upgrade_ome_zarr – pass paths or URLs.

  • [ ] config.cache_store is 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-tensorstore are removed from calls and scripts.

  • [ ] chunk_store= is removed from to_ome_zarr calls.

  • [ ] fsspec, s3fs, gcsfs, and adlfs are declared directly if your code imports them.

Further reading