Using a property as type guard

I have some properties on a class telling me if the instance is of the right type.
For example:

class Object:
    @property
    def is_module(self):
        return isinstance(self, Module)

    @property
    def is_class(self):
        return isinstance(self, Class)

class Module(Object): ...

class Class(Object): ...

I would like to annotate these properties as type guards, but it does not seem to be supported by the specification (PEP 647):

    @property
    def is_module(self) -> TypeGuard[Module]:
        return isinstance(self, Module)

…gives error: TypeGuard functions must have a positional argument [valid-type].

PEP 647 says this:

A concern was raised that there may be cases where it is desired to apply the narrowing logic on self and cls. This is an unusual use case, and accommodating it would significantly complicate the implementation of user-defined type guards. It was therefore decided that no special provision would be made for it. If narrowing of self or cls is required, the value can be passed as an explicit argument to a type guard function.

Is there any other way :slight_smile: ?

2 Likes

I don’t think there is another way :confused:

It’s funny that the only times I have wanted to use TypeGuard was when I wanted to narrow self, so I don’t think it’s as unusual as the PEP makes it sound.

4 Likes

Yup, I keep coming back to this. I almost never use TypeGuard, and the only times I want to use it is when I add a convenient property to a class to avoid having to use isinstance (and therefore having to import an additional class at runtime) :sweat_smile:

Yep, this feels like a shame - this is the first use case I’ve had for TypeGuard and it isn’t supported. :frowning:

Nothing is stopping anyone here from making a proper proposal and pushing it through.[1] It would just need a champion to argue for this. I don’t think it would need a new PEP, just an update to the spec after discussion with type-checkers-authors.


  1. Ok, later discussion might prevent this from going through if something comes up, I can’t guarantee this will succeed ↩︎

The closest I’ve got to a solution is by using a custom property-like desciptor, with which pyright is able to correctly reveal the type of the property as TypeGuard[Module] but is still unable narrow the type of self accordingly even though self is properly passed as the first argument to the __get__ method of the descriptor:

from typing import TypeGuard, reveal_type

class type_guard[T]:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, obj_type) -> TypeGuard[T]:
        return self.func(obj)

class Object:
    @type_guard['Module']
    def is_module(self):
        return isinstance(self, Module)

    def do(self) -> None:
        # pyright: Type of "self.is_module" is "TypeGuard[Module]"
        reveal_type(self.is_module)
        if self.is_module:
            # pyright: Type of "self" is "Self@Object"
            reveal_type(self)

class Module(Object): ...

mypy on the other hand reveals self.is_module to be of type bool so it needs even more work for type narrowing to be possible.

This seems to get through both:

from typing import Optional, reveal_type

class Object:
    def to_module(self) -> Optional['Module']:
        return self if isinstance(self, Module) else None
    
    def do(self):
        reveal_type(self.to_module())
        if m := self.to_module():
            reveal_type(m)

class Module(Object):
    ...

class Class(Object):
    ...

if __name__ == "__main__":
    mod = Module()
    mod.do()
    cls = Class()
    cls.do()