python / mypy

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

Incorrect widening of `T | None` with unions to `builtins.object` in generic functions #17103

Open olahesoo opened 5 months ago

olahesoo commented 5 months ago

Bug Report

Generic function with signature (fun: Callable[[], T | None]) -> T | None has its type incorrectly widened to builtins.object when T is a union type

To Reproduce

import typing

T = typing.TypeVar("T")

def gen_fun_one(fun: typing.Callable[[], T | int]) -> T | int:
    return fun()

def gen_fun_two(fun: typing.Callable[[], T | None]) -> T | None:
    return fun()

def foo() -> int | str | None:
    pass

typing.reveal_type(gen_fun_one(foo))  # Union[builtins.str, None, builtins.int]
typing.reveal_type(gen_fun_two(foo))  # builtins.object

Gist URL: https://gist.github.com/mypy-play/f2401bd493520f4c72cad4a416195e19 Playground URL: https://mypy-play.net/?mypy=latest&python=3.12&gist=f2401bd493520f4c72cad4a416195e19

Expected Behavior

Both gen_fun_one(foo) and gen_fun_two(foo) are of type str | int | None

Actual Behavior

Function gen_fun_two(foo) is of type builtins.object

Your Environment

terencehonles commented 5 months ago

I believe this is because T in the first is str | None, which mypy can probably capture as a single type, but for the second it's str | int which mypy is coercing to object since that's the only type that captures both of them.

Does it work with:

def gen_fun(fun: Callable[[], T]) -> T:
    return fun()
olahesoo commented 5 months ago

It does work, furthermore adding float to the union makes the second one no longer work.

import typing

T = typing.TypeVar("T")

def gen_fun_one(fun: typing.Callable[[], T | int]) -> T | int:
    return fun()

def gen_fun_two(fun: typing.Callable[[], T | None]) -> T | None:
    return fun()

def gen_fun_three(fun: typing.Callable[[], T]) -> T:
    return fun()

def foo() -> int | str | None:
    pass

def bar() -> int | str | float | None:
    pass

typing.reveal_type(gen_fun_one(foo))  # Union[builtins.str, None, builtins.int]
typing.reveal_type(gen_fun_one(bar))  # builtins.object
typing.reveal_type(gen_fun_two(foo))  # builtins.object
typing.reveal_type(gen_fun_two(bar))  # builtins.object
typing.reveal_type(gen_fun_three(foo))  # Union[builtins.int, builtins.str, None]
typing.reveal_type(gen_fun_three(bar))  # Union[builtins.int, builtins.str, builtins.float, None]

Gist URL: https://gist.github.com/mypy-play/5d4c79853b831518d2f870b3d9d9ca93 Playground URL: https://mypy-play.net/?mypy=latest&python=3.12&gist=5d4c79853b831518d2f870b3d9d9ca93

It looks to me like a join-v-union issue