Skip to content

No TypeVar variable inference in ternary operators #18817

Open
@sasanjac

Description

@sasanjac

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

Activity

changed the title [-]Wrong `TypeVar` variable inference in ternary operators[/-] [+]No `TypeVar` variable inference in ternary operators[/+] on Mar 19, 2025
terencehonles

terencehonles commented on Mar 25, 2025

@terencehonles
Contributor

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 default T = None and it's never used so there's no way mypy can infer something other than None which matches what your error is suggesting Ok[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:

class Ok[T]:
    def __init__(self, value: T) -> None:
        self._value = value

class Err[E]: 
    def __init__(self, value: E) -> None:
        self._value = value

type Result[T, E] = Ok[T] | Err[E]

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:

Incompatible return value type (got "Ok[U] | Err[str]", expected "Ok[U] | Err[str]")

sasanjac

sasanjac commented on Mar 26, 2025

@sasanjac
Author

Unfortunately, this approach doesn't work as well since there are methods that may convert the type of the inner value, e.g.:

def map[U, E](self, op: t.Callable[[T], U]) -> Result[U, E]:
    return Ok(op(self._value))

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:

def bar(*, x: int, y: str) -> str:
    return str(x) + y


def foo(x: Result[int, str]) -> Result[str, str]:
    return x.map(lambda x: bar(x=x, y="foo"))

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

terencehonles commented on Mar 28, 2025

@terencehonles
Contributor

I see, but for your map case should you actually be writing that as?

def map[U](self, op: t.Callable[[T], U]) -> Ok[U]:
    return Ok(op(self._value))

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 the if/else case is a simpler expression that it's more forgiving about.

sasanjac

sasanjac commented on Apr 4, 2025

@sasanjac
Author

This implementation actually gets out of mypy's coverage pretty quickly:

class State:
    def foo(self) -> None:
        pass

def bar() -> Result[State, str]:
     return Ok(State())

bar().map(lamba state: state.foo())

mypy complains:
"T" has no attribute "foo"

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-type-contextType context / bidirectional inference

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @terencehonles@sasanjac@sterliakov

        Issue actions

          No `TypeVar` variable inference in ternary operators · Issue #18817 · python/mypy