Summary
zarr_warnings_suppress_unstable_structs_v3 and zarr_warnings_suppress_unstable_numcodecs_v3 in mdio/core/zarr_io.py call warnings.filterwarnings("ignore", ...) without warnings.catch_warnings(), and their finally block is pass. The filter is therefore never removed: entering either context manager once silences that warning category process-globally for the remainder of the interpreter's life, not just for the block.
Affected code
src/mdio/core/zarr_io.py (v1.2.1, a2895b5):
@contextmanager
def zarr_warnings_suppress_unstable_structs_v3() -> Generator[None, None, None]:
"""Context manager to suppress Zarr V3 unstable structured array warning."""
warn = r"The data type \((.*?)\) does not have a Zarr V3 specification\."
warnings.filterwarnings("ignore", message=warn, category=UnstableSpecificationWarning)
try:
yield
finally:
pass # <- filter is never restored
zarr_warnings_suppress_unstable_numcodecs_v3 has the same shape for ZarrUserWarning.
By contrast mdio/segy/creation.py:92 does use with warnings.catch_warnings(): and is correctly scoped — so the pattern is already used properly elsewhere in the codebase.
Reproduction
import warnings
from mdio.core.zarr_io import (
zarr_warnings_suppress_unstable_structs_v3 as structs,
zarr_warnings_suppress_unstable_numcodecs_v3 as codecs,
)
print(len(warnings.filters)) # 10
with structs():
pass
with codecs():
pass
print(len(warnings.filters)) # 12 <- still 12, outside both blocks
Measured on multidimio==1.2.1, segy==0.6.0, zarr==3.3.0, Python 3.12.13.
The two leaked entries are:
action='ignore' category=UnstableSpecificationWarning
message='The data type \\((.*?)\\) does not have a Zarr V3 specification\\.'
action='ignore' category=ZarrUserWarning
message='Numcodecs codecs are not in the Zarr version 3 specification'
Why it matters
A caller that touches any code path entering these context managers is permanently deafened to those two warnings, including for its own arrays written later and entirely outside MDIO. There is no way for that caller to opt back in: an outer warnings.catch_warnings(record=True) with simplefilter("always") observes zero of the suppressed warnings, because the leaked filter is inserted at the front of warnings.filters and matches first.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
with structs():
warnings.warn("The data type (struct) does not have a Zarr V3 specification.",
UnstableSpecificationWarning)
print(len(caught)) # 0
This matters most for tools that must report, rather than inherit, what a dependency chose to silence — the UnstableSpecificationWarning here is a genuine portability signal (the headers array's data_type: {"name": "struct"} has no Zarr v3 core specification), and a downstream consumer may reasonably need to surface it.
It also affects test suites: a suite running with -W error or filterwarnings = ["error"] will silently stop erroring on these categories after the first ingest, in a way that does not reset between tests in the same process.
Suggested fix
Scope the filter, matching the pattern already used in creation.py:
@contextmanager
def zarr_warnings_suppress_unstable_structs_v3() -> Generator[None, None, None]:
"""Context manager to suppress Zarr V3 unstable structured array warning."""
warn = r"The data type \((.*?)\) does not have a Zarr V3 specification\."
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=warn, category=UnstableSpecificationWarning)
yield
Note warnings.catch_warnings() is not thread-safe (it mutates global state), so if these context managers can be entered concurrently that is worth considering separately — though the current code has the same exposure, plus the leak.
A regression test could simply assert len(warnings.filters) is unchanged across the block, or better, assert the warning is observable again afterwards.
Context
Found while building a SEG-Y → MDIO conversion validator that records, rather than inherits, every warning a dependency suppresses. Happy to open a PR with the fix and a regression test if that's useful.
Summary
zarr_warnings_suppress_unstable_structs_v3andzarr_warnings_suppress_unstable_numcodecs_v3inmdio/core/zarr_io.pycallwarnings.filterwarnings("ignore", ...)withoutwarnings.catch_warnings(), and theirfinallyblock ispass. The filter is therefore never removed: entering either context manager once silences that warning category process-globally for the remainder of the interpreter's life, not just for the block.Affected code
src/mdio/core/zarr_io.py(v1.2.1,a2895b5):zarr_warnings_suppress_unstable_numcodecs_v3has the same shape forZarrUserWarning.By contrast
mdio/segy/creation.py:92does usewith warnings.catch_warnings():and is correctly scoped — so the pattern is already used properly elsewhere in the codebase.Reproduction
Measured on
multidimio==1.2.1,segy==0.6.0,zarr==3.3.0, Python 3.12.13.The two leaked entries are:
Why it matters
A caller that touches any code path entering these context managers is permanently deafened to those two warnings, including for its own arrays written later and entirely outside MDIO. There is no way for that caller to opt back in: an outer
warnings.catch_warnings(record=True)withsimplefilter("always")observes zero of the suppressed warnings, because the leaked filter is inserted at the front ofwarnings.filtersand matches first.This matters most for tools that must report, rather than inherit, what a dependency chose to silence — the
UnstableSpecificationWarninghere is a genuine portability signal (theheadersarray'sdata_type: {"name": "struct"}has no Zarr v3 core specification), and a downstream consumer may reasonably need to surface it.It also affects test suites: a suite running with
-W errororfilterwarnings = ["error"]will silently stop erroring on these categories after the first ingest, in a way that does not reset between tests in the same process.Suggested fix
Scope the filter, matching the pattern already used in
creation.py:Note
warnings.catch_warnings()is not thread-safe (it mutates global state), so if these context managers can be entered concurrently that is worth considering separately — though the current code has the same exposure, plus the leak.A regression test could simply assert
len(warnings.filters)is unchanged across the block, or better, assert the warning is observable again afterwards.Context
Found while building a SEG-Y → MDIO conversion validator that records, rather than inherits, every warning a dependency suppresses. Happy to open a PR with the fix and a regression test if that's useful.