# 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: ```py from typing import final from typing_extensions import LiteralString class NonFinalClass: ... def _(obj: NonFinalClass): if hasattr(obj, "spam"): reveal_type(obj) # revealed: NonFinalClass & reveal_type(obj.spam) # revealed: object else: reveal_type(obj) # revealed: NonFinalClass & ~ # 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`: ```py @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. `` is a supertype of `WithSpam`, and so `WithSpam & ` simplifies to `WithSpam`: ```py 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: ```py 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 & reveal_type(obj.spam) # revealed: int else: reveal_type(obj) # revealed: MaybeWithSpam & ~ # 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: ```py 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 `` has no attribute `foo`" reveal_type(x.foo) # revealed: Unknown ```