pythonmypypython-typingpython-3.11

StrEnums and type narrowing (mypy)


I am fighting with type narrowing of StrEnums

To summarize my issue I have some abstract base class

from abc import ABC, abstractmethod
from enum import StrEnum

class Animal(ABC):
    @staticmethod
    @abstractmethod
    def speak(animal_type: StrEnum):
        ...

and derivative classes each with their own StrEnums:

class FrogType(StrEnum):
    frog = "frog"
    toad = "toad"

class Frog(Animal):
    @staticmethod
    def speak(animal_type: FrogType):
        print(f"{animal_type} goes ribbit")

The following code works as expected:


Frog.speak(FrogType.frog)  # "frog goes ribbit"

and enums seem to be compatible:

print(isinstance(FrogType.frog, FrogType))  # True
print(isinstance(FrogType.frog, StrEnum))  # True

However MyPy doesnt recognize this as valid:

# python3.11 -m mypy frog.py
frog.py:16: error: Argument 1 of "speak" is incompatible with supertype "Animal"; supertype defines the argument type as "StrEnum"  [override]
frog.py:16: note: This violates the Liskov substitution principle
frog.py:16: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
Found 1 error in 1 file (checked 1 source file)

This seems to be a valid type narrowing, so I must be annoting something wrong. What would be a correct solution?


Solution

  • You can avoid this issue by using a generic in place of StrEnum. Below AnimalType is defined such that it has a bound of StrEnum:

    from abc import ABC, abstractmethod
    from enum import StrEnum
    from typing import Generic, TypeVar
    
    AnimalType = TypeVar("AnimalType", bound=StrEnum)
    class Animal(ABC, Generic[AnimalType]):
        @staticmethod
        @abstractmethod
        def speak(animal_type: AnimalType):
            ...
    
    class FrogType(StrEnum):
        frog = "frog"
        toad = "toad"
    
    class Frog(Animal[FrogType]):
        @staticmethod
        def speak(animal_type: FrogType):
            print(f"{animal_type} goes ribbit")
    
    print(isinstance(FrogType.frog, FrogType))  # True
    print(isinstance(FrogType.frog, StrEnum))  # True
    

    Hope this helps!