Skip to content

Add third-party type stubs to the mypy tox environment #20443

Description

@nstarman

Opened by Claude Code on my behalf and at my request while exploring #19044.

Description

Follow-up to #19044, which adds a [tool.mypy] allowlist and a CI job that type checks only the modules explicitly listed in it.

The mypy tox environment in #19044 declares deps = mypy and nothing else. That means every import of scipy, setuptools or PyYAML resolves to an untyped module, and mypy reports import-untyped instead of checking the code. Three cosmology modules are blocked from the allowlist purely by this, with no code problem of their own:

astropy/cosmology/_src/setup_package.py:7: error: Library stubs not installed for "setuptools"  [import-untyped]
astropy/cosmology/_src/scipy_compat.py:10: error: Library stubs not installed for "scipy.integrate"  [import-untyped]
astropy/cosmology/_src/scipy_compat.py:11: error: Library stubs not installed for "scipy.special"  [import-untyped]
astropy/cosmology/_src/io/builtin/yaml.py:24: error: Library stubs not installed for "yaml"  [import-untyped]

This is not cosmology-specific — scipy, setuptools and PyYAML are imported throughout astropy, so any subpackage that later wants to join the allowlist hits the same wall.

Proposal

Add the stub distributions to the mypy environment in tox.ini:

[testenv:mypy]
deps =
    mypy
    scipy-stubs
    types-PyYAML
    types-setuptools

What this actually does (measured, not assumed)

I want to be precise here, because the effect is not simply "four errors go away". Installing the stubs replaces Any with real types, which surfaces errors the untyped imports were masking. Measured with mypy 2.3.1, scipy-stubs 1.18.1.0, types-PyYAML 6.0.12.20260906, types-setuptools 84.0.0.20260812, against the three modules above:

Module Before After
_src/setup_package.py 1 0
_src/scipy_compat.py 2 3
_src/io/builtin/yaml.py 1 3

So the change immediately unblocks setup_package.py, and converts the other two from "mypy cannot see anything" into concrete, actionable findings. That is the point: with import-untyped in the way, those modules were never really being checked.

The newly surfaced errors fall into three groups, and I think each is worth its own discussion:

1. The optional-dependency shim pattern (scipy_compat.py). HAS_SCIPY is a runtime bool, so mypy analyses both branches of the if HAS_SCIPY: ... else: ... block and flags the fallbacks as redefinitions:

scipy_compat.py:15: error: Name "quad" already defined (possibly by an import)  [no-redef]
scipy_compat.py:18: error: Incompatible redefinition (redefinition with type "def ellipkinc(*args: Any, **kwargs: Any) -> Never", original type "_UFunc21f[...]")  [misc]
scipy_compat.py:21: error: Incompatible redefinition (...)  [misc]

This pattern comes from astropy.utils.compat.optional_deps and is used in many places across the codebase, so whatever convention we settle on here (a TYPE_CHECKING guard, a Final flag mypy can narrow, module-level __getattr__, …) will need to apply repo-wide. This is probably the most consequential item in this issue.

2. A genuine incorrect annotation (yaml.py). yaml_representer's inner function is annotated -> str, but it returns dumper.represent_mapping(tag, map), which is a MappingNode:

https://github.com/astropy/astropy/blob/main/astropy/cosmology/_src/io/builtin/yaml.py#L100

yaml.py:108: error: Incompatible return value type (got "MappingNode", expected "str")  [return-value]
yaml.py:167: error: Argument 2 to "add_representer" of "BaseRepresenter" has incompatible type "Callable[[AstropyDumper, Cosmology], str]"; expected "Callable[[AstropyDumper, Cosmology], Node]"  [arg-type]

The annotation is simply wrong and the stubs caught it. Harmless at runtime, but a good demonstration of the value of the CI job in #19044.

3. A loose signature (yaml.py:151). loader.construct_mapping(node) returns dict[Hashable, Any], which is passed to from_mapping declared as taking Mapping[str, Any].

Additional context

These stub packages are typing-only and are not runtime dependencies — they would be confined to the mypy tox environment and would not affect users or any other CI job.

One consideration for whoever picks this up: deps = mypy in #19044 is unpinned, and scipy-stubs tracks scipy releases closely. An unpinned stub set means a new upstream release can redden the job with no astropy change behind it. Lower bounds, or pinning, may be worth deciding at the same time.

Found while surveying astropy.cosmology for allowlist candidates in #19044.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    typingrelated to type annotations

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions