Skip to content

Type narrowing fails to account for reassignment in a conditional based on an intermediate variable (aliased conditional expressions) #719

@matthewlloyd

Description

@matthewlloyd

Summary

Ty fails to correctly narrow the type of a variable when a is None check is stored in an intermediate boolean variable, and the original variable is then reassigned within a conditional block that uses that boolean.

In the example below, int_or_none starts as int | None. A boolean is_none is created to store the result of int_or_none is None. Inside an if is_none: block, int_or_none is reassigned to an int. After this conditional block, the type of int_or_none is guaranteed to be int. However, Ty still considers its type to be int | None, leading to a false positive invalid-argument-type error.

Other type checkers like Pyright correctly narrow the type in this scenario.

Minimal Reproducible Example

import random


def return_int_or_none() -> int | None:
    if random.randint(0, 1):
        return 1
    return None

def take_int_only(must_be_int: int):
    pass

int_or_none: int | None = return_int_or_none()
is_none = int_or_none is None
if is_none:
    int_or_none = 1

# At this point, int_or_none is guaranteed to be an `int`.
# If it was None, it was reassigned to 1.
# If it was an int, it remains an int.
take_int_only(int_or_none)

Current Behavior (Ty's Output)

Ty incorrectly reports an error, failing to narrow the type of int_or_none.

$ uvx ty check test.py
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
error[invalid-argument-type]: Argument to function `take_int_only` is incorrect
  --> test.py:17:15
   |
15 |     int_or_none = 1
16 | # At this point, int_or_none is guaranteed to be an `int`.
17 | take_int_only(int_or_none)
   |               ^^^^^^^^^^^ Expected `int`, found `int | None`
   |
info: Function defined here
  --> test.py:9:5
   |
 7 |     return None
 8 |
 9 | def take_int_only(must_be_int: int):
   |     ^^^^^^^^^^^^^ ---------------- Parameter declared here
10 |     pass
   |
info: rule `invalid-argument-type` is enabled by default

Found 1 diagnostic

Expected Behavior

Ty should recognize that after the conditional block, the variable int_or_none can only be of type int and should not raise an error.

For comparison, Pyright correctly handles this control flow and reports no errors:

$ pyright test.py
0 errors, 0 warnings, 0 informations

Command and Settings

The command used was uvx ty check <filename>.py with default settings.

Playground Link

This issue can be reproduced in the Ty playground here:

https://play.ty.dev/eba9c0b6-0443-492f-be34-b8eea7d795c2

Version

ty 0.0.1-alpha.12

Metadata

Metadata

Assignees

No one assigned

    Labels

    narrowingrelated to flow-sensitive type narrowing

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions