Skip to content

fix: clear the mechanical type errors and shrink the allowlist to one open question - #1148

Open
anevolbap wants to merge 3 commits into
chore/mypy-ipw-axesfrom
chore/mypy-shrink-allowlist
Open

fix: clear the mechanical type errors and shrink the allowlist to one open question#1148
anevolbap wants to merge 3 commits into
chore/mypy-ipw-axesfrom
chore/mypy-shrink-allowlist

Conversation

@anevolbap

@anevolbap anevolbap commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #1146. Takes the allowlist from nine entries to three, and the branch from 30 errors in 13 files to 10 in 5.

Four clusters, not 20 separate bugs. In every case the annotation was narrower than the value that actually reaches it, and the runtime behaviour was already correct.

round_num declared n: float while four call sites pass a zero-dimensional DataArray from .mean() on posterior samples. xarray formats those like the scalar they wrap, so this was purely the annotation. Widened to float | xr.DataArray, matching its sibling convert_to_string, which already declared that union. Clears 4 errors in four modules.

Six constructors take data: DataFrameLike and rebind that same parameter to the pandas frame to_pandas returns. mypy keeps the declared parameter type, so .index, .copy() and data[column] were all reported against NativeDataFrame. The conversion helpers were already annotated -> pd.DataFrame; the information was lost at the rebinding. The converted frame now gets its own name, which is also what the existing comments in those constructors already claimed. Clears 9 errors.

Then five one-liners: np.abs on a DataArray replaced by the builtin abs, which xarray types correctly and which computes the same thing; xarray dimension names sorted as strings, since Hashable is not orderable and they only go into an error message; the ETI branch of _interval_bound_values given its own names so it stops colliding with the array-typed HDI branch; a dimension name cast to str for a coords key that PyMC requires to be a string anyway; and a list turned into the 4-tuple tight_layout(rect=...) documents.

One inline ignore, on iterating the "bodies" entry of a violinplot result. matplotlib types every entry as a single Collection rather than a list of them, so this is a stub limitation rather than our bug, and warn_unused_ignores will flag the ignore once matplotlib describes the return properly.

What is left, and why

The remaining 10 errors are one question, not a backlog. Several attributes and parameters are declared wider than the values that reach them, and narrowing them means deciding what the model lifecycle promises:

  • self.model is declared as the base model union on inverse_propensity_weighting and instrumental_variable, while both reach PropensityScore.fit and InstrumentalVariableRegression.fit, which take numpy arrays and extra keywords (6 errors). Both call sites already carry type: ignore[call-arg] for this, without arg-type. Narrowing the attribute to the subclass each experiment requires is the real fix, and it is a public type on the experiment classes.
  • self.idata starts as None and is a DataTree at every point that returns it (2 errors), and self.causal_impact is the same shape (1 error).
  • Axes.get_figure is Optional for a detached artist, which cannot happen at that call site (1 error).

I did not want to pick an answer to the first one inside a cleanup PR, and asserting non-None at the other three would be adding runtime checks for states that cannot occur just to satisfy the checker. Happy to do whichever you prefer.

Since opening this, #1149 takes a position on all four and gets the allowlist to empty, each decision marked in the code. It is a draft, and it stacks on this branch, so this PR still stands on its own if you would rather stop here.

Verification

  • mypy green under --python-version 3.12 and 3.14.
  • Full suite twice, before and after the final cluster: 2284 passed, 18 skipped.
  • prek run --all-files clean, including the regenerated environment.yml.

Stacked on a feature branch, so this gets the repo-wide workflows but not the test matrix until the stack merges down.

@drbenvincent drbenvincent added review:high High-impact change requiring thorough human review needs:maintainer-decision Maintainer direction needed before review can conclude labels Aug 10, 2026
@drbenvincent

Copy link
Copy Markdown
Collaborator

Automated triage

Recommendation: review:high — maintainer decision needed.

Why:

  • Reduces the mypy allowlist from 9 entries to 3 and errors from 30 in 13 files to 10 in 5. Clear mechanical improvements (variable rebinding, widened type unions).
  • The author explicitly flags 3 unresolved clusters requiring public-type decisions on self.model, self.idata, and self.causal_impact — these are maintainer-significant API choices.
  • Stacked on fix: keep plot_ate axes as a list so the panels type-check #1146 and target feature branch pymc6_and_pymcmarketing1_migration. CI is UNSTABLE (readthedocs pending).
  • Cross-sections 13 source files across the core library, including experiment classes and plot utils.

Review focus:

  1. Decide the self.model type narrowing strategy for IPW and IV experiment classes (the 6-call-arg error cluster).
  2. Decide whether to add assert runtime checks for the None-on-Optional value errors or accept inline ignores.
  3. Confirm the stacked branch order is correct before merging.

Confidence: high

`round_num` declared `n: float`, but four call sites pass a zero-dimensional DataArray straight from `.mean()` on posterior samples: regression_kink twice, regression_discontinuity and prepostnegd. That works at runtime, because xarray formats a scalar DataArray like the number it wraps, so this was the annotation being narrower than the function.

Widened to `float | xr.DataArray`, matching its sibling `convert_to_string`, which already declared exactly that union. `_format_sig_figs` takes the same value one call deeper and gets the same treatment. Widening a parameter is backwards compatible, so no caller has to change.

Clears 4 errors and removes regression_kink, regression_discontinuity and prepostnegd from the allowlist entirely.
Each of these constructors takes `data: DataFrameLike` and then rebinds that same parameter to the pandas frame that `to_pandas` or `to_pandas_with_time_index` returns. mypy keeps the declared parameter type for the rest of the body, so `data.index`, `data.copy()` and `data[column]` were all reported against `NativeDataFrame`, which has none of them. The functions were already annotated `-> pd.DataFrame`; the information was lost at the rebinding, not at the source.

The converted frame now gets its own name, `pandas_data`, in synthetic_control, interrupted_time_series, synthetic_difference_in_differences, staggered_did, diff_in_diff and panel_regression. That also matches what the existing comments in those constructors already said, that the conversion returns a copy the experiment owns rather than the caller's frame.

Clears 9 errors and empties the `attr-defined` group. Behaviour is unchanged: the same object is stored on `self.data` and passed to validation as before. Full suite: 2284 passed, 18 skipped.
Five unrelated one-liners, each where the annotation and the value already agreed at runtime.

`reporting` used `np.abs(effect)` on a DataArray. numpy's stubs say that returns an ndarray, while xarray dispatches it back to a DataArray, which is what `_posterior_probability` wants. The builtin `abs()` goes through `__abs__` and is typed correctly, and computes the same thing.

`_arviz_compat` sorted a set of xarray dimension names, which are `Hashable` and not necessarily orderable. The names go straight into an error message, so they sort as strings.

`plot_utils._interval_bound_values` bound `lower` and `upper` to arrays in the HDI branch and to DataArrays in the ETI branch, then read `.dims` off the second. The ETI bounds now get their own names and convert to arrays before the branches rejoin, which is what the shared tail did anyway.

`pymc_models` passed a `Hashable` dimension name as a key into a `dict[str, Any]` of coords. Dimension names are strings here and PyMC requires string coord keys.

`operating_characteristics` passed a list to `tight_layout(rect=...)`, which takes a 4-tuple.

`diff_in_diff` iterates the "bodies" entry of a violinplot result, which matplotlib types as a single Collection rather than a list of them. That one is a stub limitation, so it takes an inline ignore, and `warn_unused_ignores` will flag it once matplotlib describes the return properly.

The allowlist drops from nine entries to three, and from 49 errors in 16 files to 10 in 5. Full suite: 2284 passed, 18 skipped.
@anevolbap
anevolbap force-pushed the chore/mypy-shrink-allowlist branch from 911652c to 7c41e50 Compare August 10, 2026 11:40
@read-the-docs-community

read-the-docs-community Bot commented Aug 10, 2026

Copy link
Copy Markdown

Documentation build overview

📚 causalpy | 🛠️ Build #33992179 | 📁 Comparing 7c41e50 against latest (ea44a98)

  🔍 Preview build  

252 files changed · + 30 added · ± 200 modified · - 22 deleted

+ Added

± Modified

- Deleted

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

Labels

needs:maintainer-decision Maintainer direction needed before review can conclude review:high High-impact change requiring thorough human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants