Home Assistant uses two different patterns for entity attributes that can be confusing and lead to subtle bugs. This guide explains these patterns to help you avoid common pitfalls when developing integrations.
For most entity attributes (state, name, icon, etc.), Home Assistant uses protected attributes with an _attr_
prefix. These are managed by the CachedProperties
metaclass which provides automatic caching and invalidation:
class MyEntity(Entity):
"""My entity implementation."""
# Protected attributes with _attr_ prefix
_attr_name: str | None = None
_attr_icon: str | None = None
_attr_device_class: str | None = None
_attr_extra_state_attributes: dict[str, Any] = {}
The CachedProperties
metaclass:
- Automatically creates property getters/setters for
_attr_*
attributes - Manages caching of property values
- Invalidates cache when attributes are modified
- Handles type annotations correctly
While most attributes use the protected _attr_
pattern, there are a few special cases that use direct public attributes:
entity_description
: The primary example, used for storing entity descriptionsunique_id
: In some cases, used for direct entity identificationplatform
: Used to identify the platform an entity belongs toregistry_entry
: Used for entity registry entrieshass
: Reference to the Home Assistant instance
Example:
# These are set directly without _attr_ prefix
self.entity_description = description
self.unique_id = f"{serial_number}_{entity_id}"
self.platform = platform
The reason these attributes are public varies:
- They represent fundamental identity or configuration that shouldn't be overridden
- They are part of the public API contract
- They are frequently accessed by the core framework
- They are used in property getter fallback chains
When extending an entity description with custom attributes, type checkers will often complain when you try to access the custom attributes. This is because the type system only sees the base class type (e.g., BinarySensorEntityDescription
), not your custom type.
# Your custom entity description class with added attributes
@dataclass(frozen=True)
class MyCustomEntityDescription(BinarySensorEntityDescription):
"""Custom entity description with extra attributes."""
value_fn: Callable[[Any], bool] # Custom attribute
# Your entity class
class MyEntity(BinarySensorEntity):
def __init__(self, description: MyCustomEntityDescription):
self.entity_description = description # Type is seen as BinarySensorEntityDescription
def update(self):
# Type error! BinarySensorEntityDescription has no attribute 'value_fn'
result = self.entity_description.value_fn(self.data)
There are several ways to handle this typing issue, each with their own advantages:
The cleanest solution is to store direct references to the custom attributes during initialization:
def __init__(self, description: MyCustomEntityDescription):
super().__init__()
self.entity_description = description
# Store a direct reference to value_fn to avoid type issues later
self._value_fn = description.value_fn
def update(self):
# Use the directly stored reference - no type issues!
result = self._value_fn(self.data)
This approach:
- Works correctly even with optimized Python (
-O
flag) - Has no runtime overhead
- Keeps code clean and readable
- Preserves proper type information
For cases where storing a direct reference isn't feasible, use typing.cast
:
from typing import cast
def update(self):
# Cast to our specific type for type-checking - this has no runtime overhead
description = cast(MyCustomEntityDescription, self.entity_description)
result = description.value_fn(self.data)
This approach:
- Satisfies the type checker
- Has zero runtime overhead (cast is removed during compilation)
- Doesn't protect against actual type errors at runtime
Create helper properties or methods that handle the typing:
@property
def my_description(self) -> MyCustomEntityDescription:
"""Return the entity description as the specific type."""
return self.entity_description # type: ignore[return-value]
def update(self):
result = self.my_description.value_fn(self.data)
❌ Do not use assertions for type checking:
def update(self):
description = self.entity_description
assert isinstance(description, MyCustomEntityDescription) # BAD PRACTICE!
result = description.value_fn(self.data)
This approach is problematic because:
- Assertions are completely removed when Python runs with optimizations enabled (
-O
flag) - This can lead to runtime errors in production environments
- Security linters like Bandit will flag this as a vulnerability (B101)
- Use
self._attr_*
for most entity attributes (name, state, device_class, etc.) - Use
self.entity_description
specifically for the entity description
The most common mistake is using self._attr_entity_description = description
instead of self.entity_description = description
.
This can cause subtle bugs because:
- The entity will initialize without errors
- Basic functionality might work
- But properties that fall back to the entity description (like device_class) won't work correctly
- Runtime errors may occur when trying to access methods or properties of the entity description
# INCORRECT - Will cause bugs
def __init__(self, coordinator, description):
super().__init__(coordinator)
self._attr_entity_description = description # WRONG!
self._attr_device_class = description.device_class
# CORRECT
def __init__(self, coordinator, description):
super().__init__(coordinator)
self.entity_description = description # Correct!
self._attr_device_class = description.device_class # This is also correct
Understanding how Home Assistant uses entity_description
internally helps explain why it's treated differently:
# From Home Assistant's Entity class
@cached_property
def device_class(self) -> str | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
return self._attr_device_class
if hasattr(self, "entity_description"): # Fallback to entity_description
return self.entity_description.device_class
return None
This pattern appears throughout Home Assistant's code. The framework first checks the direct attribute, then falls back to the entity description if available.
Home Assistant's approach evolved over time:
- Historical Evolution: Older code used direct attributes, newer code uses the
_attr_
pattern - Special Role:
entity_description
serves as a container of defaults and is a public API - Cached Properties: The
_attr_
pattern works with Home Assistant's property caching system - Fallback Chain: Property getters use a fallback chain:
_attr_*
→entity_description.*
→ default
Home Assistant likely uses a public attribute for entity_description
for several reasons:
- API Contract: The entity description represents a public API contract that is meant to be preserved and directly accessed
- Composition vs. Inheritance: It emphasizes composition (an entity has a description) rather than inheritance (an entity is a description)
- Interoperability: Allows for more flexible interoperability between integrations and the core framework
- Serialization: May facilitate easier serialization/deserialization when needed
- Accessor Pattern: Other parts of Home Assistant can access the description directly without needing accessor methods
The inconsistency between entity_description
and other _attr_*
attributes may simply be an architectural decision made at different points in Home Assistant's development history.
-
Use
self._attr_*
for entity attributes - This automatically gets you:- Protected attribute storage
- Cached property getters/setters (via the
CachedProperties
metaclass) - Proper type annotation handling
- Automatic cache invalidation
-
Use
self.entity_description
(neverself._attr_entity_description
) for entity descriptions -
When extending
Entity
classes:- Check the parent class implementation to understand the attribute pattern
- Use the same pattern as the parent class for consistency
- Include proper type annotations to help catch issues earlier
-
For custom entity descriptions:
- Store direct references to custom description attributes in your entity's
__init__
method - Use proper type annotations to avoid type checker issues
- Test property access, especially for device_class and other properties that might come from entity_description
- Store direct references to custom description attributes in your entity's
-
For custom properties (when you need something beyond the standard
_attr_*
pattern):@cached_property def custom_property(self) -> str: """Return a computed property value.""" return self._compute_value()
Home Assistant's dual attribute pattern can be confusing, but following these guidelines will help avoid subtle bugs:
- Use
self._attr_*
for most attributes (this automatically includes caching) - Use
self.entity_description
(no underscore prefix) for the entity description - Store direct references to custom description attributes to avoid type issues
This inconsistency in the framework's design is unfortunately something developers need to be aware of when building integrations.