Type annotation of methods in narrowed subclass of generics

This may be a tooling question, but I’d like to know of there’s any convention or best practice regarding the type annotation of method signatures in subclasses of generic classes. Consider the following (contrived) example:

from abc import ABC, abstractmethod
from typing import Generic, TypeVar

ID = TypeVar('ID', int, str)  # identifiers can be str or int


class IdChecker(Generic[ID], ABC):
    """Generic class to validate identifiers."""

    @abstractmethod
    def is_id_valid(self, id: ID) -> bool: ...


class IntIdChecker(IdChecker[int]):
    """Class to validate integer identifiers."""

    def is_id_valid(self, id):
        value: bool = id > 0  # note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
        return value


iic = IntIdChecker()
print(iic.is_id_valid(123))  # True
print(iic.is_id_valid(-5))  # False
reveal_type(iic.is_id_valid(123))  # note: Revealed type is "Any"

Mypy has no problem with the above when the body of IntIdChecker.is_id_valid() is just return id > 0, but when I have a typed statement like value: bool = id > 0, I get the note about untyped functions in the comment above. This note is indicative of why the revealed type of the return value is Any and not int.

My question is: is it expected that I annotate the method signature of the overridden method? Explicit is better than implicit and all, but I assumed that the IdChecker[int] part of the IntIdChecker class would appropriately narrow the method and repeating the annotation on the method seemed redundant.

I’m also making the assumption that the annotation of an overridden method should be compatible with that of its base class, but this may be incorrect.

I also note that Mypy, Pyre, and ty all say the revealed type is Any/Unknown, but Pyright seems to narrow the method signature as I expected and the revealed type is bool.

Yes, you need to annotate overridden methods. Checkers will error if they aren’t compatible with the base class. This is an aspect called ‘gradual typing’, which is intended for compatibility with existing code. If a function/method has no annotations, type checkers assume that the code wasn’t typed at all/yet, and so suppress errors. You probably want to set the flag it mentions to enable checking of all functions, so you’ll then get an error for all missing annotations.

Pyright deviates from the spec in this way, always checking all functions. It is a bit of a wart that you need to repeat definitions, though it does have the small benefit that if the base class changes, you’ll get notified if subclasses won’t be compatible, and you also don’t need to find the base definition to see the annotations.

Type inference of return types is sometimes also attempted

That depends on the configuration. In pyright’s strict mode, for example, it will report an error for every unknown type.


But I agree that the idiomatic approach is to annotate everything. It’s not very DRY, but as @TeamSpen210 explained, that’s actually a good thing.

Thanks for the answers!

I’m familiar with gradual typing, but I thought that maybe the combination of (1) method annotation on the base class, and (2) narrowing via the generic class’s type variable on the subclass (i.e., IdChecker[int]) constituted explicit typing on the method of the subclass. It seems like the answer is no.

Yeah, makes sense. Even a single developer can make mistakes in their own codebase, so affirming and then reaffirming typing assumptions is a good way to catch these bugs.

Thanks, @ElevenPhonons, for the examples. I’m aware of the Python 3.12 syntax and I’m looking forward to when 3.12 is the lowest version I support so I can make use of it.

Unfortunately, the new syntax and use of protocols is orthogonal to my question, which is whether an explicitly typed method on a generic base class plus type narrowing of the generic on a subclass constitutes explicit typing of the subclass’s implementation of the method even without in-situ annotations on the method. The answer was no.