Repository navigation
[BUG] 0.32.0 wheels ship py.typed but no .pyi stubs — breaks type checking of mlx.core downstream #3916
Description
Activity
Thanks for reporting the issue, I'm not familiar with python's type system, what would be the best command to verify the correctness of type stubs so we can run it in CI?
uv pip install mlx uv run ????You can run
uv checkto verify correctness of the type stubs.Reacted by Cheng@zcbenz thanks for looking at this — happy to give you a concrete recipe.
One small clarification so CI doesn't end up green by accident:
uv checkverifies a project's metadata/lockfile rather than type stubs, so it will pass either way here. The checks below are aimed specifically at the.pyifiles.The cheap gate: assert the stubs are in the wheel
This is the one that would actually have caught the regression, and it needs no type checker:
python -m zipfile -l dist/mlx-*.whl | grep -E 'mlx/core/.*\.pyi'
Or as a hard CI check right after the frontend wheel is built:
python - <<'EOF' import glob, sys, zipfile whl = glob.glob("dist/mlx-*.whl")[0] names = zipfile.ZipFile(whl).namelist() missing = [f for f in ("mlx/core/__init__.pyi", "mlx/core/fast.pyi", "mlx/core/random.pyi", "mlx/core/linalg.pyi") if f not in names] if missing: sys.exit(f"{whl}: missing stubs {missing}") print(f"{whl}: stubs OK") EOF
For reference, the frontend wheels contain (same platform tag,
cp312-macosx_15_0_arm64):version mlx/py.typedmlx/core/*.pyi0.31.2 yes 8 files ( __init__,cuda,distributed,fast,fft,linalg,metal,random)0.32.0 yes 0 files The end-to-end check: type-check a downstream snippet
If you want CI to verify that the shipped package is actually usable by a type checker (this is what breaks for downstream projects):
uv pip install mlx mypy cat > stub_probe.py <<'EOF' import mlx.core as mx a: mx.array = mx.zeros((2, 3), dtype=mx.float32) b: mx.array = mx.fast.rms_norm(a, mx.ones((3,)), 1e-5) reveal_type(mx.zeros) EOF mypy stub_probe.py
Verified A/B in a clean venv (Python 3.12, macOS arm64), only the mlx version differs:
# mlx==0.32.0 stub_probe.py:1: error: Cannot find implementation or library stub for module named "mlx.core" [import-not-found] stub_probe.py:5: note: Revealed type is "Any" Found 1 error in 1 file (checked 1 source file) # mlx==0.31.2 stub_probe.py:5: note: Revealed type is "def (shape: int | typing.Sequence[int], dtype: mlx.core.Dtype | None =, *, stream: None | mlx.core.Stream | mlx.core.Device =) -> mlx.core.array" Success: no issues found in 1 source fileAny checker works here (
pyright,ty, …); mypy just has the least setup. Note that becausepy.typedis shipped, the missing stubs are worse than having no typing marker at all: downstream checkers trust the package and silently resolve everything through the extension module.(
python -m mypy.stubtest mlx.coreis the tool for verifying stubs match the runtime, but it already reports a handful of pre-existing nits on 0.31.2, so I wouldn't make it a blocking gate — the two checks above are enough for this regression.)Where it likely regressed
The stub machinery itself is unchanged between 0.31.2 and 0.32.0 —
python/src/CMakeLists.txtis byte-identical, andsetup.pystill has bothnanobind_add_stub/--component core_stubandpackage_data = {"mlx.core": ["*.pyi"]}.What changed is how release wheels are built. In
.github/actions/build-wheel/action.ymlthe frontend step now runs:python setup.py clean --all MLX_BUILD_STAGE=1 python -m build -wThe stubs are generated in
CMakeBuild.run()(i.e. duringbuild_ext), which setuptools runs afterbuild_py— andbuild_pyis what collectspackage_datafrompython/mlx/core/*.pyi. So on a tree that already has stubs from a previous build the files get picked up, but on a freshly cleaned tree (clean --all+ a fresh CI checkout)build_pyruns before they exist and the wheel ships without them. That would explain why local builds look fine while the released wheels don't.Haven't tested a fix, so treat that as a hypothesis — but if it's right, generating the stubs before
build_py(or adding the.pyifiles via abuild_py/bdist_wheelhook afterbuild_ext) plus the zip assertion above should close it for good.Happy to open a PR if that's useful.
Thanks for looking into this, I had included a fix in https://github.com/ml-explore/mlx/pull/3818/changes#diff-60f61ab7a8d1910d86d9fda2261620314edcae5894d5aaa236b821c7256badd7R193 and I could verify the type stubs were added to the wheels https://github.com/ml-explore/mlx/actions/runs/31754270684/job/94626578013. We just have too much type errors to run
uv checkin CI.I think there are two issues here:
-
missing type stubs in the latest build, and
-
type errors in the codebase.
uv checkwill error on both of these, but may be possible to parse its output to separate the two issues. Otherwise a simpler check for the presence of the type stubs?-
- added a commit that references this issue
on Aug 21, 2026
Summary
The macOS arm64 wheels for 0.32.0 ship
mlx/py.typedbut contain no.pyifiles at all. The 0.31.2 wheels contain 8 (mlx/core/__init__.pyi,cuda.pyi,distributed.pyi,fast.pyi, …).This is the worst of both worlds for downstream type checking:
py.typedasserts the package is typed, so checkers trust it and do not fall back to inference or bundled third-party stubs — but there is nothing to resolvemlx.coreagainst, since it is a compiled extension.Reproduction
No install needed — inspect the published wheels directly:
Same result for
cp311. (Checked on 2026-07-25; 0.32.0 is the latest release.)Downstream impact
Any type checker fails on every import. With
ty:Runtime is completely unaffected — this is packaging-only. In our project we had to constrain the dev environment to
mlx<0.32to keep the type-check job green, which in turn blocks adopting any 0.32-only API (e.g. themx.fast.metal_kernelmath-mode option from #3728), since amlx>=0.32runtime floor would make the dev extra uninstallable.Possible cause
Stub generation does not appear to have been removed deliberately —
setup.pystill runs it at the end ofbuild_ext.run():So the stubs are most likely generated but then dropped before the wheel is assembled. One hypothesis worth checking first: the two-stage release build (
MLX_BUILD_STAGE=1formlx, stage 2 formlx-metal) combined with the CI rework in #3491, which movedpython setup.py clean --allto run afterpython -m build -wand addeduv venv --clearbefore the build. Something in that ordering could remove or hide the installed stubs relative to the directory that gets packaged. I have not bisected this — treat it as a starting point, not a diagnosis.Prior art for the same class of regression: #2398 ("Fix including stubs in wheel") and #2942 ("Fix stubgen").
Expected
0.32.x wheels ship
mlx/core/*.pyiagain, as 0.31.2 did. Failing that, droppingmlx/py.typedwould at least let downstream checkers fall back gracefully instead of hard-failing — but restoring the stubs is clearly the better outcome.A regression test in the release job — assert the built wheel contains
mlx/core/__init__.pyibefore upload — would prevent a third recurrence.