Inconsistent narrowing into nested function when using TypeIs guard stored in a local variable
- 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
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