Skip to content

fix float32 precision loss in aggregate variance: cast to float64 before squaring#4122

Closed
zboldyga wants to merge 2 commits into
scverse:mainfrom
zboldyga:aggregate-welford
Closed

fix float32 precision loss in aggregate variance: cast to float64 before squaring#4122
zboldyga wants to merge 2 commits into
scverse:mainfrom
zboldyga:aggregate-welford

Conversation

@zboldyga
Copy link
Copy Markdown
Contributor

@zboldyga zboldyga commented May 14, 2026

  • Closes #
  • Tests included or not required because:

While working on the wilcoxon illico integration, I ran into numerical stability issues with the 'aggregate' function.

Upon further inspection, the issues primarily stem from float32 squaring before the mean_of_sq - sq_of_mean variance calculation. It seems there was some code to cast to float64 e.g. in the csc and csr cases, but then the next line cast it right back to float32 value = data.data[j]. And other cases like the Dask and dense paths were not using float64.

I'll add a writeup from AI in another message below about the error levels / empirical findings.

...

So this small fix solves a large part of the numerical stability problem. This is a sufficient-enough fix to enable the stats computation code in the wilcoxon illico work to be completed using 'aggregate', which speeds it up an OOM.

That said, 'aggregate' has further issues -- the variance calculation used here is known to have a 'catastrophic cancellation' scenario when the mean/var ratio is high, e.g. 10,000+. I will open another PR for this soon, but it's a much bigger change, probably requiring more testing and review.

Note: This uses float64 so it's a bit slower. I tested a few perturb-seq datasets and it was about 30% slower on the Dask path. But only about 5% slower on the CSR path, and ~10% on CSC. However, I am fairly sure once we add the welford's online approach (or similar) this will speed up.

@ilan-gold tagging you here since this resolves the issue in wilcoxon-illico work / unblocks completion of my PR on stats computation speedup

zboldyga and others added 2 commits May 13, 2026 16:21
…ore squaring

In all three variance paths of `sc.get.aggregate` — the sparse CSR/CSC
kernels, the dense branch of `Aggregate.mean_var`, and the dask
`aggregate_dask_mean_var` — per-element squaring was happening at the
input dtype. For float32 input (scanpy's default for log1p-normalized X),
each squared value carried only ~7 digits of precision before being
promoted into the float64 accumulator. The downstream `mean_sq - sq_mean`
cancellation then amplifies that accumulated absolute error by the
`mean²/var` ratio.

This commit makes intermediate squaring happen at float64 across all
three paths:
- sparse kernels: remove the dead `value = data.data[j]` line that
  was overwriting the existing `np.float64(...)` cast (so the cast
  on the prior line actually applies);
- dense path: cast `self.data` to float64 before `_power(..., 2)`;
- dask path: cast `data` to float64 before `fau_power(..., 2)`
  (lazy, so no eager copy).

Reduces synthetic-adversarial variance error from ~3.5e-4 to ~1.6e-10
on the sparse kernel (6 OOM). On real perturb-seq pseudobulk-of-
pseudobulks workloads, error drops 30-60× from ~2e-5 to ~5e-7.

Perf impact: sparse paths are essentially free (within ±5% noise).
Dense paths take ~10-26% longer per call; dask paths ~27-46% longer.
The full table is at de-optimization/welford_explore/results/aggregate_fix_perf.md.
@zboldyga
Copy link
Copy Markdown
Contributor Author

Lengthy AI writeup that attempts to explain the underlying stability issue with the float32 squaring in this data regime 😅

Why float32 squaring is problematic for scanpy data

The math

Variance is computed as var = Σx²/n − μ². This is a subtraction
of two nearly-equal large numbers
when variance is small relative to
the mean — and that's where precision dies.

Specifically: each input to that subtraction (Σx²/n and μ²) is
correct to roughly the relative precision of the floating-point
format used to compute it. The subtraction itself doesn't lose
precision — but its result, which is much smaller than either
operand, inherits the inputs' absolute error and so its relative
error is amplified by:

amplification factor  =  mean² / var

Concrete numbers

storage dtype relative precision digits in Σx²/n and μ²
float32 ε ≈ 1.2e-7 ~7 digits
float64 ε ≈ 2.2e-16 ~16 digits

For a ribosomal gene with mean²/var ≈ 5,000:

  • log₁₀(5,000) ≈ 3.7 → the subtraction loses ~3.7 digits.
  • float32 inputs: 7 digits − 3.7 ≈ 3.3 digits left in var.
    Relative error ≈ 5,000 × 1.2e-7 ≈ 6e-4.
  • float64 inputs: 16 digits − 3.7 ≈ 12.3 digits left.
    Relative error ≈ 5,000 × 2.2e-16 ≈ 1e-12.

That's the 9-orders-of-magnitude difference between the two regimes.

The fix is one cast — value.astype(np.float64) before squaring —
so the inputs to the cancellation step are at float64's 16-digit
precision instead of float32's 7-digit precision. The cancellation
amplifier still exists; it just has much less to amplify.

Why scanpy's typical data lives in this regime

scanpy stores log1p-normalized counts as float32 by default,
with values mostly in [0, ~10]. In that range, common gene
categories routinely have small per-cell-type variance relative to
mean — pushing mean²/var high:

gene category log1p mean per-cell-type var mean²/var digits left in var (float32)
Random / noisy genes ~1 ~1 ~1 ~7
Housekeeping (ACTB, EEF1A1) ~4 ~0.05 ~300 ~4.5
Mitochondrial (MT-CO*) ~5 ~0.04 ~600 ~4.2
Ribosomal (RPL/RPS) ~5 ~0.005 ~5,000 ~3.3
Abundant lncRNA (MALAT1) ~6 ~0.002 ~18,000 ~2.7

Pseudobulk-of-pseudobulks workflows (the documented use case for
sc.get.aggregate) compress within-group variance further, pushing
typical workloads into the 10³–10⁵ ratio range. At those ratios,
float32 inputs leave 2–4 digits of usable precision in var; the
fix restores ~11–13 digits.

What this looks like empirically

On the wessels23 RPS15 pseudobulk scenario (real data,
mean²/var ≈ 4,000):

predicted measured
float32 inputs (before fix) ~5e-4 2.5e-5
float64 inputs (after fix) ~1e-12 5e-7

Predictions overshoot observed values by 10–100× because real
log1p-normalized data has correlated (integer-quantized) rounding
errors rather than random-walk. The order-of-magnitude shape — that
the cost of float32 inputs scales with mean²/var — matches the
data exactly.

The 5e-7 residual after the fix isn't kernel error anymore — it's
the float32-precision floor of the input values themselves, which
is the best any algorithm can do given float32-stored data.

@zboldyga
Copy link
Copy Markdown
Contributor Author

zboldyga commented May 14, 2026

And some evidence of the resolution via float64 usage (AI writeup again here, lengthy data).

The gist is that this was impacting the results enough that it would have caused a regression if aggregated was used in wilcoxon-illico stats computation. And I'd say it is likely the float32 squaring would cause quite tangible issues when dealing with other use cases like pseudobulk-related things, manipulated data that is not standard log1p counts, or maybe newer datasets with higher library sizes.

Reference Note

"Correct" is defined as two-pass shifted-mean variance computed at
float64. That reference was cross-checked against arbitrary-precision
decimal.Decimal at 50 digits: agreement to 9.5e-16 (machine
precision). All error numbers below are relative errors against
that reference.

Numerical impact

Synthetic adversarial scenarios

Each row is a single sc.get.aggregate call on synthetic float32
input with the noted mean / variance / N. Numbers shown are the
max relative error across the (group, gene) output grid vs the
reference. Lower is better; 1e-16 is the machine-precision floor.

scenario mean var N current main this PR
housekeeping (5 groups) 6.5 0.01 5,000 4.6e-6 6.2e-11
ribosomal (184 groups) 8.5 0.002 200 3.5e-4 1.6e-10
clonal population (5 groups) 7 5e-4 100 8.4e-4 2.6e-10

Reading: on the ribosomal scenario, the kernel returned variances that
were off by ~0.035% relatively before the fix — meaningful given
downstream consumers (e.g., t-test scores) propagate variance error
through 1/sqrt(var). After the fix, errors are below 1e-9.

Real pseudobulk workloads

The same measurement on real perturb-seq pseudobulk-of-pseudobulks
data (the documented use case for sc.get.aggregate). "Worst gene"
means the (gene, kind) cell with the largest mean²/var ratio, where
the cancellation issue bites hardest. Sparse-CSR backend.

dataset worst gene mean²/var current main this PR
adamson16 RPL8 11,635 1.7e-5 2.8e-7
frangieh21 EEF1A1 12,964 2.3e-5 4.4e-7
tian21crispri MALAT1 18,728 2.1e-5 7.0e-7
wessels23 RPS15 4,237 2.5e-5 7.0e-7

Reading: routine biological workflows (housekeeping/ribosomal/abundant
genes) reliably trigger the bug. Errors are ~10⁻⁵ before the fix —
not catastrophic in absolute terms, but enough to propagate into
downstream ULP-level rank flips. Fix reduces them by ~30–80×.

Cross-backend confirmation

The bug is in the squaring step, which appears identically across all
four aggregate backends. Verified by running the same wessels23
pseudobulk scenario through each path, with float32 input cast to the
backend's native form (sparse.csr_matrix, sparse.csc_matrix,
np.ndarray, dask.array):

backend current main this PR improvement
csr 2.48e-5 5.16e-7 48×
csc 2.48e-5 5.16e-7 48×
dense 2.48e-5 5.16e-7 48×
dask 2.48e-5 5.16e-7 48×

Reading: identical errors across all four backends — same root cause,
same fix.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
1852 5 1847 925
View the top 3 failed test(s) by shortest run time
tests/test_readwrite.py::test_write_strings_to_cats[no_s2c-zarr]
Stack Traces | 0.009s run time
fmt = 'zarr'

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.skipif(#x1B[90m#x1B[39;49;00m
        pkg_version(#x1B[33m"#x1B[39;49;00m#x1B[33manndata#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m) < Version(#x1B[33m"#x1B[39;49;00m#x1B[33m0.11.0rc2#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
        reason=#x1B[33m"#x1B[39;49;00m#x1B[33mOlder AnnData has no convert_strings_to_categoricals#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33mfmt#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, pytest.param(#x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, marks=needs.zarr)])#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33ms2c#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[94mTrue#x1B[39;49;00m, #x1B[94mFalse#x1B[39;49;00m], ids=[#x1B[33m"#x1B[39;49;00m#x1B[33ms2c#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mno_s2c#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m])#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_write_strings_to_cats#x1B[39;49;00m(fmt: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m], *, s2c: #x1B[96mbool#x1B[39;49;00m) -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        adata = AnnData(np.array([[#x1B[94m1#x1B[39;49;00m, #x1B[94m2#x1B[39;49;00m], [#x1B[94m3#x1B[39;49;00m, #x1B[94m4#x1B[39;49;00m], [#x1B[94m5#x1B[39;49;00m, #x1B[94m6#x1B[39;49;00m]]), obs=#x1B[96mdict#x1B[39;49;00m(a=[#x1B[33m"#x1B[39;49;00m#x1B[33ma#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mb#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33ma#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m]))#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
>       sc.write(#x1B[33m"#x1B[39;49;00m#x1B[33mtest#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, convert_strings_to_categoricals=s2c, ext=fmt)#x1B[90m#x1B[39;49;00m

#x1B[1m#x1B[31mtests/test_readwrite.py#x1B[0m:110: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
#x1B[1m#x1B[31msrc/scanpy/readwrite.py#x1B[0m:746: in write
    #x1B[0mwrite_zarr(filename, adata, **extra_kw)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:349: in raise_error_if_dataset_2d_present
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m write(store, adata, *args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:59: in write_zarr
    #x1B[0mwrite_dispatched(f, #x1B[33m"#x1B[39;49;00m#x1B[33m/#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, callback=callback, dataset_kwargs=ds_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/experimental/_dispatch_io.py#x1B[0m:74: in write_dispatched
    #x1B[0mwriter.write_elem(store, key, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:359: in write_anndata
    #x1B[0m_writer.write_elem(g, #x1B[33m"#x1B[39;49;00m#x1B[33mX#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata.X, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:332: in func_wrapper
    #x1B[0mfunc(f, k, elem, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:519: in write_basic
    #x1B[0m#x1B[94mwith#x1B[39;49;00m zarr_v3_sharding(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../...../_temp/uv-python-dir/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/contextlib.py#x1B[0m:141: in __enter__
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mnext#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m.gen)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

dataset_kwargs = {}, format = 3

    #x1B[0m#x1B[37m@contextmanager#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mzarr_v3_sharding#x1B[39;49;00m(dataset_kwargs: #x1B[96mdict#x1B[39;49;00m, #x1B[96mformat#x1B[39;49;00m: Literal[#x1B[94m2#x1B[39;49;00m, #x1B[94m3#x1B[39;49;00m]) -> Generator[#x1B[96mdict#x1B[39;49;00m]:#x1B[90m#x1B[39;49;00m
        auto_sharding = (#x1B[90m#x1B[39;49;00m
            #x1B[33m"#x1B[39;49;00m#x1B[33mshards#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[95min#x1B[39;49;00m dataset_kwargs#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m ad.settings.auto_shard_zarr_v3#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m ad.settings.auto_shard_zarr_v3 #x1B[95mis#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           warnings.warn(#x1B[90m#x1B[39;49;00m
                #x1B[33m"#x1B[39;49;00m#x1B[33mzarr v3 autosharding will be the default in the next minor release.#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                #x1B[96mUserWarning#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                stacklevel=#x1B[94m2#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               UserWarning: zarr v3 autosharding will be the default in the next minor release.#x1B[0m
#x1B[1m#x1B[31mE               Error raised while writing key 'X' of <class 'zarr.core.group.Group'> to /#x1B[0m

#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:129: UserWarning
tests/test_readwrite.py::test_write_strings_to_cats[s2c-zarr]
Stack Traces | 0.01s run time
fmt = 'zarr'

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.skipif(#x1B[90m#x1B[39;49;00m
        pkg_version(#x1B[33m"#x1B[39;49;00m#x1B[33manndata#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m) < Version(#x1B[33m"#x1B[39;49;00m#x1B[33m0.11.0rc2#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
        reason=#x1B[33m"#x1B[39;49;00m#x1B[33mOlder AnnData has no convert_strings_to_categoricals#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33mfmt#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, pytest.param(#x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, marks=needs.zarr)])#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33ms2c#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[94mTrue#x1B[39;49;00m, #x1B[94mFalse#x1B[39;49;00m], ids=[#x1B[33m"#x1B[39;49;00m#x1B[33ms2c#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mno_s2c#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m])#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_write_strings_to_cats#x1B[39;49;00m(fmt: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m], *, s2c: #x1B[96mbool#x1B[39;49;00m) -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        adata = AnnData(np.array([[#x1B[94m1#x1B[39;49;00m, #x1B[94m2#x1B[39;49;00m], [#x1B[94m3#x1B[39;49;00m, #x1B[94m4#x1B[39;49;00m], [#x1B[94m5#x1B[39;49;00m, #x1B[94m6#x1B[39;49;00m]]), obs=#x1B[96mdict#x1B[39;49;00m(a=[#x1B[33m"#x1B[39;49;00m#x1B[33ma#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mb#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33ma#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m]))#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
>       sc.write(#x1B[33m"#x1B[39;49;00m#x1B[33mtest#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, convert_strings_to_categoricals=s2c, ext=fmt)#x1B[90m#x1B[39;49;00m

#x1B[1m#x1B[31mtests/test_readwrite.py#x1B[0m:110: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
#x1B[1m#x1B[31msrc/scanpy/readwrite.py#x1B[0m:746: in write
    #x1B[0mwrite_zarr(filename, adata, **extra_kw)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:349: in raise_error_if_dataset_2d_present
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m write(store, adata, *args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:59: in write_zarr
    #x1B[0mwrite_dispatched(f, #x1B[33m"#x1B[39;49;00m#x1B[33m/#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, callback=callback, dataset_kwargs=ds_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/experimental/_dispatch_io.py#x1B[0m:74: in write_dispatched
    #x1B[0mwriter.write_elem(store, key, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:359: in write_anndata
    #x1B[0m_writer.write_elem(g, #x1B[33m"#x1B[39;49;00m#x1B[33mX#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata.X, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:332: in func_wrapper
    #x1B[0mfunc(f, k, elem, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:519: in write_basic
    #x1B[0m#x1B[94mwith#x1B[39;49;00m zarr_v3_sharding(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31m../...../_temp/uv-python-dir/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/contextlib.py#x1B[0m:141: in __enter__
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mnext#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m.gen)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

dataset_kwargs = {}, format = 3

    #x1B[0m#x1B[37m@contextmanager#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mzarr_v3_sharding#x1B[39;49;00m(dataset_kwargs: #x1B[96mdict#x1B[39;49;00m, #x1B[96mformat#x1B[39;49;00m: Literal[#x1B[94m2#x1B[39;49;00m, #x1B[94m3#x1B[39;49;00m]) -> Generator[#x1B[96mdict#x1B[39;49;00m]:#x1B[90m#x1B[39;49;00m
        auto_sharding = (#x1B[90m#x1B[39;49;00m
            #x1B[33m"#x1B[39;49;00m#x1B[33mshards#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[95min#x1B[39;49;00m dataset_kwargs#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m ad.settings.auto_shard_zarr_v3#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m ad.settings.auto_shard_zarr_v3 #x1B[95mis#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           warnings.warn(#x1B[90m#x1B[39;49;00m
                #x1B[33m"#x1B[39;49;00m#x1B[33mzarr v3 autosharding will be the default in the next minor release.#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                #x1B[96mUserWarning#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                stacklevel=#x1B[94m2#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               UserWarning: zarr v3 autosharding will be the default in the next minor release.#x1B[0m
#x1B[1m#x1B[31mE               Error raised while writing key 'X' of <class 'zarr.core.group.Group'> to /#x1B[0m

#x1B[1m#x1B[31m../../../..../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:129: UserWarning
tests/test_readwrite.py::test_write[ext-zarr]
Stack Traces | 0.011s run time
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f936a5f7bd0>
tmp_path = PosixPath('.../pytest-0/popen-gw0/test_write_ext_zarr_0')
ext = 'zarr', style = 'ext'

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, pytest.param(#x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, marks=needs.zarr), #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m])#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33mstyle#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mdefault#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m])#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_write#x1B[39;49;00m(#x1B[90m#x1B[39;49;00m
        monkeypatch: pytest.MonkeyPatch,#x1B[90m#x1B[39;49;00m
        tmp_path: Path,#x1B[90m#x1B[39;49;00m
        ext: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
        style: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mdefault#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
    ) -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        monkeypatch.chdir(tmp_path)#x1B[90m#x1B[39;49;00m
        adata = AnnData(np.array([[#x1B[94m1#x1B[39;49;00m, #x1B[94m2#x1B[39;49;00m], [#x1B[94m3#x1B[39;49;00m, #x1B[94m4#x1B[39;49;00m]]))#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# test that writing works (except style="default" and ext="csv")#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        ctx = (#x1B[90m#x1B[39;49;00m
            pytest.warns(#x1B[96mFutureWarning#x1B[39;49;00m, match=#x1B[33mr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mremoved from this function#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m ext == #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m nullcontext()#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        #x1B[94mmatch#x1B[39;49;00m style, ext:#x1B[90m#x1B[39;49;00m
            #x1B[94mcase#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94m_#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mwith#x1B[39;49;00m ctx:#x1B[90m#x1B[39;49;00m
                    sc.write(#x1B[33mf#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mtest.#x1B[39;49;00m#x1B[33m{#x1B[39;49;00mext#x1B[33m}#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata)#x1B[90m#x1B[39;49;00m
                d = tmp_path#x1B[90m#x1B[39;49;00m
            #x1B[94mcase#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94m_#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mwith#x1B[39;49;00m ctx:#x1B[90m#x1B[39;49;00m
>                   sc.write(#x1B[33m"#x1B[39;49;00m#x1B[33mtest#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, ext=ext)#x1B[90m#x1B[39;49;00m

#x1B[1m#x1B[.../scanpy/tests/test_readwrite.py#x1B[0m:71: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
#x1B[1m#x1B[.../src/scanpy/readwrite.py#x1B[0m:746: in write
    #x1B[0mwrite_zarr(filename, adata, **extra_kw)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:349: in raise_error_if_dataset_2d_present
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m write(store, adata, *args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:59: in write_zarr
    #x1B[0mwrite_dispatched(f, #x1B[33m"#x1B[39;49;00m#x1B[33m/#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, callback=callback, dataset_kwargs=ds_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/experimental/_dispatch_io.py#x1B[0m:74: in write_dispatched
    #x1B[0mwriter.write_elem(store, key, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:359: in write_anndata
    #x1B[0m_writer.write_elem(g, #x1B[33m"#x1B[39;49;00m#x1B[33mX#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata.X, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:332: in func_wrapper
    #x1B[0mfunc(f, k, elem, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:519: in write_basic
    #x1B[0m#x1B[94mwith#x1B[39;49;00m zarr_v3_sharding(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../_temp/uv-python-dir/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/contextlib.py#x1B[0m:141: in __enter__
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mnext#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m.gen)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

dataset_kwargs = {}, format = 3

    #x1B[0m#x1B[37m@contextmanager#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mzarr_v3_sharding#x1B[39;49;00m(dataset_kwargs: #x1B[96mdict#x1B[39;49;00m, #x1B[96mformat#x1B[39;49;00m: Literal[#x1B[94m2#x1B[39;49;00m, #x1B[94m3#x1B[39;49;00m]) -> Generator[#x1B[96mdict#x1B[39;49;00m]:#x1B[90m#x1B[39;49;00m
        auto_sharding = (#x1B[90m#x1B[39;49;00m
            #x1B[33m"#x1B[39;49;00m#x1B[33mshards#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[95min#x1B[39;49;00m dataset_kwargs#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m ad.settings.auto_shard_zarr_v3#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m ad.settings.auto_shard_zarr_v3 #x1B[95mis#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           warnings.warn(#x1B[90m#x1B[39;49;00m
                #x1B[33m"#x1B[39;49;00m#x1B[33mzarr v3 autosharding will be the default in the next minor release.#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                #x1B[96mUserWarning#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                stacklevel=#x1B[94m2#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               UserWarning: zarr v3 autosharding will be the default in the next minor release.#x1B[0m
#x1B[1m#x1B[31mE               Error raised while writing key 'X' of <class 'zarr.core.group.Group'> to /#x1B[0m

#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:129: UserWarning
tests/test_readwrite.py::test_write[path-zarr]
Stack Traces | 0.011s run time
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f936a90dda0>
tmp_path = PosixPath('.../pytest-0/popen-gw0/test_write_path_zarr_0')
ext = 'zarr', style = 'path'

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, pytest.param(#x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, marks=needs.zarr), #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m])#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33mstyle#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mdefault#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m])#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_write#x1B[39;49;00m(#x1B[90m#x1B[39;49;00m
        monkeypatch: pytest.MonkeyPatch,#x1B[90m#x1B[39;49;00m
        tmp_path: Path,#x1B[90m#x1B[39;49;00m
        ext: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
        style: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mdefault#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
    ) -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        monkeypatch.chdir(tmp_path)#x1B[90m#x1B[39;49;00m
        adata = AnnData(np.array([[#x1B[94m1#x1B[39;49;00m, #x1B[94m2#x1B[39;49;00m], [#x1B[94m3#x1B[39;49;00m, #x1B[94m4#x1B[39;49;00m]]))#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# test that writing works (except style="default" and ext="csv")#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        ctx = (#x1B[90m#x1B[39;49;00m
            pytest.warns(#x1B[96mFutureWarning#x1B[39;49;00m, match=#x1B[33mr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mremoved from this function#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m ext == #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m nullcontext()#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        #x1B[94mmatch#x1B[39;49;00m style, ext:#x1B[90m#x1B[39;49;00m
            #x1B[94mcase#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94m_#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mwith#x1B[39;49;00m ctx:#x1B[90m#x1B[39;49;00m
>                   sc.write(#x1B[33mf#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mtest.#x1B[39;49;00m#x1B[33m{#x1B[39;49;00mext#x1B[33m}#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata)#x1B[90m#x1B[39;49;00m

#x1B[1m#x1B[.../scanpy/tests/test_readwrite.py#x1B[0m:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
#x1B[1m#x1B[.../src/scanpy/readwrite.py#x1B[0m:746: in write
    #x1B[0mwrite_zarr(filename, adata, **extra_kw)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:349: in raise_error_if_dataset_2d_present
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m write(store, adata, *args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:59: in write_zarr
    #x1B[0mwrite_dispatched(f, #x1B[33m"#x1B[39;49;00m#x1B[33m/#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, callback=callback, dataset_kwargs=ds_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/experimental/_dispatch_io.py#x1B[0m:74: in write_dispatched
    #x1B[0mwriter.write_elem(store, key, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:359: in write_anndata
    #x1B[0m_writer.write_elem(g, #x1B[33m"#x1B[39;49;00m#x1B[33mX#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata.X, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:332: in func_wrapper
    #x1B[0mfunc(f, k, elem, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:519: in write_basic
    #x1B[0m#x1B[94mwith#x1B[39;49;00m zarr_v3_sharding(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../_temp/uv-python-dir/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/contextlib.py#x1B[0m:141: in __enter__
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mnext#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m.gen)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

dataset_kwargs = {}, format = 3

    #x1B[0m#x1B[37m@contextmanager#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mzarr_v3_sharding#x1B[39;49;00m(dataset_kwargs: #x1B[96mdict#x1B[39;49;00m, #x1B[96mformat#x1B[39;49;00m: Literal[#x1B[94m2#x1B[39;49;00m, #x1B[94m3#x1B[39;49;00m]) -> Generator[#x1B[96mdict#x1B[39;49;00m]:#x1B[90m#x1B[39;49;00m
        auto_sharding = (#x1B[90m#x1B[39;49;00m
            #x1B[33m"#x1B[39;49;00m#x1B[33mshards#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[95min#x1B[39;49;00m dataset_kwargs#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m ad.settings.auto_shard_zarr_v3#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m ad.settings.auto_shard_zarr_v3 #x1B[95mis#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           warnings.warn(#x1B[90m#x1B[39;49;00m
                #x1B[33m"#x1B[39;49;00m#x1B[33mzarr v3 autosharding will be the default in the next minor release.#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                #x1B[96mUserWarning#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                stacklevel=#x1B[94m2#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               UserWarning: zarr v3 autosharding will be the default in the next minor release.#x1B[0m
#x1B[1m#x1B[31mE               Error raised while writing key 'X' of <class 'zarr.core.group.Group'> to /#x1B[0m

#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:129: UserWarning
tests/test_readwrite.py::test_write[default-zarr]
Stack Traces | 0.021s run time
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f938c578de0>
tmp_path = PosixPath('.../pytest-0/popen-gw0/test_write_default_zarr_0')
ext = 'zarr', style = 'default'

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, pytest.param(#x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, marks=needs.zarr), #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m])#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[33m"#x1B[39;49;00m#x1B[33mstyle#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, [#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mdefault#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m])#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_write#x1B[39;49;00m(#x1B[90m#x1B[39;49;00m
        monkeypatch: pytest.MonkeyPatch,#x1B[90m#x1B[39;49;00m
        tmp_path: Path,#x1B[90m#x1B[39;49;00m
        ext: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
        style: Literal[#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mdefault#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m],#x1B[90m#x1B[39;49;00m
    ) -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        monkeypatch.chdir(tmp_path)#x1B[90m#x1B[39;49;00m
        adata = AnnData(np.array([[#x1B[94m1#x1B[39;49;00m, #x1B[94m2#x1B[39;49;00m], [#x1B[94m3#x1B[39;49;00m, #x1B[94m4#x1B[39;49;00m]]))#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# test that writing works (except style="default" and ext="csv")#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        ctx = (#x1B[90m#x1B[39;49;00m
            pytest.warns(#x1B[96mFutureWarning#x1B[39;49;00m, match=#x1B[33mr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mremoved from this function#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m ext == #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m nullcontext()#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        #x1B[94mmatch#x1B[39;49;00m style, ext:#x1B[90m#x1B[39;49;00m
            #x1B[94mcase#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mpath#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94m_#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mwith#x1B[39;49;00m ctx:#x1B[90m#x1B[39;49;00m
                    sc.write(#x1B[33mf#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mtest.#x1B[39;49;00m#x1B[33m{#x1B[39;49;00mext#x1B[33m}#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata)#x1B[90m#x1B[39;49;00m
                d = tmp_path#x1B[90m#x1B[39;49;00m
            #x1B[94mcase#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mext#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94m_#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mwith#x1B[39;49;00m ctx:#x1B[90m#x1B[39;49;00m
                    sc.write(#x1B[33m"#x1B[39;49;00m#x1B[33mtest#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, ext=ext)#x1B[90m#x1B[39;49;00m
                d = sc.settings.writedir#x1B[90m#x1B[39;49;00m
            #x1B[94mcase#x1B[39;49;00m #x1B[33m"#x1B[39;49;00m#x1B[33mdefault#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[90m# check that it throws an error instead#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                ff = sc.settings.file_format_data#x1B[90m#x1B[39;49;00m
                #x1B[94mwith#x1B[39;49;00m pytest.raises(#x1B[96mValueError#x1B[39;49;00m, match=#x1B[33mr#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mshould be #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mh5ad#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m or #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mzarr#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m.*#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mcsv#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
                    sc.settings.file_format_data = ext  #x1B[90m# type: ignore[assignment]#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[94massert#x1B[39;49;00m sc.settings.file_format_data == ff#x1B[90m#x1B[39;49;00m
                #x1B[94mreturn#x1B[39;49;00m  #x1B[90m# return early#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[94mcase#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33mdefault#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, #x1B[94m_#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                sc.settings.file_format_data, old = ext, sc.settings.file_format_data#x1B[90m#x1B[39;49;00m
                #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                    #x1B[94mwith#x1B[39;49;00m ctx:#x1B[90m#x1B[39;49;00m
>                       sc.write(#x1B[33m"#x1B[39;49;00m#x1B[33mtest#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata)#x1B[90m#x1B[39;49;00m

#x1B[1m#x1B[.../scanpy/tests/test_readwrite.py#x1B[0m:84: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
#x1B[1m#x1B[.../src/scanpy/readwrite.py#x1B[0m:746: in write
    #x1B[0mwrite_zarr(filename, adata, **extra_kw)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:349: in raise_error_if_dataset_2d_present
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m write(store, adata, *args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:59: in write_zarr
    #x1B[0mwrite_dispatched(f, #x1B[33m"#x1B[39;49;00m#x1B[33m/#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata, callback=callback, dataset_kwargs=ds_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/experimental/_dispatch_io.py#x1B[0m:74: in write_dispatched
    #x1B[0mwriter.write_elem(store, key, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:359: in write_anndata
    #x1B[0m_writer.write_elem(g, #x1B[33m"#x1B[39;49;00m#x1B[33mX#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m, adata.X, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:272: in func_wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(*args, **kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:391: in write_elem
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.callback(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/zarr.py#x1B[0m:57: in callback
    #x1B[0mwrite_func(store, elem_name, elem, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/registry.py#x1B[0m:78: in wrapper
    #x1B[0mresult = func(g, k, *args, **kwargs)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:200: in wrapper
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m func(f, k, val, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../anndata/_io/utils.py#x1B[0m:332: in func_wrapper
    #x1B[0mfunc(f, k, elem, _writer=_writer, dataset_kwargs=dataset_kwargs)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:519: in write_basic
    #x1B[0m#x1B[94mwith#x1B[39;49;00m zarr_v3_sharding(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[.../_temp/uv-python-dir/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/contextlib.py#x1B[0m:141: in __enter__
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mnext#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m.gen)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

dataset_kwargs = {}, format = 3

    #x1B[0m#x1B[37m@contextmanager#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mzarr_v3_sharding#x1B[39;49;00m(dataset_kwargs: #x1B[96mdict#x1B[39;49;00m, #x1B[96mformat#x1B[39;49;00m: Literal[#x1B[94m2#x1B[39;49;00m, #x1B[94m3#x1B[39;49;00m]) -> Generator[#x1B[96mdict#x1B[39;49;00m]:#x1B[90m#x1B[39;49;00m
        auto_sharding = (#x1B[90m#x1B[39;49;00m
            #x1B[33m"#x1B[39;49;00m#x1B[33mshards#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[95min#x1B[39;49;00m dataset_kwargs#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m ad.settings.auto_shard_zarr_v3#x1B[90m#x1B[39;49;00m
            #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m ad.settings.auto_shard_zarr_v3 #x1B[95mis#x1B[39;49;00m #x1B[94mNone#x1B[39;49;00m #x1B[95mand#x1B[39;49;00m #x1B[96mformat#x1B[39;49;00m == #x1B[94m3#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           warnings.warn(#x1B[90m#x1B[39;49;00m
                #x1B[33m"#x1B[39;49;00m#x1B[33mzarr v3 autosharding will be the default in the next minor release.#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                #x1B[96mUserWarning#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                stacklevel=#x1B[94m2#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE               UserWarning: zarr v3 autosharding will be the default in the next minor release.#x1B[0m
#x1B[1m#x1B[31mE               Error raised while writing key 'X' of <class 'zarr.core.group.Group'> to /#x1B[0m

#x1B[1m#x1B[.../home/runner/.local.../scanpy/B9PcT7QG/hatch-test.few-extras/lib/python3.14.../_io/specs/methods.py#x1B[0m:129: UserWarning

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@ilan-gold
Copy link
Copy Markdown
Contributor

ilan-gold commented May 15, 2026

That said, 'aggregate' has further issues -- the variance calculation used here is known to have a 'catastrophic cancellation' scenario when the mean/var ratio is high, e.g. 10,000+. I will open another PR for this soon, but it's a much bigger change, probably requiring more testing and review.

I started one for dask, will post now. I think we should just replace the algorithm IMO

Update: #4123

@zboldyga
Copy link
Copy Markdown
Contributor Author

sweet ok I'll close this and am happy to review or contribute on that one as needed

@zboldyga zboldyga closed this May 15, 2026
@ilan-gold
Copy link
Copy Markdown
Contributor

sweet ok I'll close this and am happy to review or contribute on that one as needed

Your review would be extremely welcome!!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants