typing
typing copied to clipboard
`isinstance` check with nested `Protocol`.
isinstance(obj, ProtocolSubclass) only checks the the existence of ProtocolSubclass's methods on obj and not the type signature. To provide deeper checks, maybe isinstance could check attributes/methods on ProtocolSubclass that are themselves a Protocol.
A small example showing the change in behavior:
@runtime_checkable
class FooLike(Protocol):
attr: str
@runtime_checkable
class BarLike(Protocol):
attr: FooLike
@dataclass
class Foo:
attr: str = "test"
@dataclass
class Bar:
attr: Foo
foo = Foo()
bar = Bar(foo)
# No change in current behavior
assert isinstance(foo, FooLike) # passes
assert isinstance(bar, BarLike) # passes
assert isinstance(bar, FooLike) # passes
# Change in behavior
assert not isinstance(foo, BarLike) # NOT because `BarLike.attr` should be `FooLike` and `foo.attr` is not
This is a big can of worms and I don't think it's a good idea to add it to the standard library. For example, we'd have to get runtime checks for TypeVars, and generic types, and callable compatibility.
Just wondering, is there any clean way to do structural typing for more complex objects?
E.g.
def isbarlike(obj) -> TypeGuard[BarLike]
....
looks useful, but really isn't because all we learn is that obj has an attribute named attr, not also that the attribute is FooLike. We can regress to using specific classes:
def isabar(obj) -> TypeGuard[Bar] # functionally equivalent to ``isinstance(obj, Bar)``
....
but that defeats the intent of static duck typing.
The semantics of isinstance are well established and are unlikely to change. What you're proposing would not only be a can of worms. It would also be a backward compatibility break.
If you want to apply different semantics (e.g. perform deeper nested checks), you can write your own implementation. If you use TypeGuard as a return type, then a static type checker will also be able to use it for type narrowing.
all we learn is that obj has an attribute named attr, not also that the attribute is FooLike
If isbarlike is implemented correctly (i.e. it properly validates that obj matches a BarLike protocol), then it will need to validate that the attribute is FooLike.
def isfoolike(obj) -> TypeGuard[FooLike]:
return hasattr(obj, 'attr') and isinstance(obj.attr, str)
def isbarlike(obj) -> TypeGuard[BarLike]:
return hasattr(obj, 'attr') and isfoolike(obj.attr)