I find Protocols very useful to be able statically validate behavior expectations -- without the constraints of actual subclassing.
For checking for attribute existence, they work like a charm:
from typing import Protocol, Callable, runtime_checkable
from collections import namedtuple
class HasItems(Protocol):
items: Callable
assert isinstance(dict(), HasItems)
assert not isinstance(list(), HasItems)
But the type of the attribute seems to be ignored. In some cases I'd like to actually have that level of comparison.
# @runtime_checkable # doesn't make a difference in what I'm checking
class HasIntItems(Protocol):
items: int
obj = namedtuple('IntItems', 'items')(3)
obj_with_no_items = namedtuple('IntItems', 'not_items')(3)
assert isinstance(obj.items, int) # obj has items and is an int
assert isinstance(obj, HasIntItems) # so this is true, and it's expected
assert not isinstance(obj_with_no_items, HasIntItems) # also expected
obj_w_non_int_items = namedtuple('IntItems', 'items')('i am not an int')
assert not isinstance(obj_w_non_int_items.items, int) # items exists, but is not an int
assert isinstance(obj_w_non_int_items, HasIntItems) # expected this to fail: items is a
assert isinstance(dict(), HasIntItems) # expected this to fail:dict() considered to have items, but it's a callable, not an int!
But how?....