# 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 ```