Python
Type Hinting
Subclassing
Programming
PEP484

Subclass in type hinting

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In Python type hints, subclassing matters in two different ways: sometimes you want an instance of a base class or any subclass, and sometimes you want the class object itself so you can construct or inspect subclasses. Those cases use different annotations. The most common mistake is confusing a hint for instances such as Animal with a hint for classes such as type[Animal].

Hinting Instances: Use the Base Class

If a function accepts an object that can be an Animal or any subclass of Animal, annotate it with Animal.

python
1class Animal:
2    def speak(self) -> str:
3        return "?"
4
5
6class Dog(Animal):
7    def speak(self) -> str:
8        return "woof"
9
10
11def describe(animal: Animal) -> str:
12    return animal.speak()
13
14
15print(describe(Dog()))

This works because a Dog instance is a valid Animal instance for type-checking purposes.

Hinting Class Objects: Use type[Base]

If the function accepts the class itself rather than an instance, use type[Animal].

python
1class Animal:
2    pass
3
4
5class Dog(Animal):
6    pass
7
8
9def create_animal(cls: type[Animal]) -> Animal:
10    return cls()
11
12
13pet = create_animal(Dog)
14print(type(pet).__name__)

This means the caller must pass a class object that is Animal or a subclass of it.

This distinction is one of the most important subclass-related type-hinting patterns in Python.

Preserve the Specific Subclass with a Type Variable

Sometimes you want the return type to match the specific subclass that came in. That is where a type variable bound to a base class helps.

python
1from typing import TypeVar
2
3class Animal:
4    pass
5
6
7class Dog(Animal):
8    pass
9
10
11TAnimal = TypeVar("TAnimal", bound=Animal)
12
13
14def build(cls: type[TAnimal]) -> TAnimal:
15    return cls()
16
17
18dog = build(Dog)
19print(type(dog).__name__)

Without the type variable, the return type would usually collapse to the base class and lose the more specific information.

Accepting "Any Subclass" versus "Exactly This Class"

A hint such as type[Animal] allows Animal and its subclasses. If you really need to restrict behavior at runtime to an exact class, that is no longer a type-hinting question alone. It becomes a runtime validation rule.

Type hints generally describe substitutability, and subclasses are normally allowed where the base type is expected.

Protocols Can Matter More Than Inheritance

Subclass hints are useful, but inheritance is not the only way to describe valid inputs. If the real requirement is behavior rather than ancestry, a Protocol may communicate the contract better.

python
1from typing import Protocol
2
3class Speaker(Protocol):
4    def speak(self) -> str:
5        ...
6
7
8def announce(item: Speaker) -> str:
9    return item.speak()

This does not require subclassing at all. It only requires the right method shape. In some APIs, that is a better design than forcing everything under one class hierarchy.

Runtime Behavior Is Separate from Static Checking

Python does not enforce type hints at runtime by default. That means this is primarily for tools such as mypy, pyright, IDEs, and human readers.

If the code truly requires a subclass relationship at runtime, check it explicitly.

python
def require_subclass(cls: type[Animal]) -> None:
    if not issubclass(cls, Animal):
        raise TypeError("cls must inherit from Animal")

Static hints help catch mistakes early, but they do not replace runtime validation when runtime safety matters.

Common Pitfalls

  • Annotating a parameter with Animal when the function actually expects the class object and should use type[Animal].
  • Forgetting to use a bound type variable when the returned object should keep its specific subclass type.
  • Treating type hints as if Python enforces them automatically at runtime.
  • Overusing subclass-based annotations when a protocol would better describe the required behavior.
  • Confusing instance substitution with exact-class constraints.

Summary

  • Use the base class name such as Animal when the function expects an instance of that class or any subclass.
  • Use type[Animal] when the function expects a class object.
  • Use a bound type variable when the return type should preserve the specific subclass.
  • Consider Protocol when the contract is behavioral rather than inheritance-based.
  • Remember that type hints help static analysis, but runtime checks are still separate.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.