Description
Pyrefly infers an unannotated conditional expression containing a typed list (i.e., list[T]) and an empty list as list[T] | list[Unknown] rather than list[T]. This is inconsistent with how short-circuited or expressions are handled already.
Version: pyrefly 1.2.0
Reproduction
Sandbox Link
from typing import assert_type, reveal_type
def condition() -> bool:
return True
result = list[int]() if condition() else []
reveal_type(result)
assert_type(result, list[int])
Running:
reports that result has type list[int] | list[Unknown], and the assert_type call fails.
Expected behavior
I would expect result to be inferred as list[int]. The empty-list branch can be contextually typed from the list[int] branch. Pyright also infers list[int] for this expression.
Empty containers in conditional expressions are common, for example:
values = get_values() if should_fetch() else []
Additional context
Pyrefly already performs similar contextual inference for a boolean operation:
result = list[int]() or []
reveal_type(result) # list[int]
assert_type(result, list[int])
An explicit expected type also makes the conditional expression type-check:
result: list[int] = list[int]() if condition() else []
This suggests that the conditional-expression branches are currently inferred independently when there is no expected type for the overall expression, whereas boolean-operation operands can provide a contextual hint to later operands.
I could not find an existing issue for this exact conditional-expression case. #4301 appears related because it also concerns contextual typing of an empty container, but it covers a different situation involving an explicit annotation.
Conclusion
This is an extremely common pattern that other type checkers explicitly support. This is not incompatible with pyrefly's first-use policy for inferring type parameters, but would be an additional heuristic that could be layered on top in cases where a gradual type (e.g., list[Unknown]) is unioned with an exact type (e.g., list[int]) as a result of control flow. Pyrefly already does this in certain cases, but the rule doesn't seem to be applied consistently.
Whether this should just apply to conditional expressions or also to if-else statements and so on is a design decision that I do not specify here (pyright seems to apply this to statements as well), but I think bringing if-else expressions to parity with short-circuited or expressions is a reasonable start.
Description
Pyrefly infers an unannotated conditional expression containing a typed list (i.e.,
list[T]) and an empty list aslist[T] | list[Unknown]rather thanlist[T]. This is inconsistent with how short-circuitedorexpressions are handled already.Version:
pyrefly 1.2.0Reproduction
Sandbox Link
Running:
pyrefly check repro.pyreports that
resulthas typelist[int] | list[Unknown], and theassert_typecall fails.Expected behavior
I would expect
resultto be inferred aslist[int]. The empty-list branch can be contextually typed from thelist[int]branch. Pyright also inferslist[int]for this expression.Empty containers in conditional expressions are common, for example:
Additional context
Pyrefly already performs similar contextual inference for a boolean operation:
An explicit expected type also makes the conditional expression type-check:
This suggests that the conditional-expression branches are currently inferred independently when there is no expected type for the overall expression, whereas boolean-operation operands can provide a contextual hint to later operands.
I could not find an existing issue for this exact conditional-expression case. #4301 appears related because it also concerns contextual typing of an empty container, but it covers a different situation involving an explicit annotation.
Conclusion
This is an extremely common pattern that other type checkers explicitly support. This is not incompatible with pyrefly's first-use policy for inferring type parameters, but would be an additional heuristic that could be layered on top in cases where a gradual type (e.g.,
list[Unknown]) is unioned with an exact type (e.g.,list[int]) as a result of control flow. Pyrefly already does this in certain cases, but the rule doesn't seem to be applied consistently.Whether this should just apply to conditional expressions or also to if-else statements and so on is a design decision that I do not specify here (pyright seems to apply this to statements as well), but I think bringing if-else expressions to parity with short-circuited
orexpressions is a reasonable start.