python / typing

Python static typing home. Hosts the documentation and a user help forum.
https://typing.readthedocs.io/
Other
1.59k stars 233 forks source link

`isinstance` check with nested `Protocol`. #1257

Open nstarman opened 2 years ago

nstarman commented 2 years ago

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
JelleZijlstra commented 2 years ago

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.

nstarman commented 2 years ago

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.

erictraut commented 2 years ago

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)