python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.33k stars 2.81k forks source link

False positive when using coroutines and generics #15886

Open Apakottur opened 1 year ago

Apakottur commented 1 year ago

Bug report

Mypy is not consistent between sync and async variants of the same code.

How to reproduce

Consider the following code:

from typing import Generic, TypeVar

T = TypeVar("T")

class Cls(Generic[T]):
    pass

def create_cls(arg: T) -> Cls[tuple[T]]:
    return Cls()

def inner(var: Cls[tuple[T]]) -> T | None:
    return None

def outer(x: int) -> int | None:
    c = create_cls(x)
    return inner(c)

Running mypy test.py yields no errors, as expected.

However, if we make the functions async:

from typing import Generic, TypeVar

T = TypeVar("T")

class Cls(Generic[T]):
    pass

def create_cls(arg: T) -> Cls[tuple[T]]:
    return Cls()

async def inner(var: Cls[tuple[T]]) -> T | None:
    return None

async def outer(x: int) -> int | None:
    c = create_cls(x)
    return await inner(c)

Then running mypy test.py gives:

test.py:21: error: Argument 1 to "inner" has incompatible type "Cls[tuple[int]]"; expected "Cls[tuple[int | None]]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Expected Behavior

Mypy should give no errors in the async case, there's no difference in any of the function signatures.

Your Environment

JaylenLuc commented 1 year ago

I created a new test file and ran pytest with your exact code on Python 3.11 and it seemed to pass. Let me know if i missed anything.

[case checkasync]
# flags: --python-version 3.11

from typing import Generic, TypeVar

T = TypeVar("T")

class Cls(Generic[T]):
    pass

def create_cls(arg: T) -> Cls[tuple[T]]:
    return Cls()

async def inner(var: Cls[tuple[T]]) -> T | None:
    return None

async def outer(x: int) -> int | None:
    c = create_cls(x)
    return await inner(c)
[builtins fixtures/tuple.pyi]
image
Apakottur commented 1 year ago

I'm getting the same results as you, the unit test is passing. However running the mypy executable directly on the code still yields the issue. What can lead to a difference between the unit test and the regular execution?

Here's an even smaller snippet to reproduce the error:

from typing import Generic, TypeVar

T = TypeVar("T")

class Cls(Generic[T]):
    pass

async def inner(c: Cls[T]) -> T | None:
    return None

async def outer(c: Cls[T]) -> T | None:
    return await inner(c)
JaylenLuc commented 1 year ago

I'll check it out tonight? My guess is the pytest does not run the same code on the file as the regular execution ? Maybe we can collaborate on this issue on discord if you have one.

Apakottur commented 1 year ago

Made some progress, here's an even simpler snippet that also fails in the mypy unit tests:

from typing import Generic, TypeVar

T = TypeVar("T")

class Cls(Generic[T]):
    pass

def inner(c: Cls[T]) -> T | int:
    return 1

def outer(c: Cls[T]) -> T | int:
    return inner(c)

There's no async/await even, so it seems to be a problem purely with generics.

hauntsaninja commented 1 year ago

mypy can sometimes over eagerly use type context in cases like this. Removing type context by putting it on its own line can often fix (as can supplying exactly the context you want via annotation). That is:

def outer(c: Cls[T]) -> T | int:
    ret = inner(c)
    return ret

Using a covariant type var would also "fix", if acceptable in your real world case.

Apakottur commented 1 year ago

Thanks for the reply @hauntsaninja !

In case it matters, my real world case involves the Select class from SQLAlchemy, and the .one_or_none function which has the same T -> T | None pattern.

I have hundreds of instances of this issue, so having to create a variable every time is not ideal (not to mention that linters like Ruff will try to optimize this statement and remove the variable). Using covariant does work but requires some extra hacking because this happens in a 3rd party lib.

I've made a PR that fixes the issue based on your comment, which also fixes the issue in my real world case.