3.2 KiB
Narrowing using hasattr()
The builtin function hasattr() can be used to narrow nominal and structural types. This is
accomplished using an intersection with a synthesized protocol:
from typing import final
from typing_extensions import LiteralString
class NonFinalClass: ...
def _(obj: NonFinalClass):
if hasattr(obj, "spam"):
reveal_type(obj) # revealed: NonFinalClass & <Protocol with members 'spam'>
reveal_type(obj.spam) # revealed: object
else:
reveal_type(obj) # revealed: NonFinalClass & ~<Protocol with members 'spam'>
# error: [unresolved-attribute]
reveal_type(obj.spam) # revealed: Unknown
if hasattr(obj, "not-an-identifier"):
reveal_type(obj) # revealed: NonFinalClass
else:
reveal_type(obj) # revealed: NonFinalClass
For a final class, we recognize that there is no way that an object of FinalClass could ever have
a spam attribute, so the type is narrowed to Never:
@final
class FinalClass: ...
def _(obj: FinalClass):
if hasattr(obj, "spam"):
reveal_type(obj) # revealed: Never
reveal_type(obj.spam) # revealed: Never
else:
reveal_type(obj) # revealed: FinalClass
# error: [unresolved-attribute]
reveal_type(obj.spam) # revealed: Unknown
When the corresponding attribute is already defined on the class, hasattr narrowing does not
change the type. <Protocol with members 'spam'> is a supertype of WithSpam, and so
WithSpam & <Protocol …> simplifies to WithSpam:
class WithSpam:
spam: int = 42
def _(obj: WithSpam):
if hasattr(obj, "spam"):
reveal_type(obj) # revealed: WithSpam
reveal_type(obj.spam) # revealed: int
else:
reveal_type(obj) # revealed: Never
When a class may or may not have a spam attribute, hasattr narrowing can provide evidence that
the attribute exists. Here, no possibly-unbound-attribute error is emitted in the if branch:
def returns_bool() -> bool:
return False
class MaybeWithSpam:
if returns_bool():
spam: int = 42
def _(obj: MaybeWithSpam):
# error: [possibly-unbound-attribute]
reveal_type(obj.spam) # revealed: int
if hasattr(obj, "spam"):
reveal_type(obj) # revealed: MaybeWithSpam & <Protocol with members 'spam'>
reveal_type(obj.spam) # revealed: int
else:
reveal_type(obj) # revealed: MaybeWithSpam & ~<Protocol with members 'spam'>
# TODO: Ideally, we would emit `[unresolved-attribute]` and reveal `Unknown` here:
# error: [possibly-unbound-attribute]
reveal_type(obj.spam) # revealed: int
All attribute available on object are still available on these synthesized protocols, but
attributes that are not present on object are not available:
def f(x: object):
if hasattr(x, "__qualname__"):
reveal_type(x.__repr__) # revealed: bound method object.__repr__() -> str
reveal_type(x.__str__) # revealed: bound method object.__str__() -> str
reveal_type(x.__dict__) # revealed: dict[str, Any]
# error: [unresolved-attribute] "Type `<Protocol with members '__qualname__'>` has no attribute `foo`"
reveal_type(x.foo) # revealed: Unknown