Skip to content

Irregular inference with ternary based on attribute. #19561

@randolf-scholz

Description

@randolf-scholz

Consider the following test cases, which all work just fine:

from typing import TypeVar, Union, Callable, reveal_type

NOOP = lambda: None
class A: pass

def test_static_attr(x: Union[A, None]) -> None:
    def foo(t: A) -> None: ...

    l1: Callable[[], None] = (lambda: foo(x)) if x is not None else NOOP   # ✅
    r1: Callable[[], None] = NOOP if x is None else (lambda: foo(x))   # ✅
    l2 = (lambda: foo(x)) if x is not None else NOOP   # ✅
    r2 = NOOP if x is None else (lambda: foo(x))   # ✅
    reveal_type(l2)  # N: Revealed type is "def ()" ✅
    reveal_type(r2)  # N: Revealed type is "def ()" ✅

def test_generic_attr(x: Union[A, None]) -> None:
    T = TypeVar("T")
    def bar(t: T) -> T: return t

    l1: Callable[[], None] = (lambda: bar(x)) if x is None else NOOP   # ✅
    r1: Callable[[], None] = NOOP if x is not None else (lambda: bar(x))   # ✅
    l2 = (lambda: bar(x)) if x is None else NOOP   # ✅
    r2 = NOOP if x is not None else (lambda: bar(x))   # ✅
    reveal_type(l2)  # N: Revealed type is "def ()" ✅
    reveal_type(r2)  # N: Revealed type is "def ()" ✅

However, when we add a level of indirection by introducing a class B with B.attr: A | None, and basing the decision on this attribute, it sometimes works and sometimes doesn't:

from typing import TypeVar, Union, Callable, reveal_type

NOOP = lambda: None
class A: pass
class B:
    attr: Union[A, None]

def test_static_with_attr(x: B) -> None:
    def foo(t: A) -> None: ...

    l1: Callable[[], None] = (lambda: foo(x.attr)) if x.attr is not None else NOOP   # ❌
    r1: Callable[[], None] = NOOP if x.attr is None else (lambda: foo(x.attr))   # ❌
    l2 = (lambda: foo(x.attr)) if x.attr is not None else NOOP   # ✅
    r2 = NOOP if x.attr is None else (lambda: foo(x.attr))   # ❌
    reveal_type(l2)  # N: Revealed type is "def ()" ✅
    reveal_type(r2)  # N: Revealed type is "def ()" ✅

def test_generic_with_attr(x: B) -> None:
    T = TypeVar("T")
    def bar(t: T) -> T: return t

    l1: Callable[[], None] = (lambda: bar(x.attr)) if x.attr is None else NOOP  # ❌
    r1: Callable[[], None] = NOOP if x.attr is not None else (lambda: bar(x.attr))  # ❌
    l2 = (lambda: bar(x.attr)) if x.attr is None else NOOP  # ✅
    r2 = NOOP if x.attr is not None else (lambda: bar(x.attr))  # ✅
    reveal_type(l2)  # N: Revealed type is "def ()" ✅
    reveal_type(r2)  # N: Revealed type is "def () -> __main__.A | None"  ❌

https://mypy-play.net/?mypy=1.17.0&python=3.12&gist=106fbfcd39eee8f3a2c51d45b154b5fb

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions