From 8edfafaf868e45743cc542b155f91692082b2783 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 30 Sep 2026 18:13:22 +0200 Subject: [PATCH 1/3] feat(api): declare mode and data on create `create` accepted `mode` and `data` only through `**kwargs`: `mode` was popped for opening the store and `data` reached `AsyncArray._create`. Both are now named, documented parameters of the sync and async `create`, and `**kwargs` is gone, so an unknown keyword raises a TypeError from `create` itself. Assisted-by: ClaudeCode:claude-opus-5-5 --- changes/create-mode-data.feature.md | 1 + src/zarr/api/asynchronous.py | 15 ++++++-- src/zarr/api/synchronous.py | 16 ++++++-- tests/test_api.py | 58 ++++++++++++++++++++++++++++- 4 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 changes/create-mode-data.feature.md diff --git a/changes/create-mode-data.feature.md b/changes/create-mode-data.feature.md new file mode 100644 index 0000000000..4155507b1d --- /dev/null +++ b/changes/create-mode-data.feature.md @@ -0,0 +1 @@ +`zarr.create` and `zarr.api.asynchronous.create` now declare and document their `mode` and `data` parameters. Both parameters were accepted before but were not part of the signature. `create` no longer takes arbitrary keyword arguments, so an unknown keyword raises a `TypeError` naming `create` instead of the private `AsyncArray._create`. diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 8371fb0663..048c9577b3 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -922,7 +922,8 @@ async def create( dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, config: ArrayConfigLike | None = None, - **kwargs: Any, + mode: AccessModeLiteral | None = None, + data: npt.ArrayLike | None = None, ) -> AnyAsyncArray: """Create an array. @@ -1042,6 +1043,15 @@ async def create( config : ArrayConfigLike, optional Runtime configuration of the array. If provided, will override the default values from `zarr.config.array`. + mode : {'r', 'r+', 'a', 'w', 'w-'}, optional + The access mode used to open `store`. The default, `None`, is `'a'`. + With `'w'`, everything stored under `path` is deleted before the array is + created. With `'w-'`, creation fails if anything is stored under `path`. + With `'r'`, the store is opened read-only and creation fails. + Ignored if `store` is a `StorePath`, which is already open. + data : array-like, optional + Values written into the new array after it is created, as `array[...] = data`. + `data` does not determine the shape or data type of the array. Returns ------- @@ -1066,7 +1076,6 @@ async def create( if write_empty_chunks is not None: _warn_write_empty_chunks_kwarg() - mode = kwargs.pop("mode", None) if mode is None: mode = "a" store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) @@ -1102,7 +1111,7 @@ async def create( dimension_names=dimension_names, attributes=attributes, config=config_parsed, - **kwargs, + data=data, ) diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index 13bdf2a651..0acfd38000 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -630,7 +630,6 @@ def create_group( ) -# TODO: add type annotations for kwargs def create( shape: tuple[int, ...] | int, *, # Note: this is a change from v2 @@ -666,7 +665,8 @@ def create( dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, config: ArrayConfigLike | None = None, - **kwargs: Any, + mode: AccessModeLiteral | None = None, + data: npt.ArrayLike | None = None, ) -> AnyArray: """Create an array. @@ -786,6 +786,15 @@ def create( config : ArrayConfigLike, optional Runtime configuration of the array. If provided, will override the default values from `zarr.config.array`. + mode : {'r', 'r+', 'a', 'w', 'w-'}, optional + The access mode used to open `store`. The default, `None`, is `'a'`. + With `'w'`, everything stored under `path` is deleted before the array is + created. With `'w-'`, creation fails if anything is stored under `path`. + With `'r'`, the store is opened read-only and creation fails. + Ignored if `store` is a `StorePath`, which is already open. + data : array-like, optional + Values written into the new array after it is created, as `array[...] = data`. + `data` does not determine the shape or data type of the array. Returns ------- @@ -822,7 +831,8 @@ def create( dimension_names=dimension_names, storage_options=storage_options, config=config, - **kwargs, + mode=mode, + data=data, ) ) ) diff --git a/tests/test_api.py b/tests/test_api.py index 16dc5bbffb..03f1285542 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,7 +15,7 @@ from pathlib import Path from zarr.abc.store import Store - from zarr.core.common import JSON, MemoryOrder, ZarrFormat + from zarr.core.common import JSON, AccessModeLiteral, MemoryOrder, ZarrFormat from zarr.types import AnyArray import contextlib @@ -46,6 +46,7 @@ from zarr.core.buffer import NDArrayLike from zarr.errors import ( ArrayNotFoundError, + ContainsArrayError, MetadataValidationError, ZarrDeprecationWarning, ZarrUserWarning, @@ -84,6 +85,61 @@ def test_create(memory_store: Store) -> None: z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore[arg-type] +@pytest.mark.parametrize( + ("mode", "existing"), + [(None, False), ("a", False), ("r+", False), ("w", False), ("w", True), ("w-", False)], +) +@pytest.mark.parametrize("data", [None, np.arange(3, dtype="float64")]) +def test_create_mode_data(mode: AccessModeLiteral | None, existing: bool, data: Any) -> None: + """ + `create` opens the store with `mode` and writes `data` into the new array. + """ + store = MemoryStore() + if existing: + create_array(store, shape=(2,), dtype="int8") + z = create(shape=(3,), store=store, mode=mode, data=data) + assert z.shape == (3,) + assert z.dtype == np.dtype("float64") + expected = np.zeros(3) if data is None else data + assert_array_equal(z[:], expected) + + +def test_create_mode_a_existing_array() -> None: + """ + `create` with mode `'a'` refuses to replace an existing array. + """ + store = MemoryStore() + create_array(store, shape=(2,), dtype="int8") + with pytest.raises(ContainsArrayError): + create(shape=(3,), store=store, mode="a") + + +def test_create_mode_w_minus_existing_data() -> None: + """ + `create` with mode `'w-'` refuses to write where data is stored. + """ + store = MemoryStore() + create_array(store, shape=(2,), dtype="int8") + with pytest.raises(FileExistsError, match="mode 'w-'"): + create(shape=(3,), store=store, mode="w-") + + +def test_create_mode_r() -> None: + """ + `create` with mode `'r'` opens the store read-only, so creation fails. + """ + with pytest.raises(ValueError, match="read-only"): + create(shape=(3,), store=MemoryStore(), mode="r") + + +def test_create_unknown_keyword() -> None: + """ + `create` rejects keyword arguments it does not declare, naming itself in the error. + """ + with pytest.raises(TypeError, match=r"^create\(\) got an unexpected keyword argument 'typo'"): + create(shape=(3,), typo=1) # type: ignore[call-arg] + + @pytest.mark.parametrize( "func", [ From 3b73c12ccc7c9b69695361d5b6f9ca9c2e637766 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 30 Sep 2026 18:16:28 +0200 Subject: [PATCH 2/3] chore: number the changelog fragment for #4455 Assisted-by: ClaudeCode:claude-opus-5-5 --- changes/{create-mode-data.feature.md => 4455.feature.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changes/{create-mode-data.feature.md => 4455.feature.md} (100%) diff --git a/changes/create-mode-data.feature.md b/changes/4455.feature.md similarity index 100% rename from changes/create-mode-data.feature.md rename to changes/4455.feature.md From 4dc06f0698dfcd5c5b75e9f0d821dcfc5d39fa00 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 30 Sep 2026 18:45:37 +0200 Subject: [PATCH 3/3] fix(api): document mode as legacy and pin create's mode and data behavior Adversarial review of #4455 found the new docstrings overpromised: `data` is not `array[...] = data` for a zarr Array, `'r'` does not always open the store read-only, and the `mode`/`overwrite` interaction was undocumented. `mode` is now documented as the legacy spelling with `overwrite` preferred, each mode's outcome is stated, and `data` points to `create_array(data=...)`. The tests now cover the async `create`, the default mode, the `mode`/`overwrite` combinations, that `'w'` removes old chunks and stray keys, invalid modes, and that an unknown keyword leaves the store untouched. The fragment moves to misc. Assisted-by: ClaudeCode:claude-opus-5-5 --- changes/4455.feature.md | 1 - changes/4455.misc.md | 1 + src/zarr/api/asynchronous.py | 25 ++++++--- src/zarr/api/synchronous.py | 25 ++++++--- tests/test_api.py | 103 +++++++++++++++++++++++++++-------- 5 files changed, 118 insertions(+), 37 deletions(-) delete mode 100644 changes/4455.feature.md create mode 100644 changes/4455.misc.md diff --git a/changes/4455.feature.md b/changes/4455.feature.md deleted file mode 100644 index 4155507b1d..0000000000 --- a/changes/4455.feature.md +++ /dev/null @@ -1 +0,0 @@ -`zarr.create` and `zarr.api.asynchronous.create` now declare and document their `mode` and `data` parameters. Both parameters were accepted before but were not part of the signature. `create` no longer takes arbitrary keyword arguments, so an unknown keyword raises a `TypeError` naming `create` instead of the private `AsyncArray._create`. diff --git a/changes/4455.misc.md b/changes/4455.misc.md new file mode 100644 index 0000000000..362c96bb3f --- /dev/null +++ b/changes/4455.misc.md @@ -0,0 +1 @@ +`zarr.create` and `zarr.api.asynchronous.create` now list `mode` and `data` in their signatures and docstrings. Both were accepted before without being declared. An unknown keyword now raises a `TypeError` from `create` before anything in the store is touched. Previously, with `mode="w"`, the existing data was deleted before the error was raised. `mode` is documented as a legacy spelling; `overwrite` is preferred. diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 048c9577b3..bef4d5c78f 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -1044,14 +1044,25 @@ async def create( Runtime configuration of the array. If provided, will override the default values from `zarr.config.array`. mode : {'r', 'r+', 'a', 'w', 'w-'}, optional - The access mode used to open `store`. The default, `None`, is `'a'`. - With `'w'`, everything stored under `path` is deleted before the array is - created. With `'w-'`, creation fails if anything is stored under `path`. - With `'r'`, the store is opened read-only and creation fails. - Ignored if `store` is a `StorePath`, which is already open. + Legacy way to control overwriting, kept for compatibility with Zarr-Python 2. + Prefer `overwrite`. The access mode used to open `store`; the default, `None`, + is `'a'`. + + - `'a'` and `'r+'` create the array and fail if a node exists at `path`, + unless `overwrite` is `True`. + - `'w'` replaces anything stored under `path` (the whole store if `path` is + not set), even if `overwrite` is `False`. On a store that cannot delete + keys, `'w'` raises an error instead of replacing an existing node. + - `'w-'` fails if anything is stored under `path`, even if `overwrite` is + `True`. + - `'r'` always fails. + + `mode` has no effect, and is not validated, if `store` is a `StorePath`. data : array-like, optional - Values written into the new array after it is created, as `array[...] = data`. - `data` does not determine the shape or data type of the array. + Values written into the new array after it is created. Unlike the `data` + parameter of `create_array`, it does not set the shape or data type of the + array. To create an array from existing data, use `create_array(data=...)`. + A Zarr array is not supported as `data`. Returns ------- diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index 0acfd38000..6b387dafa5 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -787,14 +787,25 @@ def create( Runtime configuration of the array. If provided, will override the default values from `zarr.config.array`. mode : {'r', 'r+', 'a', 'w', 'w-'}, optional - The access mode used to open `store`. The default, `None`, is `'a'`. - With `'w'`, everything stored under `path` is deleted before the array is - created. With `'w-'`, creation fails if anything is stored under `path`. - With `'r'`, the store is opened read-only and creation fails. - Ignored if `store` is a `StorePath`, which is already open. + Legacy way to control overwriting, kept for compatibility with Zarr-Python 2. + Prefer `overwrite`. The access mode used to open `store`; the default, `None`, + is `'a'`. + + - `'a'` and `'r+'` create the array and fail if a node exists at `path`, + unless `overwrite` is `True`. + - `'w'` replaces anything stored under `path` (the whole store if `path` is + not set), even if `overwrite` is `False`. On a store that cannot delete + keys, `'w'` raises an error instead of replacing an existing node. + - `'w-'` fails if anything is stored under `path`, even if `overwrite` is + `True`. + - `'r'` always fails. + + `mode` has no effect, and is not validated, if `store` is a `StorePath`. data : array-like, optional - Values written into the new array after it is created, as `array[...] = data`. - `data` does not determine the shape or data type of the array. + Values written into the new array after it is created. Unlike the `data` + parameter of `create_array`, it does not set the shape or data type of the + array. To create an array from existing data, use `create_array(data=...)`. + A Zarr array is not supported as `data`. Returns ------- diff --git a/tests/test_api.py b/tests/test_api.py index 03f1285542..0c0ecff77f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,6 +14,8 @@ from collections.abc import Callable from pathlib import Path + import numpy.typing as npt + from zarr.abc.store import Store from zarr.core.common import JSON, AccessModeLiteral, MemoryOrder, ZarrFormat from zarr.types import AnyArray @@ -43,7 +45,8 @@ save_array, save_group, ) -from zarr.core.buffer import NDArrayLike +from zarr.core.buffer import NDArrayLike, default_buffer_prototype +from zarr.core.sync import sync from zarr.errors import ( ArrayNotFoundError, ContainsArrayError, @@ -85,43 +88,85 @@ def test_create(memory_store: Store) -> None: z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore[arg-type] +def _create_async(**kwargs: Any) -> AnyArray: + return Array(sync(zarr.api.asynchronous.create(**kwargs))) + + +CREATE_FUNCS = pytest.mark.parametrize( + "create_func", [create, _create_async], ids=["sync", "async"] +) + + +def _write_existing_array(store: MemoryStore) -> None: + """ + Store an array with written chunks, plus a key that belongs to no Zarr node. + """ + old = create_array(store, shape=(4,), chunks=(2,), dtype="int8") + old[:] = [1, 2, 3, 4] + sync(store.set("junk", default_buffer_prototype().buffer.from_bytes(b"junk"))) + + +@CREATE_FUNCS @pytest.mark.parametrize( - ("mode", "existing"), - [(None, False), ("a", False), ("r+", False), ("w", False), ("w", True), ("w-", False)], + ("mode", "overwrite", "existing"), + [ + (None, False, False), + ("a", False, False), + ("r+", False, False), + ("w", False, False), + ("w-", False, False), + ("a", True, True), + ("r+", True, True), + ("w", False, True), + ("w", True, True), + ], + ids=lambda v: repr(v), ) -@pytest.mark.parametrize("data", [None, np.arange(3, dtype="float64")]) -def test_create_mode_data(mode: AccessModeLiteral | None, existing: bool, data: Any) -> None: +@pytest.mark.parametrize("data", [None, np.arange(3, dtype="int8")], ids=["no-data", "int8-data"]) +def test_create_mode_data( + create_func: Callable[..., AnyArray], + mode: AccessModeLiteral | None, + overwrite: bool, + existing: bool, + data: npt.NDArray[Any] | None, +) -> None: """ - `create` opens the store with `mode` and writes `data` into the new array. + `create` opens the store with `mode`, replaces what is stored under the path when + `mode` or `overwrite` says so, and writes `data` without taking its data type. """ store = MemoryStore() if existing: - create_array(store, shape=(2,), dtype="int8") - z = create(shape=(3,), store=store, mode=mode, data=data) - assert z.shape == (3,) + _write_existing_array(store) + reference = MemoryStore() + create(shape=(3,), chunks=(2,), store=reference, data=data) + + z = create_func(shape=(3,), chunks=(2,), store=store, mode=mode, overwrite=overwrite, data=data) + + assert sorted(store._store_dict) == sorted(reference._store_dict) assert z.dtype == np.dtype("float64") - expected = np.zeros(3) if data is None else data - assert_array_equal(z[:], expected) + assert_array_equal(z[:], np.zeros(3) if data is None else data) -def test_create_mode_a_existing_array() -> None: +@pytest.mark.parametrize("mode", [None, "a", "r+"]) +def test_create_existing_array_without_overwrite(mode: AccessModeLiteral | None) -> None: """ - `create` with mode `'a'` refuses to replace an existing array. + `create` refuses to replace an existing array unless `mode` or `overwrite` allows it. """ store = MemoryStore() - create_array(store, shape=(2,), dtype="int8") + _write_existing_array(store) with pytest.raises(ContainsArrayError): - create(shape=(3,), store=store, mode="a") + create(shape=(3,), store=store, mode=mode) -def test_create_mode_w_minus_existing_data() -> None: +@pytest.mark.parametrize("overwrite", [False, True]) +def test_create_mode_w_minus_existing_data(overwrite: bool) -> None: """ - `create` with mode `'w-'` refuses to write where data is stored. + `create` with mode `'w-'` refuses to write where data is stored, whatever `overwrite` says. """ store = MemoryStore() - create_array(store, shape=(2,), dtype="int8") + _write_existing_array(store) with pytest.raises(FileExistsError, match="mode 'w-'"): - create(shape=(3,), store=store, mode="w-") + create(shape=(3,), store=store, mode="w-", overwrite=overwrite) def test_create_mode_r() -> None: @@ -132,12 +177,26 @@ def test_create_mode_r() -> None: create(shape=(3,), store=MemoryStore(), mode="r") -def test_create_unknown_keyword() -> None: +def test_create_invalid_mode() -> None: """ - `create` rejects keyword arguments it does not declare, naming itself in the error. + `create` rejects a mode that is not an access mode. """ + with pytest.raises(ValueError, match="Invalid mode: x"): + create(shape=(3,), store=MemoryStore(), mode="x") # type: ignore[arg-type] + + +@CREATE_FUNCS +def test_create_unknown_keyword(create_func: Callable[..., AnyArray]) -> None: + """ + `create` rejects keyword arguments it does not declare, naming itself in the error, + before `mode="w"` deletes anything. + """ + store = MemoryStore() + _write_existing_array(store) + before = dict(store._store_dict) with pytest.raises(TypeError, match=r"^create\(\) got an unexpected keyword argument 'typo'"): - create(shape=(3,), typo=1) # type: ignore[call-arg] + create_func(shape=(3,), store=store, mode="w", typo=1) + assert store._store_dict == before @pytest.mark.parametrize(