Open
Description
I have searched for a similar problem to reproduce but I haven't found anything, so:
I using a self-implemented monad which roughly looks as follows:
class Ok[T, E = None]:
def __init__(self, value: T) -> None:
self._value = value
class Err[E, T = None]:
def __init__(self, value: E) -> None:
self._value = value
type Result[T, E] = Ok[T, E] | Err[E, T]
Now, inference of the second TypeVar
variable works as intended, apart from the ternary operator.
Actual Behavior
class[U] Bar:
def foo(data: U, cond: bool) -> Result[U, str]:
return Ok(data) if cond else Err("Error")
results in
Incompatible return value type (got "Ok[U, str] | Err[str, None]", expected "Ok[U, str] | Err[str, U]")
Expected Behavior
def foo[U](data: U, cond: bool) -> Result[U, str]:
if cond:
return Ok(data)
else
return Err("Error")
and
def foo[U](data: U, cond: bool) -> Result[U, str]:
if cond:
return Ok(data)
return Err("Error")
typecheck just fine.
Your Environment
- Mypy version used:
mypy 1.15.0 (compiled: yes) - Mypy command-line flags:
none - Mypy configuration options from
mypy.ini
(and other config files):
[tool.mypy]
check_untyped_defs = true
disallow_any_unimported = true
disallow_untyped_defs = true
follow_imports = "normal"
mypy_path = "src"
namespace_packages = true
no_implicit_optional = true
show_error_codes = true
strict_optional = true
warn_no_return = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_ignores = true
- Python version used:
Python 3.13.2
Metadata
Metadata
Assignees
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
[-]Wrong `TypeVar` variable inference in ternary operators[/-][+]No `TypeVar` variable inference in ternary operators[/+]terencehonles commentedon Mar 25, 2025
I don't think this is an issue if the ternary expression. It looks like it's an issue how you define
Err
. In your type expression you defaultT = None
and it's never used so there's no way mypy can infer something other thanNone
which matches what your error is suggestingOk[U, str] | Err[str, None]
.If you rewrite your generic functions as class methods do they still work? (Just wondering if this is because one is using function generics rather than class generics)
For your two base types you never use one of the type parameters. Do you need the second type? So something like:
I would think this probably would work for you since if you drop the second parameters from the error you're seeing it would be as expected:
sasanjac commentedon Mar 26, 2025
Unfortunately, this approach doesn't work as well since there are methods that may convert the type of the inner value, e.g.:
When only using one type parameter, the other parameter has to be provided using function generics which mypy can also not infer.
The following example would not work with your proposed implementation:
mypy infers map as:
(method) map: ((op: ((int) -> U@map)) -> Result[U@map, E@map]) | ((op: ((T@map) -> U@map)) -> Result[U@map, str])
and complains:
Argument "x" to "bar" has incompatible type "T"; expected "int"Mypy[arg-type](https://mypy.readthedocs.io/en/latest/_refs.html#code-arg-type)
With my implementation, map is
(method) map: ((op: ((int) -> U@map)) -> Result[U@map, str])
What I find weird, is that inference using the ternary operator does not work, however when writing the verbose if-else, it works.
terencehonles commentedon Mar 28, 2025
I see, but for your map case should you actually be writing that as?
I assume you could come up with more and more complex examples and you're likely getting out of mypy's coverage.
I believe the reason you're seeing different behavior between the ternary and the
if/else
expression is the ternary will be generating a type union that the constraint solver needs to match, whereas theif/else
case is a simpler expression that it's more forgiving about.sasanjac commentedon Apr 4, 2025
This implementation actually gets out of mypy's coverage pretty quickly:
mypy complains:
"T" has no attribute "foo"