Search code examples
pythonmetaprogrammingtypechecking

How to properly create a new type whith a custom behaviour of isinstance?


I would like to have for exemple a posint class that inherit from int, but with a custom behaviour when calling isinstance():

>>> isinstance(1, int), isinstance(1, posint)
(True, True)
>>> isinstance(-1, int), isinstance(-1, posint)
(True, False)

I tried first:

class posint(int):

    def __instancecheck__(self, obj):
        try:
            obj >= 0
            return True
        except:
            return False

But the __instancecheck__ have to be declared in a metaclass.

So I end up with this heavy and ugly thing:

class NewType(type):

    def __instancecheck__(self, obj):
        try:
            obj >= 0
            return True
        except:
            return False

class posint(metaclass=NewType):
    pass

It works, but it cannot be the good solution... Doesn't work with any other checking, doesn't support inheritance...

After that I managed to implement something better:

class CheckedType(type):

    def __instancecheck__(cls, obj):
        if not all(isinstance(obj, base) for base in cls.mro()[1:-1]):
            return False
        return cls.__instancecheck__(obj)

class posint(int, metaclass=CheckedType):

    @classmethod
    def __instancecheck__(cls, obj):
        if obj >= 0:
            return True
        return False

But this seems an abuse of __instancecheck__. I was thinking we can use use something from the abc or the typing module...

Any ideas?


Solution

  • A this time, after some experimentations, I'm using this recipe:

    class TypedMeta(type):
        """Metaclass used for custom types."""
    
        def __instancecheck__(cls, obj):
            return cls._test(obj)
    
        @staticmethod
        def _init(self, x):
            if not self._test(x):
                raise ValueError(f"bad value for '{self.__class__.__name__}' object")
    
        def __init__(cls, name, bases, clsdict):
            if not clsdict.get('_test'):
                raise TypeError(f"cannot instaciate '{name}' class without '_test' method")
            setattr(cls, '__init__', TypedMeta._init)
    
    
    class posint(int, metaclass=TypedMeta):
        """Strictly positive 'int'."""
    
        @classmethod
        def _test(cls, val):
            return val > 0
    

    So even if somebody wants to instantiate an object of this type, or to cast another into it, it will perform the _test method first.