ruff/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md
2025-05-03 19:49:15 +02:00

19 KiB

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:

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

Common use cases

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_CHECKING

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

[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

Based on type inference

For the 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

If statements

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

Always false

If

x = 1

if False:
    x = 2

reveal_type(x)  # revealed: Literal[1]

Else

x = 1

if True:
    pass
else:
    x = 2

reveal_type(x)  # revealed: Literal[1]

Always true

If

x = 1

if True:
    x = 2

reveal_type(x)  # revealed: Literal[2]

Else

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:

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

x = 1

if True:
    x = 2
else:
    x = 3

reveal_type(x)  # revealed: Literal[2]

elif branches

Always false

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

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

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:

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:

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

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

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

x = 1

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

reveal_type(x)  # revealed: Literal[2]

if False inside if True

x = 1

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

reveal_type(x)  # revealed: Literal[1]

if <bool> inside if True

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>

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

x = 1

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

reveal_type(x)  # revealed: Literal[3]

if False inside if False ... else

x = 1

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

reveal_type(x)  # revealed: Literal[1]

if <bool> inside if False ... else

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

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

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

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>

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

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

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

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

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

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

Always false

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

(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

(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

(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

(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

x = 1

while False:
    x = 2

reveal_type(x)  # revealed: Literal[1]

Always true

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:

def flag() -> bool:
    return True

x = 1

while flag():
    x = 2

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

while ... else

while False

while False:
    x = 1
else:
    x = 2

reveal_type(x)  # revealed: Literal[2]

while False with break

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

reveal_type(x)  # revealed: Literal[4]

while True

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

reveal_type(x)  # revealed: Literal[1]

match statements

[environment]
python-version = "3.10"

Single-valued types, always true

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

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:

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

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

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:

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

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

[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

Matching on 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]

Conditional declarations

Always false

if False

x: str

if False:
    x: int

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

if True … else

x: str

if True:
    pass
else:
    x: int

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

Always true

if True

x: str

if True:
    x: int

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

if False … else

x: str

if False:
    pass
else:
    x: int

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

Ambiguous

def flag() -> bool:
    return True

x: str

if flag():
    x: int

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

Conditional function definitions

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

if True:
    class C:
        x: int = 1

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

reveal_type(C.x)  # revealed: int

Conditional class attributes

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

reveal_type(C.x)  # revealed: int

(Un)boundness

Unbound, if False

if False:
    x = 1

# error: [unresolved-reference]
x

Unbound, if True … else

if True:
    pass
else:
    x = 1

# error: [unresolved-reference]
x

Bound, if True

if True:
    x = 1

# x is always bound, no error
x

Bound, if False … else

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:

def flag() -> bool:
    return True

if flag():
    x = 1

# error: [possibly-unresolved-reference]
x

Nested conditionals

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

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

if True:
    x = 1

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

Imports of conditionally defined symbols

Always false, unbound

module.py:

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

Always true, bound

module.py:

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

Ambiguous, possibly unbound

module.py:

def flag() -> bool:
    return True

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

Always false, undeclared

module.py:

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

reveal_type(symbol)  # revealed: Unknown

Always true, declared

module.py:

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

Unreachable code

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.