crates/ty_python_semantic/resources/mdtest/statically_known_branches.md
We have the ability to infer precise types and boundness information for symbols that are defined in
branches whose conditions we can statically determine to be always true or always false. This is
useful for sys.version_info branches, which can make new features available based on the Python
version:
If we can statically determine that the condition is always true, then we can also understand that
SomeFeature is always bound, without raising any errors:
import sys
class C:
if sys.version_info >= (3, 9):
SomeFeature: str = "available"
# C.SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
reveal_type(C.SomeFeature) # revealed: str
Another scenario where this is useful is for typing.TYPE_CHECKING branches, which are often used
for conditional imports:
module.py:
class SomeType: ...
main.py:
import typing
if typing.TYPE_CHECKING:
from module import SomeType
# `SomeType` is unconditionally available here for type checkers:
def f(s: SomeType) -> None: ...
This section makes sure that we can handle all commonly encountered patterns of static conditions.
sys.version_info[environment]
python-version = "3.10"
import sys
if sys.version_info >= (3, 11):
greater_equals_311 = True
elif sys.version_info >= (3, 9):
greater_equals_309 = True
else:
less_than_309 = True
if sys.version_info[0] == 2:
python2 = True
# error: [unresolved-reference]
greater_equals_311
# no error
greater_equals_309
# error: [unresolved-reference]
less_than_309
# error: [unresolved-reference]
python2
sys.platform[environment]
python-platform = "linux"
import sys
if sys.platform == "linux":
linux = True
elif sys.platform == "darwin":
darwin = True
else:
other = True
# no error
linux
# error: [unresolved-reference]
darwin
# error: [unresolved-reference]
other
typing.TYPE_CHECKINGimport typing
if typing.TYPE_CHECKING:
type_checking = True
else:
runtime = True
# no error
type_checking
# error: [unresolved-reference]
runtime
sys.platform check and sys.version_info check[environment]
python-version = "3.10"
python-platform = "darwin"
import sys
if sys.platform == "darwin" and sys.version_info >= (3, 11):
only_platform_check_true = True
elif sys.platform == "win32" and sys.version_info >= (3, 10):
only_version_check_true = True
elif sys.platform == "linux" and sys.version_info >= (3, 11):
both_checks_false = True
elif sys.platform == "darwin" and sys.version_info >= (3, 10):
both_checks_true = True
else:
other = True
# error: [unresolved-reference]
only_platform_check_true
# error: [unresolved-reference]
only_version_check_true
# error: [unresolved-reference]
both_checks_false
# no error
both_checks_true
# error: [unresolved-reference]
other
For the rest of this test suite, we will mostly use True and False literals to indicate
statically known conditions, but here, we show that the results are truly based on type inference,
not some special handling of specific conditions in semantic index building. We use two modules to
demonstrate this, since semantic index building is inherently single-module:
module.py:
from typing import Literal
class AlwaysTrue:
def __bool__(self) -> Literal[True]:
return True
from module import AlwaysTrue
if AlwaysTrue():
yes = True
else:
no = True
# no error
yes
# error: [unresolved-reference]
no
The rest of this document contains tests for various control flow elements. This section tests if
statements.
x = 1
if False:
x = 2
reveal_type(x) # revealed: Literal[1]
x = 1
if True:
pass
else:
x = 2
reveal_type(x) # revealed: Literal[1]
x = 1
if True:
x = 2
reveal_type(x) # revealed: Literal[2]
x = 1
if False:
pass
else:
x = 2
reveal_type(x) # revealed: Literal[2]
Just for comparison, we still infer the combined type if the condition is not statically known:
def flag() -> bool:
return True
x = 1
if flag():
x = 2
reveal_type(x) # revealed: Literal[1, 2]
x = 1
if True:
x = 2
else:
x = 3
reveal_type(x) # revealed: Literal[2]
elif branchesdef flag() -> bool:
return True
x = 1
if flag():
x = 2
elif False:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 4]
def flag() -> bool:
return True
x = 1
if flag():
x = 2
elif True:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 3]
def flag() -> bool:
return True
x = 1
if flag():
x = 2
elif flag():
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 3, 4]
elif branches, always falseMake sure that we include bindings from all non-False branches:
def flag() -> bool:
return True
x = 1
if flag():
x = 2
elif flag():
x = 3
elif False:
x = 4
elif False:
x = 5
elif flag():
x = 6
elif flag():
x = 7
else:
x = 8
reveal_type(x) # revealed: Literal[2, 3, 6, 7, 8]
elif branches, always trueMake sure that we only include the binding from the first elif True branch:
def flag() -> bool:
return True
x = 1
if flag():
x = 2
elif flag():
x = 3
elif True:
x = 4
elif True:
x = 5
elif flag():
x = 6
else:
x = 7
reveal_type(x) # revealed: Literal[2, 3, 4]
elif without else branch, always truedef flag() -> bool:
return True
x = 1
if flag():
x = 2
elif True:
x = 3
reveal_type(x) # revealed: Literal[2, 3]
elif without else branch, always falsedef flag() -> bool:
return True
x = 1
if flag():
x = 2
elif False:
x = 3
reveal_type(x) # revealed: Literal[1, 2]
if True inside if Truex = 1
if True:
if True:
x = 2
else:
x = 3
reveal_type(x) # revealed: Literal[2]
if False inside if Truex = 1
if True:
if False:
x = 2
else:
x = 3
reveal_type(x) # revealed: Literal[1]
if <bool> inside if Truedef flag() -> bool:
return True
x = 1
if True:
if flag():
x = 2
else:
x = 3
reveal_type(x) # revealed: Literal[1, 2]
if True inside if <bool>def flag() -> bool:
return True
x = 1
if flag():
if True:
x = 2
else:
x = 3
reveal_type(x) # revealed: Literal[2, 3]
if True inside if False ... elsex = 1
if False:
x = 2
else:
if True:
x = 3
reveal_type(x) # revealed: Literal[3]
if False inside if False ... elsex = 1
if False:
x = 2
else:
if False:
x = 3
reveal_type(x) # revealed: Literal[1]
if <bool> inside if False ... elsedef flag() -> bool:
return True
x = 1
if False:
x = 2
else:
if flag():
x = 3
reveal_type(x) # revealed: Literal[1, 3]
else)if True inside if Truex = 1
if True:
if True:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2]
if False inside if Truex = 1
if True:
if False:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3]
if <bool> inside if Truedef flag() -> bool:
return True
x = 1
if True:
if flag():
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 3]
if True inside if <bool>def flag() -> bool:
return True
x = 1
if flag():
if True:
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[2, 4]
if True inside if False ... elsex = 1
if False:
x = 2
else:
if True:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3]
if False inside if False ... elsex = 1
if False:
x = 2
else:
if False:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[4]
if <bool> inside if False ... elsedef flag() -> bool:
return True
x = 1
if False:
x = 2
else:
if flag():
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3, 4]
try ... exceptif True inside trydef may_raise() -> None: ...
x = 1
try:
may_raise()
if True:
x = 2
else:
x = 3
except:
x = 4
reveal_type(x) # revealed: Literal[2, 4]
try inside if Truedef may_raise() -> None: ...
x = 1
if True:
try:
may_raise()
x = 2
except KeyError:
x = 3
except ValueError:
x = 4
else:
x = 5
reveal_type(x) # revealed: Literal[2, 3, 4]
try with else inside if Truedef may_raise() -> None: ...
x = 1
if True:
try:
may_raise()
x = 2
except KeyError:
x = 3
else:
x = 4
else:
x = 5
reveal_type(x) # revealed: Literal[3, 4]
try with finally inside if Truedef may_raise() -> None: ...
x = 1
if True:
try:
may_raise()
x = 2
except KeyError:
x = 3
else:
x = 4
finally:
x = 5
else:
x = 6
reveal_type(x) # revealed: Literal[5]
for loopsif True inside fordef iterable() -> list[object]:
return [1, ""]
x = 1
for _ in iterable():
x = 2
if True:
x = 3
reveal_type(x) # revealed: Literal[1, 3]
if True inside for ... elsedef iterable() -> list[object]:
return [1, ""]
x = 1
for _ in iterable():
x = 2
else:
if True:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3]
for inside if Truedef iterable() -> list[object]:
return [1, ""]
x = 1
if True:
for _ in iterable():
x = 2
else:
x = 3
reveal_type(x) # revealed: Literal[1, 2]
for ... else inside if Truedef iterable() -> list[object]:
return [1, ""]
x = 1
if True:
for _ in iterable():
x = 2
else:
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[3]
for loop with break inside if Truedef iterable() -> list[object]:
return [1, ""]
x = 1
if True:
x = 2
for _ in iterable():
x = 3
break
else:
x = 4
else:
x = 5
reveal_type(x) # revealed: Literal[3, 4]
Note that the result type of an if-expression can be precisely inferred if the condition is
statically known. This is a plain type inference feature that does not need support for statically
known branches. The tests for this feature are in expression/if.md.
The tests here make sure that we also handle assignment expressions inside if-expressions
correctly.
x = (y := 1) if True else (y := 2)
reveal_type(x) # revealed: Literal[1]
reveal_type(y) # revealed: Literal[1]
x = (y := 1) if False else (y := 2)
reveal_type(x) # revealed: Literal[2]
reveal_type(y) # revealed: Literal[2]
or(x := 1) or (x := 2)
reveal_type(x) # revealed: Literal[1]
(y := 1) or (y := 2) or (y := 3) or (y := 4)
reveal_type(y) # revealed: Literal[1]
and(x := 1) and (x := 2)
reveal_type(x) # revealed: Literal[2]
(y := 1) and (y := 2) and (y := 3) and (y := 4)
reveal_type(y) # revealed: Literal[4]
or(x := 0) or (x := 1)
reveal_type(x) # revealed: Literal[1]
(y := 0) or (y := 0) or (y := 1) or (y := 2)
reveal_type(y) # revealed: Literal[1]
and(x := 0) and (x := 1)
reveal_type(x) # revealed: Literal[0]
(y := 0) and (y := 1) and (y := 2) and (y := 3)
reveal_type(y) # revealed: Literal[0]
x = 1
while False:
x = 2
reveal_type(x) # revealed: Literal[1]
x = 1
while True:
x = 2
break
reveal_type(x) # revealed: Literal[2]
Make sure that we still infer the combined type if the condition is not statically known:
def flag() -> bool:
return True
x = 1
while flag():
x = 2
reveal_type(x) # revealed: Literal[1, 2]
while ... elsewhile Falsewhile False:
x = 1
else:
x = 2
reveal_type(x) # revealed: Literal[2]
while False with breakx = 1
while False:
x = 2
break
x = 3
else:
x = 4
reveal_type(x) # revealed: Literal[4]
while Truewhile True:
x = 1
break
else:
x = 2
reveal_type(x) # revealed: Literal[1]
if nested inside while TrueThese are regression test for https://github.com/astral-sh/ty/issues/365. First, make sure that we do not panic in the original scenario:
def flag() -> bool:
return True
while True:
if flag():
break
else:
c = 1
break
c # error: [possibly-unresolved-reference]
And also check that we understand control flow correctly:
c = 1
while True:
if False:
c = 2
break
break
reveal_type(c) # revealed: Literal[1]
match statements[environment]
python-version = "3.10"
x = 1
match "a":
case "a":
x = 2
case "b":
x = 3
reveal_type(x) # revealed: Literal[2]
x = 1
match "a":
case "a":
x = 2
case "b":
x = 3
case _:
pass
reveal_type(x) # revealed: Literal[2]
Make sure we don't infer a static truthiness in case there is a case guard:
def flag() -> bool:
return True
x = 1
match "a":
case "a" if flag():
x = 2
case "b":
x = 3
case _:
pass
reveal_type(x) # revealed: Literal[1, 2]
x = 1
match "something else":
case "a":
x = 2
case "b":
x = 3
reveal_type(x) # revealed: Literal[1]
x = 1
match "something else":
case "a":
x = 2
case "b":
x = 3
case _:
pass
reveal_type(x) # revealed: Literal[1]
For definitely-false cases, the presence of a guard has no influence:
def flag() -> bool:
return True
x = 1
match "something else":
case "a" if flag():
x = 2
case "b":
x = 3
case _:
pass
reveal_type(x) # revealed: Literal[1]
def _(s: str):
match s:
case "a":
x = 1
case "b":
x = 2
case _:
x = 3
reveal_type(x) # revealed: Literal[1, 2, 3]
sys.platform[environment]
python-platform = "darwin"
python-version = "3.10"
import sys
match sys.platform:
case "linux":
linux = True
case "darwin":
darwin = True
case "win32":
win32 = True
case _:
other = True
# error: [unresolved-reference]
linux
# no error
darwin
# error: [unresolved-reference]
win32
# error: [unresolved-reference]
other
sys.version_info[environment]
python-version = "3.13"
import sys
minor = "too old"
match sys.version_info.minor:
case 12:
minor = 12
case 13:
minor = 13
case _:
pass
reveal_type(minor) # revealed: Literal[13]
if Falsex: str
if False:
x: int
def f() -> None:
reveal_type(x) # revealed: str
if True … elsex: str
if True:
pass
else:
x: int
def f() -> None:
reveal_type(x) # revealed: str
if Truemod.py:
x: str
if True:
x: int
main.py:
from mod import x
reveal_type(x) # revealed: int
if False … elsemod.py:
x: str
if False:
pass
else:
x: int
main.py:
from mod import x
reveal_type(x) # revealed: int
mod.py:
def flag() -> bool:
return True
x: str
if flag():
x: int
main.py:
from mod import x
reveal_type(x) # revealed: str | int
def f() -> int:
return 1
def g() -> int:
return 1
if True:
def f() -> str:
return ""
else:
def g() -> str:
return ""
reveal_type(f()) # revealed: str
reveal_type(g()) # revealed: int
if True:
class C:
x: int = 1
else:
class C:
x: str = "a"
reveal_type(C.x) # revealed: int
class C:
if True:
x: int = 1
else:
x: str = "a"
reveal_type(C.x) # revealed: int
if Falseif False:
x = 1
# error: [unresolved-reference]
x
if True … elseif True:
pass
else:
x = 1
# error: [unresolved-reference]
x
if Trueif True:
x = 1
# x is always bound, no error
x
if False … elseif False:
pass
else:
x = 1
# x is always bound, no error
x
For comparison, we still detect definitions inside non-statically known branches as possibly unbound:
def flag() -> bool:
return True
if flag():
x = 1
# error: [possibly-unresolved-reference]
x
def flag() -> bool:
return True
if False:
if True:
unbound1 = 1
if True:
if False:
unbound2 = 1
if False:
if False:
unbound3 = 1
if False:
if flag():
unbound4 = 1
if flag():
if False:
unbound5 = 1
# error: [unresolved-reference]
# error: [unresolved-reference]
# error: [unresolved-reference]
# error: [unresolved-reference]
# error: [unresolved-reference]
(unbound1, unbound2, unbound3, unbound4, unbound5)
if False:
x = 1
if True:
x = 2
# x is always bound, no error
x
if False:
y = 1
if True:
y = 2
# y is always bound, no error
y
if False:
z = 1
if False:
z = 2
# z is never bound:
# error: [unresolved-reference]
z
if True:
x = 1
def f():
# x is always bound, no error
x
module.py:
if False:
symbol = 1
# error: [unresolved-import]
from module import symbol
reveal_type(symbol) # revealed: Unknown
module.py:
if True:
symbol = 1
# no error
from module import symbol
module.py:
def flag() -> bool:
return True
if flag():
symbol = 1
# error: [possibly-missing-import]
from module import symbol
module.py:
if False:
symbol: int
# error: [unresolved-import]
from module import symbol
reveal_type(symbol) # revealed: Unknown
module.py:
if True:
symbol: int
# no error
from module import symbol
When a non-definitely bound symbol is used as a (part of a) condition, we always infer an ambiguous
truthiness. If we didn't do that, x would be considered definitely bound in the following example:
def _(flag: bool):
if flag:
ALWAYS_TRUE_IF_BOUND = True
# error: [possibly-unresolved-reference] "Name `ALWAYS_TRUE_IF_BOUND` used when possibly not defined"
if True and ALWAYS_TRUE_IF_BOUND:
x = 1
# no error, x is considered definitely bound
x
def _(flag: bool):
if flag:
ALWAYS_TRUE_IF_BOUND = True
# error: [possibly-unresolved-reference] "Name `ALWAYS_TRUE_IF_BOUND` used when possibly not defined"
if True and ALWAYS_TRUE_IF_BOUND:
x = 1
else:
x = 2
# If `ALWAYS_TRUE_IF_BOUND` were not defined, an error would occur, and therefore the `x = 2` branch would never be executed.
reveal_type(x) # revealed: Literal[1]
A closely related feature is the ability to detect unreachable code. For example, we do not emit a diagnostic here:
if False:
x
See unreachable.md for more tests on this topic.