You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fromtypingimportOptional, TypeVarU=TypeVar('U', bound=Optional[int])
deff(u: U) ->U:
ifuisNone:
returnNoneassertisinstance(u, int) # removing this line causes it to pass.returnu
Gives
main.py:10: error: Incompatible return value type (got "int", expected "U")
I cannot remove the assertion since it stands in for other code that is effectively constraining u.
Would it be possible to get the TypeVar approach to pass? It is far more succinct, and this quickly blows up in complexity if there are more argument-return type constraints to be enforced.
This pattern is analogous to the common class factory pattern:
You can get your code snippet to pass using a value restricted typevar, instead of a bound:
from typing import Optional, TypeVar
U = TypeVar('U', None, int)
def f(u: U) -> U:
if u is None:
return None
reveal_type(u) # reveals an int
return u
Note a problematic case with bounded typevars to be mindful of in situations similar to this:
U = TypeVar('U', bound=Optional[int])
def f(u: U) -> U:
if u is None:
return None
assert isinstance(u, int)
return 0 # if this were to typecheck, it would be unsound if a subclass of int is passed in
(edit: clarifying that bounds aren't central to the issue here, eg #8354 (comment))
@hauntsaninja Could you give some insight into the difference between TypeVar('U', None, int) and TypeVar('U', bound=Optional[int])? It might not always be possible to break up a union type when the bound type is coming from a type alias. Also, the value restriction breaks callers:
fromtypingimportOptional, TypeVarU=TypeVar('U', None, int)
deff(u: U) ->U:
ifuisNone:
returnNoneassertisinstance(u, int)
returnudefg(v: Optional[int]) ->Optional[int]:
returnf(v) # error: Value of type variable "U" of "f" cannot be "Optional[int]"
@gvanrossum Looks like MyPy is the only one doing the right thing? That's great!
Callers won't break if they pass in int, None or a similar typevar type. That said, I think your example above is something mypy could support, eg, since the following type checks:
from typing import Optional, TypeVar
U = TypeVar('U', None, int)
def f(u: U) -> U:
if u is None:
return None
return u
def g(v: Optional[int]) -> Optional[int]:
if isinstance(v, int):
return f(v)
return f(v)
Anyway, for now I think you're probably still best served by overloads or using a typevar and type-ignoring within the function. Note if you type ignore, it's really easy to forgo type safety in the presence of int subclasses, eg, even the following wouldn't be type safe:
from typing import Optional, TypeVar
U = TypeVar('U', bound=Optional[int])
def f(u: U) -> U:
if u is None:
return None
return u + 0
But yes, as long as we're careful, I can't see a reason why mypy shouldn't accept:
from typing import Optional, TypeVar
U = TypeVar('U', bound=Optional[int])
def sleep(x: int) -> None: ...
def f(u: U) -> U:
if u is None:
return None
sleep(u)
return u
It should probably also not think the return None is unreachable.
Reacted by Neil Girdhar, Carlos Florensa and Brendan Batliner
at return u, mypy has forgotten that u is constrained to be of type U, and replaced that constraint with type int. In fact, u has type Intersection[U, int]. So perhaps this will be easier to implement if an Intersection type is added.
Well, in my example you're returning the exact same object t, so no matter what class it is, you'll get the same class "back".
For more complex examples where you don't return the exact same object, like this example I found with
deffoo(t: T) ->T:
ifisinstance(t, B):
returnB()
I agree that this should error because passing any subclass of B would not return an object of exactly the same class.
But in general, shouldn't it be possible to say "whatever the input class is, the output is of EXACTLY the same class"?
Does this comment address your "what if we pass a C" question @gvanrossum ?
In your first example, after isinstance(t, B), the type of t is "narrowed" to B, so mypy treats it the same (from the POV of types) as your second example. The second example shows that B is not compatible with T, so it is not correct, and therefore neither is the first example. (The type checker doesn't understand enough of the data/control flow through your code to realize that in the first example we actually return the same object.)
In any case, your example is not the same as the issue being discussed in this issue. If you need more help please consult with the helpful folks on Gitter first.
Activity
habnabit commentedon Sep 6, 2020
TypeVarisn't how you create aliases:.. though
Uisn't a great name for that.NeilGirdhar commentedon Sep 6, 2020
@habnabit I'm not trying to create an alias, I'm trying to constrain the return type to match the argument type.
NeilGirdhar commentedon Sep 6, 2020
Related to #9003 and duplicate of #5720.
[-]TypeVar incompatible with constrained type[/-][+]TypeVar incompatible with constrained union[/+]hauntsaninja commentedon Sep 6, 2020
You can get your code snippet to pass using a value restricted typevar, instead of a bound:
Note a problematic case with bounded typevars to be mindful of in situations similar to this:
(edit: clarifying that bounds aren't central to the issue here, eg #8354 (comment))
gvanrossum commentedon Sep 6, 2020
Interesting. For that snippet, mypy complains about
return 0, pyre complains about bothreturnlines, pyright and pytype don't complain about either.NeilGirdhar commentedon Sep 7, 2020
@hauntsaninja Could you give some insight into the difference between
TypeVar('U', None, int)andTypeVar('U', bound=Optional[int])? It might not always be possible to break up a union type when the bound type is coming from a type alias. Also, the value restriction breaks callers:@gvanrossum Looks like MyPy is the only one doing the right thing? That's great!
hauntsaninja commentedon Sep 7, 2020
The relevant difference here is how subclasses would get treated. The following links have more info:
https://mypy.readthedocs.io/en/stable/generics.html#type-variables-with-value-restriction
https://www.python.org/dev/peps/pep-0484/#generics
Callers won't break if they pass in int, None or a similar typevar type. That said, I think your example above is something mypy could support, eg, since the following type checks:
Anyway, for now I think you're probably still best served by overloads or using a typevar and type-ignoring within the function. Note if you type ignore, it's really easy to forgo type safety in the presence of int subclasses, eg, even the following wouldn't be type safe:
But yes, as long as we're careful, I can't see a reason why mypy shouldn't accept:
It should probably also not think the
return Noneis unreachable.NeilGirdhar commentedon Sep 7, 2020
@hauntsaninja Thanks, that makes perfect sense.
Just a guess, but it seems that in:
at
return u, mypy has forgotten thatuis constrained to be of typeU, and replaced that constraint with typeint. In fact,uhas typeIntersection[U, int]. So perhaps this will be easier to implement if anIntersectiontype is added.Thanks for the explanation.
florensacc commentedon Oct 16, 2020
This problem also happens with
boundthat are not unions:This will return
Incompatible return value type (got "B", expected "T")gvanrossum commentedon Oct 17, 2020
But what if we pass a C and expect a C back?
florensacc commentedon Oct 17, 2020
Well, in my example you're returning the exact same object
t, so no matter what class it is, you'll get the same class "back".For more complex examples where you don't return the exact same object, like this example I found with
I agree that this should error because passing any subclass of
Bwould not return an object of exactly the same class.But in general, shouldn't it be possible to say "whatever the input class is, the output is of EXACTLY the same class"?
Does this comment address your "what if we pass a C" question @gvanrossum ?
gvanrossum commentedon Oct 17, 2020
In your first example, after
isinstance(t, B), the type oftis "narrowed" toB, so mypy treats it the same (from the POV of types) as your second example. The second example shows thatBis not compatible withT, so it is not correct, and therefore neither is the first example. (The type checker doesn't understand enough of the data/control flow through your code to realize that in the first example we actually return the same object.)In any case, your example is not the same as the issue being discussed in this issue. If you need more help please consult with the helpful folks on Gitter first.
aucampia commentedon Jan 22, 2022
Similar and potentially related issues
A5rocks commentedon Jun 8, 2025
I think the initial issue here is solved; here's the output nowadays:
I think this is solely because mypy doesn't recognize that
u is Nonemeans thatUis exactlyNone? IMO that should be a new issue.