Search code examples
pythonpython-3.xtype-hinting

Is it possible to type hint exclusively a class object but exclude subclass objects?


I would like to exclusively type hint an argument to a specific class but exclude any subclasses.

class A:
    pass
class B(A):
    pass

def foo(obj: A):
    pass

foo(B()) # I'd like the type checker to warn me here that it expects A, not B

Is this possible? and if so, how?

(bonus points if you can tell me what I would call this. Googling wasn't helpful, but I'm afraid I'm using the wrong terminology to describe this)


Solution

  • No, this is not possible to do.

    Fundamentally, the Python typing ecosystem assumes that you are following the Liskov substitution principle -- assumes that it is always safe to substitute a subclass in places designed to handle the parent.

    The fact that it permits you to pass in instances of B in addition to instances of A in your code snippet is just one example of this principle in play.

    So if your subclass B is designed not to follow the Liskov substitution principle, that probably it wasn't ever really a "kind of" A to begin with and shouldn't be subclassing it.

    You could fix this by either adjusting your code so B does properly follow Liskov or by making B stop subclassing A and instead use composition instead of inheritance as a mechanism for code reuse. That is, make B keep an instance of A as a field and use it as appropriate.

    And if you run into a rare case where it's legitimately not possible to ever subclass A without breaking Liskov, something you could do to prevent people from accidentally subclassing it would be to explicitly mark A as being final:

    from typing import final
    # If you want to support Python 3.7 or earlier, pip-install 'typing_extensions'
    # and do 'from typing_extensions import final' instead
    
    @final
    class A: pass
    
    class B(A): pass
    

    This should make your type checker report a "B cannot subclass A" error on the definition of B. And if you fix that error by changing/deleting B, the call to foo(B()) should also naturally fail to type-check.