You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
pynwb cannot construct several neurodata types when a field is backed by a zarr v3 array (or any array-API-standard array that provides ndim/shape/__getitem__ but not __len__/__iter__). This surfaces when reading NWB files through the zarr backend (hdmf-zarr's zarr v3 migration): lazily-read datasets come back as raw zarr.Array objects and are passed straight into the container constructors.
Two independent root causes, both in pynwb code:
docval fields typed collections.abc.Iterable reject zarr.Array, because isinstance(zarr_array, Iterable) is False (the ABC checks for __iter__; zarr v3 arrays don't implement it, even though they iterate fine via the __getitem__ sequence protocol).
a direct len() call on array-like data raises TypeError: object of type 'Array' has no len(), because zarr v3 arrays don't implement __len__.
hdmf already handles zarr v3 arrays in its own type system (it registers zarr.Array in the array_data docval macro and uses ndim-based collection detection), so both issues are pynwb-side.
Environment
pynwb 4.0.0, hdmf 6.1.0, zarr 3.2.1, numpy 2.5.1
Reproduce
Problem 1 — type=Iterable field rejects a zarr array
data_shape=get_data_shape(kwargs['data'], strict_no_data_load=True) # line 148 — already zarr-safeif (
data_shapeisnotNoneandlen(data_shape) ==2anddata_shape[1] !=len(args_to_set['electrodes'].data) # line 152 — len() on a raw zarr.Array
):
ifdata_shape[0] ==len(args_to_set['electrodes'].data): # line 153
...
# TypeError: object of type 'Array' has no len()
Line 148 already uses get_data_shape (zarr-safe); the failure is the len(...) on electrodes.data two lines down.
Root cause detail
iter(zarr_array) / for x in zarr_arraywork (old __getitem__ sequence protocol), but isinstance(zarr_array, collections.abc.Iterable) is False since the ABC only recognizes __iter__. docval resolves an actual-type argtype via isinstance(value, argtype), so the value is rejected.
The array_data docval macro already includes np.ndarray, list, tuple, h5py.Dataset, pd.Series, ExtensionArray, zarr.Array, and hdmf datasets; data adds DataIO. These fields are all genuinely array-valued and are read back as datasets:
Field
Type
Location
control, control_description
TimeSeries (base class)
base.py:162,164
dimension, starting_frame
ImageSeries
image.py:100,89
features, feature_units
AbstractFeatureSeries
misc.py:77,75
waveform_mean, waveform_sd, peak_over_rms
SpikeEventSeries / FeatureExtraction
ecephys.py:340,342,303
Behavior change to note in the changelog: ('array_data', 'data') accepts zarr.Array (the fix) but no longer accepts non-array iterables that Iterable allowed (str, bytes, set, dict, range, generators). For these array-valued fields that is a desirable tightening (a bare str into a shape=(None,) slot was a latent bug); the only realistically-passable inputs affected are range(...) and generator expressions.
Do not change NWBFile.electrode_groups (file.py:413). It is an iterable of ElectrodeGroupobjects, never backed by a dataset, so it has no zarr issue; retyping it to array_data would give no benefit and would needlessly reject a set/generator of groups. Leave it as Iterable (or give it a purpose-fit object-collection type).
2. Replace the direct len() call
In ElectricalSeries.__init__, use get_data_shape(...)[0] / .shape[0] instead of len(electrodes.data), matching the zarr-safe pattern already used at ecephys.py:148. Audit other constructors for direct len()/positional indexing on possibly-lazy datasets.
Suggested tests
Add coverage constructing/round-tripping with zarr-backed values for at least TimeSeries.control, ImageSeries.dimension, and ElectricalSeries (electrodes region), so this does not silently regress.
Alternative / upstream
zarr-python restoring __len__/__iter__ on Array would fix both problems without any pynwb change: zarr-developers/zarr-python#3740 (__len__), #3741 (scalar __getitem__ returning a 0-d ndarray).
Summary
pynwb cannot construct several neurodata types when a field is backed by a zarr v3 array (or any array-API-standard array that provides
ndim/shape/__getitem__but not__len__/__iter__). This surfaces when reading NWB files through the zarr backend (hdmf-zarr's zarr v3 migration): lazily-read datasets come back as rawzarr.Arrayobjects and are passed straight into the container constructors.Two independent root causes, both in pynwb code:
collections.abc.Iterablerejectzarr.Array, becauseisinstance(zarr_array, Iterable)isFalse(the ABC checks for__iter__; zarr v3 arrays don't implement it, even though they iterate fine via the__getitem__sequence protocol).len()call on array-like data raisesTypeError: object of type 'Array' has no len(), because zarr v3 arrays don't implement__len__.hdmf already handles zarr v3 arrays in its own type system (it registers
zarr.Arrayin thearray_datadocval macro and usesndim-based collection detection), so both issues are pynwb-side.Environment
Reproduce
Problem 1 —
type=Iterablefield rejects a zarr arrayProblem 2 — direct
len()on a zarr-backed datasetElectricalSeries.__init__,src/pynwb/ecephys.py:152-153:Line 148 already uses
get_data_shape(zarr-safe); the failure is thelen(...)onelectrodes.datatwo lines down.Root cause detail
iter(zarr_array)/for x in zarr_arraywork (old__getitem__sequence protocol), butisinstance(zarr_array, collections.abc.Iterable)isFalsesince the ABC only recognizes__iter__. docval resolves an actual-type argtype viaisinstance(value, argtype), so the value is rejected.len(zarr_array)raises because zarr v3 removed__len__fromArray(zarr.Array.__len__removed in v3 rewrite zarr-developers/zarr-python#3740).zarr_array.shape[0](orget_data_shape(...)[0]) is the array-API-safe replacement.Proposed fix
1. Retype array-valued fields
Iterable->('array_data', 'data')The
array_datadocval macro already includesnp.ndarray,list,tuple,h5py.Dataset,pd.Series,ExtensionArray,zarr.Array, and hdmf datasets;dataaddsDataIO. These fields are all genuinely array-valued and are read back as datasets:control,control_descriptionTimeSeries(base class)base.py:162,164dimension,starting_frameImageSeriesimage.py:100,89features,feature_unitsAbstractFeatureSeriesmisc.py:77,75waveform_mean,waveform_sd,peak_over_rmsSpikeEventSeries/FeatureExtractionecephys.py:340,342,303Behavior change to note in the changelog:
('array_data', 'data')acceptszarr.Array(the fix) but no longer accepts non-array iterables thatIterableallowed (str,bytes,set,dict,range, generators). For these array-valued fields that is a desirable tightening (a barestrinto ashape=(None,)slot was a latent bug); the only realistically-passable inputs affected arerange(...)and generator expressions.Do not change
NWBFile.electrode_groups(file.py:413). It is an iterable ofElectrodeGroupobjects, never backed by a dataset, so it has no zarr issue; retyping it toarray_datawould give no benefit and would needlessly reject aset/generator of groups. Leave it asIterable(or give it a purpose-fit object-collection type).2. Replace the direct
len()callIn
ElectricalSeries.__init__, useget_data_shape(...)[0]/.shape[0]instead oflen(electrodes.data), matching the zarr-safe pattern already used atecephys.py:148. Audit other constructors for directlen()/positional indexing on possibly-lazy datasets.Suggested tests
Add coverage constructing/round-tripping with zarr-backed values for at least
TimeSeries.control,ImageSeries.dimension, andElectricalSeries(electrodes region), so this does not silently regress.Alternative / upstream
zarr-python restoring
__len__/__iter__onArraywould fix both problems without any pynwb change: zarr-developers/zarr-python#3740 (__len__), #3741 (scalar__getitem__returning a 0-d ndarray).