## Summary - Type checkers (and type-checker authors) think in terms of types, but I think most Python users think in terms of values. Rather than saying that a _type_ `X` "has no attribute `foo`" (which I think sounds strange to many users), say that "an object of type `X` has no attribute `foo`" - Special-case certain types so that the diagnostic messages read more like normal English: rather than saying "Type `<class 'Foo'>` has no attribute `bar`" or "Object of type `<class 'Foo'>` has no attribute `bar`", just say "Class `Foo` has no attribute `bar`" ## Test Plan Mdtests and snapshots updated
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-missing-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-missing-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-missing-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] "Object of type `<Protocol with members '__qualname__'>` has no attribute `foo`"
reveal_type(x.foo) # revealed: Unknown