microsoft / microsoft/pyright

Inconsistent narrowing into nested function when using TypeIs guard stored in a local variable

Open
#10,951 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
Python
Stars
15.6k
Forks
1.8k
Avg merge
12h 13m
Merged PRs (30d)
52

Description

**Describe the bug**

There's inconsistent type narrowing behavior when a `TypeIs` typeguard is assigned to a variable, and then referenced from within a nested function definition inside a conditional that references the typeguard variable.

Consider the following code:

```py
from typing import TypeIs

def is_string(value: str | int) -> TypeIs[str]:
return isinstance(value, str)

def use_str(value: str | int):
is_str = is_string(value)

if is_str:
upper = value.upper() # No Type Error
```

This works as intended—there is no type error because we have narrowed to `str` before calling `value.upper()`.

Now, let's wrap the code in a nested function:

```py
def use_str_nested(value: str | int):
is_str = is_string(value)

if is_str:
def inner():
upper = value.upper() # Type error: Cannot access attribute "upper" for class "int"
```

Now that value is inside a nested function, Pyright does not narrow `value` to `str` so that there is a type error.

This only occurs if the type guard is referenced via variable assignment. The following code does not produce a type error:

```py
def nested_no_assignment(value: str | int):
if is_string(value):
def inner():
upper = value.upper() # No type error
```

Interestingly, making an unused reference to the narrowed value within the if block but outside of the nested function also addresses the issue:

```py
def nested_unused_variable(value: str | int):
is_str = is_string(value)

if is_str:
value # This unused reference fixes the type error.
def inner():
upper = value.upper() # No type error
```

[Here is a playground link containing all this example code](https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMoAqiApgJIDOAUJQCbHCbkD65MIqaAFAG4CGANgFdiALiisQUAD6YUMAJRQAtAD5CJCgG0JAXRGUohqCGIxBIFI1SteKAMbEeA4QBpxbedToNB5YizYnIVF3SRlUBX0jRgDJAF4YiQ4g4U9ogyMkBiRmCTFoowyCqEEEBGJ4qD5ggDpS8pBOT1p6Er9YphRiVmIaFJCJaVlIosS2KASc2OTq1NHRrLGQKOKob1kuxvkV1aN6iomq52I6soqmqABidXKoCvBlqABhWxQwfF47B3JyKF4YNhIABGghgxCgACJ9iAIVBgLgoHZ%2BLwfpCIhCvK0uj0aJ0wEwUeQkGgUBBiHJ%2BmJBuE5Nt0tFFlMkuh%2BttRtF1qhNk0drsSmdKrMTtCLtcAHJgWAkO4gB5QTEMbFg3GCFC%2BXpMPjsXhA-iOIVU8Y0kYM3LjSZm9gsoVpQoM7KW3nRIVXQgACxyJTVfhoxnoFXJDjhSAAHt1YG7wfBbvdcDV2UZOShuWy%2BXsBYchacGqKoBKpTHZbhqEA)

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the three examples in the linked Pyright playground and compare narrowing inside and outside the nested function. Done means the TypeIs guard stored in a local variable narrows value within inner, while the direct-call and unused-reference cases continue to behave consistently.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.