Skip to content

Commit 0500a37

Browse files
d-v-bclaude
andcommitted
docs(zarr-metadata): the narrow configuration type is sound, not a compromise
The previous wording justified keeping `Mapping[str, JSONValue]` by what widening did to this package's Hypothesis strategies. That is backwards: a test utility's behaviour is a fact about the utility, not an argument about a type. The real reason is that the conversion mypy refuses is sound here. Mypy's rule exists because an ordinary TypedDict may carry extra items of undeclared types, so the union of declared value types does not bound the mapping. Every TypedDict in this package is `closed` (PEP 728), which forbids that; pyright implements PEP 728 and accepts the assignment, and mypy has not implemented it yet (python/mypy#8994, python/mypy#18439). So the annotation is accurate and stays accurate, one checker needs a `cast` until it catches up, and widening would buy mypy's silence by making the type say something false. Assisted-by: ClaudeCode:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
1 parent cb4e2b0 commit 0500a37

2 files changed

Lines changed: 29 additions & 15 deletions

File tree

‎packages/zarr-metadata/changes/4379.misc.2.md‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ documents report identically.
2323

2424
One thing deliberately not done. An entity's `to_json` returns its own
2525
object TypedDict, which mypy will not accept where a
26-
`ZarrV3MetadataFieldJSON` is wanted, because it reads a TypedDict as
27-
`Mapping[str, object]` and never as the `Mapping[str, JSONValue]` the
28-
envelope declares. Widening the envelope fixes mypy and costs more than
29-
it buys: `object` stops the package's own `from_type` strategies
30-
terminating, and `Any` stops them generating the ill-typed members they
31-
exist to generate. The narrow type is also the true one, so it stays, and
32-
`zarr_metadata.v3.entity` says so.
26+
`ZarrV3MetadataFieldJSON` is wanted: it reads every TypedDict as
27+
`Mapping[str, object]`, never as the `Mapping[str, JSONValue]` the
28+
envelope declares, so the write path needs a `cast` under mypy.
29+
30+
The conversion is sound and the annotation stays. Mypy's rule exists
31+
because an ordinary TypedDict may carry extra items of undeclared types,
32+
so the union of the declared value types does not bound the mapping;
33+
every TypedDict here is `closed` (PEP 728), which forbids that, and
34+
pyright implements PEP 728 while mypy does not yet (python/mypy#8994,
35+
python/mypy#18439). Widening `configuration` would satisfy mypy by making
36+
the annotation say something false, and a configuration's values are
37+
JSON. `zarr_metadata.v3.entity` records the `cast` and the reason.

‎packages/zarr-metadata/src/zarr_metadata/v3/entity.py‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,23 @@ class AcmeLz4Codec(CodecEntity):
3939
4040
One known friction, under mypy only. An entity's `to_json` returns its own
4141
object TypedDict, and mypy does not accept that where a
42-
`ZarrV3MetadataFieldJSON` is wanted -- it reads a TypedDict as
43-
`Mapping[str, object]` and never as the `Mapping[str, JSONValue]` the
44-
envelope declares. Putting `to_json()` output straight into a `codecs`
45-
list therefore needs a `cast` under mypy; pyright accepts it. Widening the
46-
envelope fixes mypy and costs more than it buys: with `object` the
47-
package's own `st.from_type` strategies stop terminating, and with `Any`
48-
they stop generating the ill-typed members they exist to generate. The
49-
narrow type is also the true one -- a configuration's values are JSON.
42+
`ZarrV3MetadataFieldJSON` is wanted: it reads every TypedDict as
43+
`Mapping[str, object]`, never as the `Mapping[str, JSONValue]` the
44+
envelope declares. So putting `to_json()` output straight into a `codecs`
45+
list needs a `cast` under mypy. Pyright accepts it.
46+
47+
That conversion is sound here, which is why pyright is the one that is
48+
right. The rule mypy is applying exists because an ordinary TypedDict may
49+
carry extra items of types it never declared, so the union of the
50+
declared value types does not bound what is in the mapping. Every
51+
TypedDict in this package is `closed` (PEP 728), which forbids exactly
52+
that, and pyright implements PEP 728. Mypy does not yet -- see
53+
python/mypy#8994 and python/mypy#18439.
54+
55+
The type therefore stays as it is. Widening `configuration` to
56+
`Mapping[str, object]` or `Mapping[str, Any]` would satisfy mypy by
57+
making the annotation say something false: a configuration's values are
58+
JSON, and that is worth more than one checker's `cast`.
5059
"""
5160

5261
from __future__ import annotations

0 commit comments

Comments
 (0)