Skip to content

Subtyping

To move forward, we'll need a quick interlude about subtyping.

Subtyping is a generalization of subclassing. If you're familiar with set theory, subtyping is very similar to one set being a subset of another.

Definition

A type Child is a subtype of type Parent if a value of type Child can be assigned to a variable of type Parent

Examples

  • Dog is a subclass of Animal, so Dog is also a subtype of Animal

    class Animal:
        pass
    
    class Dog(Animal):
        pass
    
    pet: Animal = Dog()
    

  • int and str are subtypes of int | str

    x: int | str = "hello"
    y: int | str = 42
    

  • Any type is a subtype of itself

    class Foo:
        pass
    
    foo: Foo = Foo()
    

  • Literal["nice"] is a subtype of str

    nice: Literal["nice"] = "nice"
    word: str = nice
    

  • A type is a subtype of a protocol if it satisfies it

    class Reader(Protocol):
        def read_chunk(self, max_size: int) -> str: ...
    
    class StringReader:
        def __init__(self, initial: str) -> None:
            self._buffer = initial
    
        def read_chunk(self, max_size: int) -> str:
            max_size = max(max_size, 0)
            chunk = self._buffer[:max_size]
            self._buffer = self._buffer[max_size:]
            return chunk
    
    reader: Reader = StringReader("Hello, world!")
    

  • tuple[int, ...] is a subtype of tuple[int | str, ...]

    ints: tuple[int, ...] = (1, 2, 3, 4)
    ints_or_strings: tuple[int | str, ...] = ints
    

  • Callable[[Animal], int] is a subtype of Callable[[Dog], int | str]

  • list[int] is not a subtype of list[int | str]

    Wait, what?

    You'll see why later in the series!