Skip to content

Instantly share code, notes, and snippets.

@AlexWaygood
Created June 23, 2026 18:07
Show Gist options
  • Select an option

  • Save AlexWaygood/2498a7266b0a58d14bf69e2fde999a2b to your computer and use it in GitHub Desktop.

Select an option

Save AlexWaygood/2498a7266b0a58d14bf69e2fde999a2b to your computer and use it in GitHub Desktop.
Minimal reproducer for astral-sh/ty#3833

Minimal reproducer for astral-sh/ty#3833

This is a dependency-free reduction of astral-sh/ty#3833, which reports that ty hangs while resolving pandas.Series.__mul__ against a NumPy ndarray when using NumPy 2.5.0's stubs.

Reconstruct the file layout

GitHub Gists have a flat filename namespace, so the two stub files have path-prefixed names here. Restore the package layout before running ty:

mkdir -p numpy/_typing
cp numpy__init__.pyi numpy/__init__.pyi
cp numpy__typing__init__.pyi numpy/_typing/__init__.pyi

The resulting tree is:

.
├── repro.py
└── numpy
    ├── __init__.pyi
    └── _typing
        └── __init__.pyi

Reproduce

First, verify that the reduced stubs are internally valid:

uvx ty@0.0.52 check --python-version 3.13 \
  numpy/__init__.pyi numpy/_typing/__init__.pyi

This prints:

All checks passed!

Then check the reproducer under a timeout:

timeout 30 uvx ty@0.0.52 check --python-version 3.13 repro.py

ty produces no output and is killed after 30 seconds (exit 124).

Original A/B result

The issue's original source was reproduced with Python 3.13, ty 0.0.52, pandas 2.3.3, and pandas-stubs 3.0.3.260530:

NumPy version Result
2.5.0 No output; timed out after 20 seconds (exit 124)
2.4.6 All checks passed!

Key findings from minimization

  • pandas and pandas-stubs can be removed completely.
  • The pandas side reduces to one generic Series.__mul__ signature whose argument is SupportsRMul[S2_contra, S2_NSDT].
  • The NumPy side requires exactly five ndarray.__rmul__ overloads. Removing any one makes ty terminate successfully.
  • NumPy's _NestedSequence protocol reduces to a single recursive __reversed__ method.
  • The numpynumpy._typing module cycle is essential. Inlining the two stubs into one module makes the hang disappear, which is why this reproducer contains two local stub modules.
  • No diagnostic code is emitted in the failing case because ty hangs before producing output.

Reproduction-command caveat

The issue body presents its PEP 723 script with uvx ty@0.0.52 check repro.py. In a clean directory, uvx does not install the script's PEP 723 dependencies for this command, so that literal invocation reports unresolved-import for NumPy and pandas. The original A/B result above was verified in isolated Python 3.13 virtual environments containing the exact stated pins; this reduced version needs no third-party packages.

from numpy._typing import (
NDArray,
_64Bit,
_ArrayLike,
_ArrayLikeInt,
_ArrayLikeNumber_co,
_ArrayLikeUInt_co,
)
from typing import Any, Generic, overload
from typing_extensions import TypeVar
_ShapeT_co = TypeVar("_ShapeT_co", default=Any, covariant=True)
_DTypeT_co = TypeVar("_DTypeT_co", default=Any, covariant=True)
_ScalarT_co = TypeVar("_ScalarT_co", default=Any, covariant=True)
_NBitT = TypeVar("_NBitT", default=Any)
_NBitT1 = TypeVar("_NBitT1", default=Any)
_NBitT2 = TypeVar("_NBitT2", default=_NBitT1)
_ItemT_co = TypeVar("_ItemT_co", default=Any, covariant=True)
_NumberItemT_co = TypeVar("_NumberItemT_co", default=Any, covariant=True)
_InexactItemT_co = TypeVar("_InexactItemT_co", default=Any, covariant=True)
_BoolItemT_co = TypeVar("_BoolItemT_co", default=Any, covariant=True)
_TD64ItemT_co = TypeVar("_TD64ItemT_co", default=Any, covariant=True)
class dtype(Generic[_ScalarT_co]): ...
type _ArrayComplex128_co = NDArray[number[_64Bit] | integer | bool]
type _ArrayUInt_co = NDArray[unsignedinteger | bool]
type _ArrayFloat_co = NDArray[inexact | integer | bool]
class ndarray(Generic[_ShapeT_co, _DTypeT_co]):
@overload
def __rmul__(self: _ArrayComplex128_co, other: _ArrayLike[complexfloating[_64Bit]], /) -> NDArray[complex128]: ...
@overload
def __rmul__(self: _ArrayUInt_co, other: _ArrayLikeUInt_co, /) -> NDArray[unsignedinteger]: ...
@overload
def __rmul__(self: NDArray[number], other: _ArrayLikeNumber_co, /) -> NDArray[number]: ...
@overload
def __rmul__(self: _ArrayFloat_co, other: _ArrayLike[timedelta64], /) -> NDArray[timedelta64]: ...
@overload
def __rmul__(
self: ndarray[Any, dtype[character]],
other: _ArrayLikeInt,
/,
) -> ndarray[tuple[Any, ...], _DTypeT_co]: ...
class generic(Generic[_ItemT_co]): ...
class character(generic): ...
class number(generic[_NumberItemT_co], Generic[_NBitT, _NumberItemT_co]): ...
class bool(generic[_BoolItemT_co], Generic[_BoolItemT_co]): ...
class integer(number[_NBitT, int]): ...
class unsignedinteger(integer[_NBitT]): ...
class inexact(number[_NBitT, _InexactItemT_co], Generic[_NBitT, _InexactItemT_co]): ...
class complexfloating(inexact[_NBitT1, complex], Generic[_NBitT1, _NBitT2]): ...
class complex128(complexfloating[_64Bit, _64Bit]): ...
class timedelta64(generic[_TD64ItemT_co], Generic[_TD64ItemT_co]): ...
from collections.abc import Iterator
from typing import Any, Protocol, TypeVar
import numpy as np
type _Shape = tuple[int, ...]
type _AnyShape = tuple[Any, ...]
class _64Bit: ...
_T_co = TypeVar("_T_co", covariant=True, default=Any)
class _NestedSequence(Protocol[_T_co]):
def __reversed__(self, /) -> Iterator[_T_co | _NestedSequence[_T_co]]: ...
type NDArray[ScalarT: np.generic] = np.ndarray[_AnyShape, np.dtype[ScalarT]]
class _SupportsArray[DTypeT: np.dtype](Protocol):
def __array__(self) -> np.ndarray[Any, DTypeT]: ...
type _ArrayLike[ScalarT: np.generic] = (
_SupportsArray[np.dtype[ScalarT]]
| _NestedSequence[_SupportsArray[np.dtype[ScalarT]]]
)
type _DualArrayLike[DTypeT: np.dtype, BuiltinT] = (
_SupportsArray[DTypeT]
| _NestedSequence[_SupportsArray[DTypeT]]
| BuiltinT
| _NestedSequence[BuiltinT]
)
type _ArrayLikeUInt_co = _DualArrayLike[np.dtype[np.bool | np.unsignedinteger], bool]
type _ArrayLikeNumber_co = _DualArrayLike[np.dtype[np.bool | np.number], complex]
type _ArrayLikeInt = _DualArrayLike[np.dtype[np.integer], int]
from __future__ import annotations
from numpy import ndarray
from typing import Any, Generic, Protocol, TypeVar
_T_contra = TypeVar("_T_contra", contravariant=True)
_T_co = TypeVar("_T_co", covariant=True)
S1 = TypeVar("S1", default=Any)
S2_contra = TypeVar("S2_contra", contravariant=True)
S2_NSDT = TypeVar("S2_NSDT")
class SupportsRMul(Protocol[_T_contra, _T_co]):
def __rmul__(self, x: _T_contra, /) -> _T_co: ...
class Series(Generic[S1]):
def __mul__(
self: Series[S2_contra],
other: SupportsRMul[S2_contra, S2_NSDT],
) -> Series[S2_NSDT]:
raise NotImplementedError
def f(s: Series, a: ndarray):
s.__mul__(a)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment