|
| 1 | +# Mypy's PEP 561 NumPy issue repro |
| 2 | + |
| 3 | +From <https://mypy.readthedocs.io/en/stable/running_mypy.html#how-imports-are-found> (which superseeds PEP 561): |
| 4 | + |
| 5 | +> 1. [Stubs](https://typing.python.org/en/latest/spec/glossary.html#term-stub) or Python source manually put in the beginning of the path. Type checkers SHOULD provide this to allow the user complete control of which stubs to use, and to patch broken stubs or [inline](https://typing.python.org/en/latest/spec/glossary.html#term-inline) types from packages. In mypy the `$MYPYPATH` environment variable can be used for this. |
| 6 | +> 2. User code - the files the type checker is running on. |
| 7 | +> 3. Typeshed stubs for the standard library. These will usually be vendored by type checkers, but type checkers SHOULD provide an option for users to provide a path to a directory containing a custom or modified version of typeshed; if this option is provided, type checkers SHOULD use this as the canonical source for standard-library types in this step. |
| 8 | +> 4. **[Stub](https://typing.python.org/en/latest/spec/glossary.html#term-stub) packages - these packages SHOULD supersede any installed inline package. They can be found in directories named `foopkg-stubs` for package `foopkg`.** |
| 9 | +> 5. **Packages with a `py.typed` marker file - if there is nothing overriding the installed package, and it opts into type checking, the types bundled with the package SHOULD be used (be they in `.pyi` type stub files or inline in `.py` files).** |
| 10 | +> 6. If the type checker chooses to additionally vendor any third-party stubs (from typeshed or elsewhere), these SHOULD come last in the module resolution order. |
| 11 | +
|
| 12 | +Here, [NumPy](https://github.com/numpy/numpy)'s bundled stubs fall under 5., and [NumType](https://github.com/numpy/numtype/)'s [`numpy-stubs`](https://github.com/numpy/numtype/tree/2fb0af907b68558e4b0778c9dc4b21262105adb2/src/numpy-stubs) fall under 4.. |
| 13 | +So NumType should be prioritized over the NumPy. |
| 14 | + |
| 15 | +Pyright behavior confirms that this is indeed what should happen. |
| 16 | + |
| 17 | +But mypy appears to incorrectly prioritize 5. (`numpy/__init__.pyi`) over 4. (`numpy-stubs/__init__.pyi` from NumType) — |
| 18 | +it should be the other way around. |
| 19 | + |
| 20 | +## Env setup |
| 21 | + |
| 22 | +Using python 3.10+ and [`uv`](https://docs.astral.sh/uv/) |
| 23 | + |
| 24 | +```bash |
| 25 | +uv venv .venv |
| 26 | +source .venv/bin/activate |
| 27 | +``` |
| 28 | + |
| 29 | +If you don't have `uv` installed, then replace `uv` with `python -m`, and later on you can replace `uv pip` with `pip`. |
| 30 | + |
| 31 | +### Install (without NumType) |
| 32 | + |
| 33 | +Install `mypy 1.5.0`, `pyright 1.1.400`, and `numpy 2.2.5`: |
| 34 | + |
| 35 | +```bash |
| 36 | +uv pip uninstall numtype |
| 37 | +uv pip install --reinstall -r requirements.txt |
| 38 | +``` |
| 39 | + |
| 40 | +(the `--reinstall` flag is only needed after the nuclear workaround) |
| 41 | + |
| 42 | +To confirm: |
| 43 | + |
| 44 | +```bash |
| 45 | +$ uv pip list |
| 46 | +Package Version |
| 47 | +----------------- ------- |
| 48 | +mypy 1.15.0 |
| 49 | +mypy-extensions 1.1.0 |
| 50 | +nodeenv 1.9.1 |
| 51 | +numpy 2.2.5 |
| 52 | +pyright 1.1.400 |
| 53 | +typing-extensions 4.13.2 |
| 54 | +``` |
| 55 | + |
| 56 | +### Install (with NumType) |
| 57 | + |
| 58 | +Install `mypy 1.5.0`, `pyright 1.1.400`, `numpy 2.2.5`, and `numtype @ 2fb0af9` |
| 59 | + |
| 60 | +```bash |
| 61 | +uv pip install -r requirements-numtype.txt |
| 62 | +``` |
| 63 | + |
| 64 | +To confirm: |
| 65 | + |
| 66 | +```bash |
| 67 | +$ uv pip list |
| 68 | +Package Version |
| 69 | +----------------- ------------ |
| 70 | +mypy 1.15.0 |
| 71 | +mypy-extensions 1.1.0 |
| 72 | +nodeenv 1.9.1 |
| 73 | +numpy 2.2.5 |
| 74 | +numtype 2.2.5.0.dev0 |
| 75 | +pyright 1.1.400 |
| 76 | +typing-extensions 4.13.2 |
| 77 | +``` |
| 78 | + |
| 79 | +## Bug demo |
| 80 | + |
| 81 | +In NumPy's bundled stubs (`numpy==2.2.5`), the type of `np.True_` ([src](https://github.com/numpy/numpy/blob/7be8c1f9133516fe20fd076f9bdfe23d9f537874/numpy/__init__.pyi#L1161)) is called `numpy.bool` (shadowing `builtin.bool`). |
| 82 | + |
| 83 | +In NumType's `numpy-stubs` (`2fb0af9`), the same `numpy.True_` is defined in [`numpy-stubs/_core/numeric.pyi`](https://github.com/numpy/numtype/blob/2fb0af907b68558e4b0778c9dc4b21262105adb2/src/numpy-stubs/_core/numeric.pyi#L623) (re-exported in [`__init__.pyi`](https://github.com/numpy/numtype/blob/2fb0af907b68558e4b0778c9dc4b21262105adb2/src/numpy-stubs/__init__.pyi#L77)), and its type is a `np.bool_` (note the trailing underscore). |
| 84 | + |
| 85 | +So with a `reveal_type(np.True_)` (in the `main.pyi` of this repo) we'll be able to tell which stubs used. |
| 86 | + |
| 87 | +### Pyright (without NumType) |
| 88 | + |
| 89 | +NumType: No |
| 90 | + |
| 91 | +```bash |
| 92 | +$ pyright . |
| 93 | +/home/joren/Workspace/mypy-numtype/main.pyi |
| 94 | + /home/joren/Workspace/mypy-numtype/main.pyi:5:17 - information: Type of "np.True_" is "bool[Literal[True]]" |
| 95 | +``` |
| 96 | + |
| 97 | +`numpy.bool` => NumPy's bundled stubs are used :white_check_mark: |
| 98 | + |
| 99 | +### Pyright (with NumType) |
| 100 | + |
| 101 | +```bash |
| 102 | +$ pyright . |
| 103 | +/home/joren/Workspace/mypy-numtype/main.pyi |
| 104 | + /home/joren/Workspace/mypy-numtype/main.pyi:3:13 - information: Type of "np.True_" is "bool_[Literal[True]]" |
| 105 | +``` |
| 106 | + |
| 107 | +`numpy.bool_` => NumType's `numpy-stubs` are used :white_check_mark: |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +### Mypy (without NumType) |
| 112 | + |
| 113 | +NumType: No |
| 114 | + |
| 115 | +```bash |
| 116 | +$ rm -rf .mypy_cache |
| 117 | +$ mypy . |
| 118 | +main.pyi:3: note: Revealed type is "numpy.bool[Literal[True]]" |
| 119 | +Success: no issues found in 1 source file |
| 120 | +``` |
| 121 | + |
| 122 | +`numpy.bool` => NumPy's bundled stubs are used :white_check_mark: |
| 123 | + |
| 124 | +### Mypy (with NumType) |
| 125 | + |
| 126 | +```bash |
| 127 | +$ rm -rf .mypy_cache |
| 128 | +$ mypy main.pyi |
| 129 | +main.pyi:3: note: Revealed type is "numpy.bool[Literal[True]]" |
| 130 | +``` |
| 131 | + |
| 132 | +`numpy.bool` => NumPy's bundled stubs are used :x: |
| 133 | + |
| 134 | +## The nuclear workaround |
| 135 | + |
| 136 | +<details> |
| 137 | +<summary>The only workaround I was able to find (after several full days of trying), was to remove all <code>.pyi</code> from numpy's local installation directory. But this is not something I can realistically ask the users of NumPy to do.</summary> |
| 138 | + |
| 139 | +First do a dry run: |
| 140 | + |
| 141 | +```bash |
| 142 | +$ find ./.venv/**/site-packages/numpy -name "*.pyi" -type f |
| 143 | +./.venv/lib/python3.13/site-packages/numpy/matlib.pyi |
| 144 | +./.venv/lib/python3.13/site-packages/numpy/fft/_helper.pyi |
| 145 | +./.venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.pyi |
| 146 | +[...] |
| 147 | +``` |
| 148 | + |
| 149 | +If all is good, press the red button: |
| 150 | + |
| 151 | +```bash |
| 152 | +$ find ./.venv/**/site-packages/numpy -name "*.pyi" -type f -delete |
| 153 | +``` |
| 154 | + |
| 155 | +Re-run pyright: |
| 156 | + |
| 157 | +```bash |
| 158 | +$ pyright . |
| 159 | +/home/joren/Workspace/mypy-numtype/main.pyi |
| 160 | + /home/joren/Workspace/mypy-numtype/main.pyi:3:13 - information: Type of "np.True_" is "bool_[Literal[True]]" |
| 161 | +``` |
| 162 | + |
| 163 | +Good; that's exactly the same result as before (with NumType). |
| 164 | + |
| 165 | +Now run mypy again: |
| 166 | + |
| 167 | +```bash |
| 168 | +$ rm -rf .mypy_cache |
| 169 | +$ mypy . |
| 170 | +main.pyi:3: note: Revealed type is "numpy.bool_[Literal[True]]" |
| 171 | +Success: no issues found in 1 source file |
| 172 | +``` |
| 173 | + |
| 174 | +This is indeed the correct output, which for the 2nd time demonstrates that mypy does not comply with PEP 561. |
| 175 | +</details> |
0 commit comments