Skip to content

refactor: convert experimental parameters to TypedDicts (1/4) - #1279

Open
selmanozleyen wants to merge 37 commits into
chore/kwonly-argsfrom
feat/experimental-params-typeddicts
Open

selmanozleyen wants to merge 37 commits into
chore/kwonly-argsfrom
feat/experimental-params-typeddicts

Conversation

@selmanozleyen

@selmanozleyen selmanozleyen commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Hi,

I want to bring a clear separation to the kwargs madness we have here.

  • Params: For kw only parameter bags we expect from the user: we use this PRs suggestion.
  • Fits: For configurations obtained by other functions: like StainReference or AlignmentResult we use frozen data classes . Even if they are given as input later to other functions since we don't expect the user to type those it doesn't make sense to unpack. Based on this instinct I also have a follow up PR to stack on refactor: add StainReference transform methods (2/4) #1280
  • Results: If they are simple returns only because we don't want to modify the input we can return a NamedTuple (like current SpatialNeighboursResults)

(about the nesting of functions by groups I am open to discussion, but it's really unreadable if we have a flat tl.*. I would also argue that we want to deprecate .im anyway. So we might as well put everything inside experimental.im.* under tl to make it less confusing if we will do this grouping in the docs.

Intended end result after one more pr in this stack:

Fits

Screenshot 2026-08-31 at 12 54 36 AM

Functions

Screenshot 2026-08-31 at 12 54 24 AM

Results

Screenshot 2026-08-31 at 12 53 57 AM

Params

Screenshot 2026-08-31 at 12 53 42 AM image

@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from cd1fa5b to 36451d4 Compare August 28, 2026 07:52
@selmanozleyen
selmanozleyen requested a review from timtreis August 28, 2026 08:03
@codecov

codecov Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.64865% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.90%. Comparing base (8286274) to head (a1df8a0).

Files with missing lines Patch % Lines
src/squidpy/experimental/im/_detect_tissue.py 93.02% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1279      +/-   ##
==========================================
+ Coverage   78.44%   78.90%   +0.46%     
==========================================
  Files          63       64       +1     
  Lines        9532     9496      -36     
  Branches     1594     1566      -28     
==========================================
+ Hits         7477     7493      +16     
+ Misses       1489     1459      -30     
+ Partials      566      544      -22     
Files with missing lines Coverage Δ
src/squidpy/experimental/im/_stain/_constants.py 100.00% <100.00%> (ø)
...c/squidpy/experimental/im/_stain/_decomposition.py 97.84% <100.00%> (+5.91%) ⬆️
src/squidpy/experimental/im/_stain/_normalize.py 93.75% <ø> (ø)
src/squidpy/experimental/im/_stain/_reinhard.py 100.00% <100.00%> (ø)
src/squidpy/experimental/im/_tiling.py 90.10% <100.00%> (+1.03%) ⬆️
src/squidpy/experimental/tl/_tiling_qc.py 68.84% <100.00%> (-1.73%) ⬇️
src/squidpy/experimental/tl/_tiling_stitch.py 77.72% <100.00%> (+2.42%) ⬆️
src/squidpy/experimental/utils/_params.py 100.00% <100.00%> (ø)
src/squidpy/types.py 100.00% <100.00%> (ø)
src/squidpy/experimental/im/_detect_tissue.py 70.31% <93.02%> (+2.53%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@selmanozleyen
selmanozleyen marked this pull request as ready for review August 28, 2026 08:18
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from d1001aa to affe9e0 Compare August 28, 2026 10:12
@selmanozleyen
selmanozleyen marked this pull request as draft August 28, 2026 12:50
selmanozleyen added a commit to selmanozleyen/squidpy that referenced this pull request Aug 30, 2026
Brings in scverse#1279 (params as TypedDicts with their defaults declared on the key), scverse#1280
(StainFit and its methods), the enum-to-Literal pass, and the API page restructuring, so
the align work sits on the shape those settle rather than carrying its own copy of it.

Conflicts resolved toward the stack for everything it owns: the params, the defaults
machinery, the enum conversion and the docs templates all come from there, and the three
copies this branch had made of them are dropped.

The alignment surface is what this branch adds on top -- the `stalign_align_*` entry
points, the fit classes they return, and `rasterize_points`/`sample_volume`.

`Stalign*Params` move into `squidpy.types` with the rest. Re-exporting them from
`_align._stalign` deadlocked at import: `types` would have imported the implementation
package whose `__init__` imports `types`. They are declared there now, like every other
params class, and `_stalign` reads them back.

`_matches` in the params test grew a tolerant union arm. It walks a key's declared type to
check the default against it, and `npt.ArrayLike` unions in protocols that are not
`runtime_checkable`, so `isinstance` raised before reaching the `None` arm that actually
matched.
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from 5ef501c to 5146abb Compare August 30, 2026 19:45
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch 2 times, most recently from 5146abb to 0586b10 Compare August 30, 2026 20:00
@flying-sheep

Copy link
Copy Markdown
Member

Nice, I like the plan and consistency.

What I don’t get is the distinction between spelling kwargs out in the function and having them defined in a TypedDict: are they always shared? Is there a rule about what goes where?

@selmanozleyen

selmanozleyen commented Aug 31, 2026 •

Copy link
Copy Markdown
Member Author

What I don’t get is the distinction between spelling kwargs out in the function and having them defined in a TypedDict: are they always shared? Is there a rule about what goes where?

  • For stalign there is a clear win with deduplication using the inheritence and overriding. We use Unpack here

  • For the cases like in detect_tissue, we have a bag or kwargs we sont want to bloat the signature with in the docs and the code. Honestly I haven't look into the criteria exactly. It's where @timtreis used frozen dataclasses to not bload the signature basically :D. I just converted them mechanically. We also use Unpack here.

  • for cases like in FelzenszwalbParams, we dont use Unpack. But keeping them as typed dict is better than either taking a frozen dataclass vs Mapping. Bc we used to also let the user give a mapping in case they didnt want to create the class themselves. Plus you can annotate mappings while essentially giving the Mapping flexibility to the users as well

@selmanozleyen

Copy link
Copy Markdown
Member Author

hi @timtreis, so these are what I noticed on the kwargs bags. I think it's useful to rethink them:

  • TilingQCParams is three keys against a 14-parameter signature, so no reason it should exist. If it should, it should encapsulate more of the params.
  • detect_tissue keeps auto_max_pixels, close_holes_smaller_than_frac and mask_smoothing_cycles in the signature while bagging corner_size_pct, those seem equally obscure to me

@selmanozleyen selmanozleyen changed the title refactor: convert experimental parameters to TypedDicts refactor: convert experimental parameters to TypedDicts (1/4) Sep 3, 2026
@selmanozleyen
selmanozleyen marked this pull request as ready for review September 8, 2026 11:34
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from 6dcb8f6 to c522628 Compare September 15, 2026 16:50
@flying-sheep

Copy link
Copy Markdown
Member

Very nice design, I think this 3-way split makes the most sense.

  • named tuples are great return types, especially when they have 2–4 elements and can therefore be either be used as-is or unpacked as conns, dists = spatial_neighbors(...)

  • custom classes with .transform is a great clean API for model fitting: stalign_align_obs(sdata_ref, ...).transform(sdata) makes it very obvious what’s happening.

  • using typed dicts for kwargs containers makes it very frictionless to use, as users can just rely on autocompletion, or use the imported *Params class to make a container that is then passed or unpacked later.

    Things like pydantic would add an early layer of validation here, but lose the “just use a dict literal” advantage. Ideally there would be a way to say “a pydantic instance or a dict with matching items”, but I don’t think Python’s type system is flexible enough for that, see PEP 692 follow up: Unpacking compatibility with dataclass/others python/typing#1495


I think there should be an indicator that the *Params classes are typed dicts. You could just mention it, or use sphinx_toolbox.more_autodoc, which looks great:

image

They invent custom object types, which isn’t ideal for interoperability (i.e. if you use it, it’ll be hard for users to link to them), but your call if that’s worth it.

@selmanozleyen

Copy link
Copy Markdown
Member Author

sphinx_toolbox.more_autodoc caused some compat issues because it upper bounded a version of an extension which we use the latest version of. I just added a one line mentioning they are a type dict.

@selmanozleyen
selmanozleyen marked this pull request as draft September 25, 2026 17:07
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch 2 times, most recently from ae5d9d7 to 694c55e Compare September 25, 2026 20:05
@selmanozleyen
selmanozleyen removed this pull request from stack #1281 September 25, 2026 20:17
@selmanozleyen
selmanozleyen changed the base branch from main to chore/kwonly-args September 25, 2026 20:18
Replaces the autodoc hook that injected the note at build time: plain
docstrings also show up in help() and IDEs, and cannot silently drop out.
StitchParams was six keys unpacked into assign_stitch_groups, its only
consumer: the same case as TilingQCParams, so the keys are plain keyword
arguments now and are recorded flat in .uns['tiling_stitch'].

The hint calculate_tiling_qc logs when a re-run drops the stitch columns
printed stitch_params={...}, which the **kwargs signature rejects. It now
prints the flat keys, and a test runs the printed call.
When a setting stays a keyword argument, when it becomes a *Params bag
(method_params= vs **Unpack), and what Fits and Results are.
Every *_DEFAULTS constant in squidpy.types only existed to be passed to
resolve_params. It now takes the TypedDict itself and reads its defaults
through a cached defaults_of, so the constants are gone. The import-time
'every key has a Default' check they gave is covered by test_params.
- corners_are_background accepts a numpy array of four bools (it raised),
  and refuses a string instead of reading "False" as True
- n_neighbors < 1 and a fractional min_area raise again, as on main:
  min_area is coerced before its check, not after
- the new tuning knobs of calculate_tiling_qc and assign_stitch_groups are
  keyword-only and last, so existing positional calls keep their slots
- the stitch re-run hint names qc_table_key when the QC table is not the
  default one
…nature

The private helpers repeated them as their own defaults, but
assign_stitch_groups always passes every value, so the helpers take them
as required arguments now and the signature is the one place they are
declared.
squidpy.types imported them from squidpy.experimental, the wrong way round
for a module that is not experimental. A @validates(Spec) decorator now
registers each validator with its *Params class, so resolve_params(p, Spec)
always runs it, and the per-method _resolve_*_params wrappers are gone.
The method_params docstrings no longer restate the types the annotation
already names.
DEFAULT_LUMINOSITY_THRESHOLD was used once, as that key's Default, and only
re-exported otherwise; its explanation moves into the key's docstring.
MacenkoParams and VahadaneParams take the same mean-absorbance cutoff, so it
is declared once, on _ODBetaParams, and the _OD_BETA constant goes.
A TypedDict does not keep its bases in its MRO, so autodoc cannot document
keys inherited from one: the shared base hid beta from both classes' docs.
Each class declares it with Default(0.15), and no constant is needed.
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from 06b7fa8 to 0c3243f Compare October 1, 2026 12:22

This branch has not been deployed

No deployments
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