Consider the following test cases, which all work just fine:
fromtypingimportTypeVar, Union, Callable, reveal_typeNOOP=lambda: NoneclassA: passdeftest_static_attr(x: Union[A, None]) ->None:
deffoo(t: A) ->None: ...
l1: Callable[[], None] = (lambda: foo(x)) ifxisnotNoneelseNOOP# ✅r1: Callable[[], None] =NOOPifxisNoneelse (lambda: foo(x)) # ✅l2= (lambda: foo(x)) ifxisnotNoneelseNOOP# ✅r2=NOOPifxisNoneelse (lambda: foo(x)) # ✅reveal_type(l2) # N: Revealed type is "def ()" ✅reveal_type(r2) # N: Revealed type is "def ()" ✅deftest_generic_attr(x: Union[A, None]) ->None:
T=TypeVar("T")
defbar(t: T) ->T: returntl1: Callable[[], None] = (lambda: bar(x)) ifxisNoneelseNOOP# ✅r1: Callable[[], None] =NOOPifxisnotNoneelse (lambda: bar(x)) # ✅l2= (lambda: bar(x)) ifxisNoneelseNOOP# ✅r2=NOOPifxisnotNoneelse (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:
fromtypingimportTypeVar, Union, Callable, reveal_typeNOOP=lambda: NoneclassA: passclassB:
attr: Union[A, None]
deftest_static_with_attr(x: B) ->None:
deffoo(t: A) ->None: ...
l1: Callable[[], None] = (lambda: foo(x.attr)) ifx.attrisnotNoneelseNOOP# ❌r1: Callable[[], None] =NOOPifx.attrisNoneelse (lambda: foo(x.attr)) # ❌l2= (lambda: foo(x.attr)) ifx.attrisnotNoneelseNOOP# ✅r2=NOOPifx.attrisNoneelse (lambda: foo(x.attr)) # ❌reveal_type(l2) # N: Revealed type is "def ()" ✅reveal_type(r2) # N: Revealed type is "def ()" ✅deftest_generic_with_attr(x: B) ->None:
T=TypeVar("T")
defbar(t: T) ->T: returntl1: Callable[[], None] = (lambda: bar(x.attr)) ifx.attrisNoneelseNOOP# ❌r1: Callable[[], None] =NOOPifx.attrisnotNoneelse (lambda: bar(x.attr)) # ❌l2= (lambda: bar(x.attr)) ifx.attrisNoneelseNOOP# ✅r2=NOOPifx.attrisnotNoneelse (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
Consider the following test cases, which all work just fine:
However, when we add a level of indirection by introducing a class
BwithB.attr: A | None, and basing the decision on this attribute, it sometimes works and sometimes doesn't:https://mypy-play.net/?mypy=1.17.0&python=3.12&gist=106fbfcd39eee8f3a2c51d45b154b5fb