Back to Ruff

Statically-known branches

crates/ty_python_semantic/resources/mdtest/statically_known_branches.md

0.15.1221.0 KB
Original Source

Statically-known branches

Introduction

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:

py
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:

py
class SomeType: ...

main.py:

py
import typing

if typing.TYPE_CHECKING:
    from module import SomeType

# `SomeType` is unconditionally available here for type checkers:
def f(s: SomeType) -> None: ...

Common use cases

This section makes sure that we can handle all commonly encountered patterns of static conditions.

sys.version_info

toml
[environment]
python-version = "3.10"
py
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

toml
[environment]
python-platform = "linux"
py
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_CHECKING

py
import typing

if typing.TYPE_CHECKING:
    type_checking = True
else:
    runtime = True

# no error
type_checking

# error: [unresolved-reference]
runtime

Combination of sys.platform check and sys.version_info check

toml
[environment]
python-version = "3.10"
python-platform = "darwin"
py
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

Based on type inference

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:

py
from typing import Literal

class AlwaysTrue:
    def __bool__(self) -> Literal[True]:
        return True
py
from module import AlwaysTrue

if AlwaysTrue():
    yes = True
else:
    no = True

# no error
yes

# error: [unresolved-reference]
no

If statements

The rest of this document contains tests for various control flow elements. This section tests if statements.

Always false

If

py
x = 1

if False:
    x = 2

reveal_type(x)  # revealed: Literal[1]

Else

py
x = 1

if True:
    pass
else:
    x = 2

reveal_type(x)  # revealed: Literal[1]

Always true

If

py
x = 1

if True:
    x = 2

reveal_type(x)  # revealed: Literal[2]

Else

py
x = 1

if False:
    pass
else:
    x = 2

reveal_type(x)  # revealed: Literal[2]

Ambiguous

Just for comparison, we still infer the combined type if the condition is not statically known:

py
def flag() -> bool:
    return True

x = 1

if flag():
    x = 2

reveal_type(x)  # revealed: Literal[1, 2]

Combination of always true and always false

py
x = 1

if True:
    x = 2
else:
    x = 3

reveal_type(x)  # revealed: Literal[2]

elif branches

Always false

py
def flag() -> bool:
    return True

x = 1

if flag():
    x = 2
elif False:
    x = 3
else:
    x = 4

reveal_type(x)  # revealed: Literal[2, 4]

Always true

py
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]

Ambiguous

py
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]

Multiple elif branches, always false

Make sure that we include bindings from all non-False branches:

py
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]

Multiple elif branches, always true

Make sure that we only include the binding from the first elif True branch:

py
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 true

py
def 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 false

py
def flag() -> bool:
    return True

x = 1

if flag():
    x = 2
elif False:
    x = 3

reveal_type(x)  # revealed: Literal[1, 2]

Nested conditionals

if True inside if True

py
x = 1

if True:
    if True:
        x = 2
else:
    x = 3

reveal_type(x)  # revealed: Literal[2]

if False inside if True

py
x = 1

if True:
    if False:
        x = 2
else:
    x = 3

reveal_type(x)  # revealed: Literal[1]

if <bool> inside if True

py
def 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>

py
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 ... else

py
x = 1

if False:
    x = 2
else:
    if True:
        x = 3

reveal_type(x)  # revealed: Literal[3]

if False inside if False ... else

py
x = 1

if False:
    x = 2
else:
    if False:
        x = 3

reveal_type(x)  # revealed: Literal[1]

if <bool> inside if False ... else

py
def flag() -> bool:
    return True

x = 1

if False:
    x = 2
else:
    if flag():
        x = 3

reveal_type(x)  # revealed: Literal[1, 3]

Nested conditionals (with inner else)

if True inside if True

py
x = 1

if True:
    if True:
        x = 2
    else:
        x = 3
else:
    x = 4

reveal_type(x)  # revealed: Literal[2]

if False inside if True

py
x = 1

if True:
    if False:
        x = 2
    else:
        x = 3
else:
    x = 4

reveal_type(x)  # revealed: Literal[3]

if <bool> inside if True

py
def 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>

py
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 ... else

py
x = 1

if False:
    x = 2
else:
    if True:
        x = 3
    else:
        x = 4

reveal_type(x)  # revealed: Literal[3]

if False inside if False ... else

py
x = 1

if False:
    x = 2
else:
    if False:
        x = 3
    else:
        x = 4

reveal_type(x)  # revealed: Literal[4]

if <bool> inside if False ... else

py
def 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]

Combination with non-conditional control flow

try ... except

if True inside try
py
def 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 True
py
def 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 True
py
def 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 True
py
def 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 loops

if True inside for
py
def 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 ... else
py
def 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 True
py
def 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 True
py
def 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 True
py
def 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]

If expressions

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.

Type inference

Always true

py
x = (y := 1) if True else (y := 2)

reveal_type(x)  # revealed: Literal[1]
reveal_type(y)  # revealed: Literal[1]

Always false

py
x = (y := 1) if False else (y := 2)

reveal_type(x)  # revealed: Literal[2]
reveal_type(y)  # revealed: Literal[2]

Boolean expressions

Always true, or

py
(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]

Always true, and

py
(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]

Always false, or

py
(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]

Always false, and

py
(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]

While loops

Always false

py
x = 1

while False:
    x = 2

reveal_type(x)  # revealed: Literal[1]

Always true

py
x = 1

while True:
    x = 2
    break

reveal_type(x)  # revealed: Literal[2]

Ambiguous

Make sure that we still infer the combined type if the condition is not statically known:

py
def flag() -> bool:
    return True

x = 1

while flag():
    x = 2

reveal_type(x)  # revealed: Literal[1, 2]

while ... else

while False

py
while False:
    x = 1
else:
    x = 2

reveal_type(x)  # revealed: Literal[2]

while False with break

py
x = 1
while False:
    x = 2
    break
    x = 3
else:
    x = 4

reveal_type(x)  # revealed: Literal[4]

while True

py
while True:
    x = 1
    break
else:
    x = 2

reveal_type(x)  # revealed: Literal[1]

if nested inside while True

These are regression test for https://github.com/astral-sh/ty/issues/365. First, make sure that we do not panic in the original scenario:

py
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:

py
c = 1

while True:
    if False:
        c = 2
        break
    break

reveal_type(c)  # revealed: Literal[1]

match statements

toml
[environment]
python-version = "3.10"

Single-valued types, always true

py
x = 1

match "a":
    case "a":
        x = 2
    case "b":
        x = 3

reveal_type(x)  # revealed: Literal[2]

Single-valued types, always true, with wildcard pattern

py
x = 1

match "a":
    case "a":
        x = 2
    case "b":
        x = 3
    case _:
        pass

reveal_type(x)  # revealed: Literal[2]

Single-valued types, always true, with guard

Make sure we don't infer a static truthiness in case there is a case guard:

py
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]

Single-valued types, always false

py
x = 1

match "something else":
    case "a":
        x = 2
    case "b":
        x = 3

reveal_type(x)  # revealed: Literal[1]

Single-valued types, always false, with wildcard pattern

py
x = 1

match "something else":
    case "a":
        x = 2
    case "b":
        x = 3
    case _:
        pass

reveal_type(x)  # revealed: Literal[1]

Single-valued types, always false, with guard

For definitely-false cases, the presence of a guard has no influence:

py
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]

Non-single-valued types

py
def _(s: str):
    match s:
        case "a":
            x = 1
        case "b":
            x = 2
        case _:
            x = 3

    reveal_type(x)  # revealed: Literal[1, 2, 3]

Matching on sys.platform

toml
[environment]
python-platform = "darwin"
python-version = "3.10"
py
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

Matching on sys.version_info

toml
[environment]
python-version = "3.13"
py
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]

Conditional declarations

Always false

if False

py
x: str

if False:
    x: int

def f() -> None:
    reveal_type(x)  # revealed: str

if True … else

py
x: str

if True:
    pass
else:
    x: int

def f() -> None:
    reveal_type(x)  # revealed: str

Always true

if True

mod.py:

py
x: str

if True:
    x: int

main.py:

py
from mod import x

reveal_type(x)  # revealed: int

if False … else

mod.py:

py
x: str

if False:
    pass
else:
    x: int

main.py:

py
from mod import x

reveal_type(x)  # revealed: int

Ambiguous

mod.py:

py
def flag() -> bool:
    return True

x: str

if flag():
    x: int

main.py:

py
from mod import x

reveal_type(x)  # revealed: str | int

Conditional function definitions

py
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

Conditional class definitions

py
if True:
    class C:
        x: int = 1

else:
    class C:
        x: str = "a"

reveal_type(C.x)  # revealed: int

Conditional class attributes

py
class C:
    if True:
        x: int = 1
    else:
        x: str = "a"

reveal_type(C.x)  # revealed: int

(Un)boundness

Unbound, if False

py
if False:
    x = 1

# error: [unresolved-reference]
x

Unbound, if True … else

py
if True:
    pass
else:
    x = 1

# error: [unresolved-reference]
x

Bound, if True

py
if True:
    x = 1

# x is always bound, no error
x

Bound, if False … else

py
if False:
    pass
else:
    x = 1

# x is always bound, no error
x

Ambiguous, possibly unbound

For comparison, we still detect definitions inside non-statically known branches as possibly unbound:

py
def flag() -> bool:
    return True

if flag():
    x = 1

# error: [possibly-unresolved-reference]
x

Nested conditionals

py
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)

Chained conditionals

py
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

Public boundness

py
if True:
    x = 1

def f():
    # x is always bound, no error
    x

Imports of conditionally defined symbols

Always false, unbound

module.py:

py
if False:
    symbol = 1
py
# error: [unresolved-import]
from module import symbol

reveal_type(symbol)  # revealed: Unknown

Always true, bound

module.py:

py
if True:
    symbol = 1
py
# no error
from module import symbol

Ambiguous, possibly unbound

module.py:

py
def flag() -> bool:
    return True

if flag():
    symbol = 1
py
# error: [possibly-missing-import]
from module import symbol

Always false, undeclared

module.py:

py
if False:
    symbol: int
py
# error: [unresolved-import]
from module import symbol

reveal_type(symbol)  # revealed: Unknown

Always true, declared

module.py:

py
if True:
    symbol: int
py
# no error
from module import symbol

Non-definitely bound symbols in conditions

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:

py
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
py
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]

Unreachable code

A closely related feature is the ability to detect unreachable code. For example, we do not emit a diagnostic here:

py
if False:
    x

See unreachable.md for more tests on this topic.