Agent skill
ha-emby-typing
Use when writing ANY Python code for the HA Emby integration - enforces strict typing with ZERO Any usage, proper type annotations for all functions, TypedDicts for data structures, and mypy strict compliance.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/ha-emby-typing
SKILL.md
Home Assistant Emby Strict Typing
Overview
NEVER use Any. Every type must be explicit and correct.
This integration follows Home Assistant's strict typing requirements. All code must pass mypy with --strict and be added to .strict-typing.
The Iron Law
NO Any TYPE. EVER.
Exceptions for Any:
- None
Not even for:
- "Complex nested data"
- "Third-party library returns Any"
- "It's just kwargs"
- "The type is too complicated"
If you think you need Any, you need a TypedDict, Protocol, or Generic instead.
Type Annotation Requirements
Every Function Must Be Fully Typed
# WRONG - Missing types
def process_media(data):
return data["title"]
# WRONG - Uses Any
def process_media(data: Any) -> Any:
return data["title"]
# CORRECT - Explicit types
def process_media(data: MediaItem) -> str:
return data.title
All Class Attributes Must Be Typed
# WRONG - No type annotations
class EmbyMediaPlayer:
def __init__(self, client, device):
self._client = client
self._device = device
self._state = None
# CORRECT - All types explicit
class EmbyMediaPlayer(MediaPlayerEntity):
_attr_has_entity_name = True
def __init__(
self,
client: EmbyClient,
device: EmbyDevice,
) -> None:
self._client: EmbyClient = client
self._device: EmbyDevice = device
self._state: MediaPlayerState | None = None
TypedDict for API Responses
When Emby API returns JSON dictionaries, define TypedDicts:
from typing import TypedDict, NotRequired
class EmbySessionInfo(TypedDict):
"""Type for Emby session information."""
Id: str
DeviceId: str
DeviceName: str
UserName: str
NowPlayingItem: NotRequired[EmbyNowPlayingItem]
PlayState: NotRequired[EmbyPlayState]
class EmbyNowPlayingItem(TypedDict):
"""Type for currently playing item."""
Id: str
Name: str
Type: str
RunTimeTicks: int
MediaType: str
class EmbyPlayState(TypedDict):
"""Type for playback state."""
PositionTicks: int
IsPaused: bool
IsMuted: bool
VolumeLevel: int
Dataclasses for Internal Models
Convert API responses to typed dataclasses:
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class PlaybackState:
"""Internal representation of playback state."""
position: timedelta
is_paused: bool
is_muted: bool
volume_level: float # 0.0 to 1.0
@classmethod
def from_api(cls, data: EmbyPlayState) -> PlaybackState:
"""Create from API response."""
return cls(
position=timedelta(microseconds=data["PositionTicks"] // 10),
is_paused=data["IsPaused"],
is_muted=data["IsMuted"],
volume_level=data["VolumeLevel"] / 100,
)
Custom ConfigEntry Type
Required for runtime data:
from homeassistant.config_entries import ConfigEntry
from .coordinator import EmbyDataUpdateCoordinator
type EmbyConfigEntry = ConfigEntry[EmbyDataUpdateCoordinator]
Use throughout the integration:
async def async_setup_entry(
hass: HomeAssistant,
entry: EmbyConfigEntry,
) -> bool:
"""Set up Emby from a config entry."""
coordinator = EmbyDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
# ...
Handling Optional Values
Use | None syntax (not Optional):
# WRONG - Old style
from typing import Optional
def get_title(self) -> Optional[str]:
return self._title
# CORRECT - Modern union syntax
def get_title(self) -> str | None:
return self._title
Generic Types
Use generics for containers:
# WRONG - Bare list/dict
def get_sessions() -> list:
...
def get_metadata() -> dict:
...
# CORRECT - Typed containers
def get_sessions() -> list[EmbySession]:
...
def get_metadata() -> dict[str, str]:
...
Callback and Callable Types
from collections.abc import Callable, Awaitable
# Sync callback
StateCallback = Callable[[MediaPlayerState], None]
# Async callback
AsyncStateCallback = Callable[[MediaPlayerState], Awaitable[None]]
# With optional args
UpdateCallback = Callable[[str, dict[str, str] | None], None]
Protocol for Duck Typing
When you need interface-like behavior:
from typing import Protocol
class SupportsPlayback(Protocol):
"""Protocol for objects that support playback."""
async def async_play(self) -> None: ...
async def async_pause(self) -> None: ...
async def async_stop(self) -> None: ...
def control_playback(player: SupportsPlayback) -> None:
"""Control any object supporting playback."""
...
Kwargs Handling
For methods requiring **kwargs (like HA interfaces):
from typing import Unpack
class PlayMediaKwargs(TypedDict, total=False):
"""Kwargs for play_media."""
extra_data: dict[str, str]
thumb: str
async def async_play_media(
self,
media_type: MediaType,
media_id: str,
enqueue: MediaPlayerEnqueue | None = None,
announce: bool | None = None,
**kwargs: Unpack[PlayMediaKwargs],
) -> None:
"""Play media."""
# Access typed kwargs
extra = kwargs.get("extra_data", {})
If true Any kwargs are required by parent interface, use explicit comment:
async def async_play_media(
self,
media_type: MediaType,
media_id: str,
enqueue: MediaPlayerEnqueue | None = None,
announce: bool | None = None,
**kwargs: Any, # Required by MediaPlayerEntity interface
) -> None:
This is the only acceptable use of Any - when overriding a base class method that requires it.
Mypy Configuration
In pyproject.toml:
[tool.mypy]
python_version = "3.12"
strict = true
warn_unreachable = true
enable_error_code = [
"ignore-without-code",
"redundant-cast",
"truthy-bool",
]
[[tool.mypy.overrides]]
module = "embypy.*"
ignore_missing_imports = true
Common Type Errors and Fixes
"has no attribute" on Union
# ERROR: Item "None" has no attribute "title"
def get_title(item: MediaItem | None) -> str:
return item.title # Error!
# FIX: Guard the None case
def get_title(item: MediaItem | None) -> str | None:
if item is None:
return None
return item.title
"Incompatible return type"
# ERROR: list[str] vs list[Any]
def get_sources(self) -> list[str]:
return self._sources # Error if _sources is list[Any]
# FIX: Type the attribute properly
self._sources: list[str] = []
External Library Returns Any
# BAD: Let Any propagate
result = external_lib.get_data() # Returns Any
self._data = result # Now _data is Any
# GOOD: Parse into typed structure immediately
raw = external_lib.get_data()
self._data = parse_to_typed(raw) # Returns TypedDict or dataclass
Red Flags - Type Violations
Stop and fix if you see ANY of these:
Anyin a type annotation# type: ignorewithout error code- Untyped function or method
cast()to bypass type checkingdictorlistwithout type parameters- Variables without type annotations in class
__init__
The Bottom Line
Every type explicit. Zero Any. Mypy strict passes.
If the type is hard to express, that's a sign to create proper TypedDicts, dataclasses, or Protocols.
No shortcuts. No Any. No excuses.
Didn't find tool you were looking for?