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.
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
mypytox environment in #19044 declaresdeps = mypyand nothing else. That means every import of scipy, setuptools or PyYAML resolves to an untyped module, and mypy reportsimport-untypedinstead of checking the code. Three cosmology modules are blocked from the allowlist purely by this, with no code problem of their own: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
mypyenvironment intox.ini: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
Anywith 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:_src/setup_package.py_src/scipy_compat.py_src/io/builtin/yaml.pySo 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: withimport-untypedin 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_SCIPYis a runtimebool, so mypy analyses both branches of theif HAS_SCIPY: ... else: ...block and flags the fallbacks as redefinitions:This pattern comes from
astropy.utils.compat.optional_depsand is used in many places across the codebase, so whatever convention we settle on here (aTYPE_CHECKINGguard, aFinalflag 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 returnsdumper.represent_mapping(tag, map), which is aMappingNode:https://github.com/astropy/astropy/blob/main/astropy/cosmology/_src/io/builtin/yaml.py#L100
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)returnsdict[Hashable, Any], which is passed tofrom_mappingdeclared as takingMapping[str, Any].Additional context
These stub packages are typing-only and are not runtime dependencies — they would be confined to the
mypytox environment and would not affect users or any other CI job.One consideration for whoever picks this up:
deps = mypyin #19044 is unpinned, andscipy-stubstracks 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.cosmologyfor allowlist candidates in #19044.