Skip to content

TYP: Suboptimally annotated arithmetic operator types #27965

Closed

Description

@adamjstewart

Describe the issue:

Arithmetic operators like division do not take shape typing into account, resulting in false-positive type hinting issues.

Originally reported here: #27957 (comment)
Some debugging work done by @jorenham at here: #27957 (comment)
Opening a separate bug report as requested

Reproduce the code example:

import numpy as np

x = np.zeros(1)
x = x / 1

Error message:

test.py:4: error: Incompatible types in assignment (expression has type "ndarray[tuple[int, ...], dtype[floating[Any]]]", variable has type "ndarray[tuple[int], dtype[float64]]")  [assignment]

Python and NumPy Versions:

2.2.0
3.13.0 (main, Nov 21 2024, 10:46:46) [Clang 16.0.0 (clang-1600.0.26.4)]

Type-checker version and settings:

mypy 1.11.2 (compiled: no)
mypy --strict

Additional typing packages.

No response

Activity

  1. jorenham commented on Dec 19, 2024

    @jorenham
    Member

    this example shows why type-checkers reject the x = x / 1 from your example"

    # issue_27965.pyi
    
    import numpy as np
    
    reveal_type(x := np.zeros(1))
    reveal_type(x / 1)

    relevant mypy output:

    [...]/issue_27965.pyi:3: note: Revealed type is "numpy.ndarray[tuple[builtins.int], numpy.dtype[numpy.float64]]"
    [...]/issue_27965.pyi:4: note: Revealed type is "numpy.ndarray[builtins.tuple[builtins.int, ...], numpy.dtype[numpy.floating[Any]]]"
    

    relevant pyright output:

    [...]/issue_27965.pyi:3:13 - information: Type of "x := np.zeros(1)" is "ndarray[tuple[int], dtype[float64]]"
    [...]/issue_27965.pyi:4:13 - information: Type of "x / 1" is "ndarray[tuple[int, ...], dtype[floating[Any]]]"
    

    The np.zeros(1) is inferred by both mypy and pyright as a ndarray with 1-d shape and a dtype of float64. This is correct in the most accurate way that's (reasonably) possible.

    But np.zeros(1) / 1 isn't as precisely typed, and is inferred as ndarray with ?-d shape and a dtype of floating.

    Now the problem emerges when we do x = x / 1, which means we're assigning ndarray[tuple[int, ...], floating] to ndarray[(int), float64]. But this is invalid, because

    1. tuple[int, ...] isn't assignable to tuple[int]
    2. floating isn't assignable to float64 ()

    This is because we can't assign something less specific to something more specific, just like you can't assign int to a bool.

    But if we flip the LHS and RHS, then it actually is allowed. So in the same way that bool is assignable to int, tuple[int] is assignable to tuple[int, ...] and float64 is assignable to floating.

    We can use this to create a very clean workaround to this issue:

    import numpy as np
    import numpy.typing as npt
    
    x: npt.NDArray[np.floating]  # ndarray[tuple[int, ...], dtype[floating]]
    x = np.zeros(1)  # accepted
    x = x / 1  # accepted

    Note that this workaround is only required for mypy users: Pyright infers the type of x as ndarray[tuple[int, ...], dtype[floating]], because it looks at more than one line of code at a time, and isn't "greedy" (algorithmically speaking) like mypy is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions