Skip to content

Treating all numpy floating and complex types as interchangeable in overloads #20141

Open

Description

@mcdigman

Bug Report

I am trying to specify that a function takes either an array of np.floating or np.complexfloating, and returns an array of the same type, using overloads. However, it seems like mypy is currently treating floating and complexfloating as completely interchangeable, instead of floating being a more specific type than complexfloating. As shown below, it also treats other numpy floating/complex types as interchangeable, e.g. np.float32 and np.complex128.

To Reproduce

from __future__ import annotations

from typing import overload

import numpy as np
from numpy.typing import NDArray


@overload
def test_type1(a: NDArray[np.floating]) -> NDArray[np.floating]:  ...
@overload
def test_type1(a: NDArray[np.complexfloating]) -> NDArray[np.complexfloating]: ...
def test_type1(a: NDArray[np.floating] | NDArray[np.complexfloating]) -> NDArray[np.floating] | NDArray[np.complexfloating]:
    return a


@overload
def test_type2(a: NDArray[np.complexfloating]) -> NDArray[np.complexfloating]: ...
@overload
def test_type2(a: NDArray[np.floating]) -> NDArray[np.floating]:  ...
def test_type2(a: NDArray[np.floating] | NDArray[np.complexfloating]) -> NDArray[np.floating] | NDArray[np.complexfloating]:
    return a


@overload
def test_type3(a: np.floating) -> np.floating:  ...
@overload
def test_type3(a: np.complexfloating) -> np.complexfloating: ...
def test_type3(a: np.floating | np.complexfloating) -> np.floating | np.complexfloating:
    return a


@overload
def test_type4(a: np.complexfloating) -> np.floating:  ...
@overload
def test_type4(a: np.floating) -> np.floating: ...
def test_type4(a: np.floating | np.complexfloating) -> np.floating | np.complexfloating:
    return a


@overload
def test_type5(a: np.float32) -> np.float32:  ...
@overload
def test_type5(a: np.complex128) -> np.complex128: ...
def test_type5(a: np.float32 | np.complex128) -> np.float32 | np.complex128:
    return a


@overload
def test_type6(a: float) -> float: ...
@overload
def test_type6(a: complex) -> float:  ...
def test_type6(a: float | complex) -> float | complex:
    # the only one that passes mypy
    return a

Actual Behavior

In version 1.15.0 the code above passes mypy. In version 1.16.0 and later, it gives:

mypy_test.py:12: error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match] mypy_test.py:20: error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match] mypy_test.py:28: error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match] mypy_test.py:36: error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match] mypy_test.py:44: error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match]

Expected Behavior

The errors for line 20 and 36 (test_type2 and test_type4) are correct errors as I understand the intended function of this warning. test_type6 correctly passes, consistent with the documented behavior of this check. However, the other 3 functions should pass with no warning, but are generating one.

Your Environment

I have tested and get the errors with mypy versions 1.16.0 and 1.18.2. The numpy version is 2.2.6 and python 3.13.7.

Activity

  1. A5rocks commented on Oct 30, 2025

    @A5rocks
    Collaborator

    Yeah I don't think this should error. Here's the relevant types I think:

    class inexact(number[_NBit, _InexactItemT_co], Generic[_NBit, _InexactItemT_co]):
        ...
    
    class floating(_RealMixin, _RoundMixin, inexact[_NBit1, float]):
        ...
    
    class complexfloating(inexact[_NBit1, complex], Generic[_NBit1, _NBit2]):
        ...
  2. jorenham commented on Nov 12, 2025

    @jorenham
    Contributor

    No need for overloads; generics are all you need ;)

    def f[T: np.inexact | NDArray[np.inexact]](a: T) -> T:
        return a

    or, if you want to e.g. turn a scalar into an array, you could parametrize the inner scalar-type:

    def f[S: np.inexact](s: S) -> NDArray[S]:
        return np.array(s)

    That said; the errors don't seem right to me either. np.floating and np.complexfloating are mutually incompatible types that share the same baseclass, np.inexact.

  3. mcdigman commented on Nov 12, 2025

    @mcdigman
    Author

    @jorenham I agree in the example I provided the overload isn't really necessary, though I think it should work to do that. However, one could construct a case where it is actually necessary that still fails currently, for example (just the first thing that came to mind):

    @overload
    def test_type7(a: np.float64) -> np.float64:  ...
    @overload
    def test_type7(a: np.complex128) -> NDArray[np.float64]: ...
    def test_type7(a: np.float64 | np.complex128) -> np.float64 | NDArray[np.float64]:
        if isinstance(a, np.complex128):
            return np.array([np.real(a), np.imag(a)])
        return a
    

    Which generates the same warning despite actually doing something different depending on the input type.

    Edit: Another semi-plausible example of such a function that also generates the same warning currently:

    @overload
    def test_type8(a: np.int64) -> bool:  ...
    @overload
    def test_type8(a: np.int32) -> None: ...
    def test_type8(a: np.int32 | np.int64) -> bool | None:
        # check if the 33rd bit is 1 or return None if there is no 33rd bit
        if isinstance(a, np.int64):
            return bool((a >> 33) & 1)
        return None
    
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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions