Type Narrowing in Closures #11557
class NameHavingClass:
__slots__ = ("name",)
name: str
class NotNamedClass:
__slots__ = ("someother",)
class Container:
def __init__(self, data: NameHavingClass | NotNamedClass):
self.bar: NameHavingClass | NotNamedClass = data
def foo(container: Container):
assert isinstance(container.bar, NameHavingClass)
something = container.bar.name
# Pyright sees bar as an instance of NameHavingClass and allows it!
func = lambda: container.bar.name
# Pyright complains that bar isn't narrowed
# or still can be an instance of NotNamedClasss
bar = container.bar
# However if we assign the narrowed bar to a variable
func2 = lambda: bar.name
# Pyright can correctly resolve bar to be an instance of
# NameHavingClass and does not complainError: Is this the intended way? |
Replies: 2 comments
|
Yes, this is intended behavior. Pyright retains narrowing inside a closure only for captured variables (simple names), and only when it can prove the variable isn't reassigned on any code path after the closure is defined. This is documented under Narrowing for Captured Variables. The reason for the difference you're seeing:
So the workaround you already discovered — assigning the narrowed member to a local variable first — is exactly the recommended approach: def foo(container: Container):
assert isinstance(container.bar, NameHavingClass)
bar = container.bar # narrowed to NameHavingClass, never reassigned
func = lambda: bar.name # OKThis same restriction applies to any narrowing of member/index expressions captured by a nested function or lambda, not just |
|
Thank you for answering! I get it now. |
Yes, this is intended behavior.
Pyright retains narrowing inside a closure only for captured variables (simple names), and only when it can prove the variable isn't reassigned on any code path after the closure is defined. This is documented under Narrowing for Captured Variables.
The reason for the difference you're seeing:
container.bar.nameinside the lambda —container.baris a member access expression, not a simple captured variable. Narrowing of member expressions is never retained inside closures. A closure can be invoked at any later time, and between theisinstancecheck and the actual call, anything holding a reference tocontainercould reassigncontainer.barto aNotNamedClass…