don’t know if it is really better, and also does not rely on a device card. but locking is an internal behavior, it auto-triggers inside start_shaking(), not something the user calls directly. So splitting into two separate capabilites indeed overexposes it.
A lighter pattern: private protocol + init-time check:
class _CanLockInternally(Protocol):
async def _lock_plate(self) -> None: ...
async def _unlock_plate(self) -> None: ...
class Shaker:
def __init__(self, backend):
self._can_lock = isinstance(backend, _CanLockInternally)
async def start_shaking(self, speed):
if self._can_lock:
await self.backend._lock_plate()
await self.backend.start_shaking(speed)
Non-locking backends just don’t implement the protocol, no dead methods, no supports_X flag. Locking shakers opt in by implementing _lock_plate() / _unlock_plate().
Same pattern works for supports_active_cooling on TemperatureController, anything that’s an internal behavior rather than a user-facing capability.
this would be description instead of prescription as well. which is nice of course, if it is sufficient.