pythontype-hintingmypytypeddict

Validate `Dict` against `TypedDict` with MyPy


I have a couple of data processors that have implemented an interface whose method "process" takes over the processing part with a result to be returned. The result should be a dictionary that is compliant to a given structure. That's why I have defined TypedDict's for each processor result. Maybe I haven't understood TypedDict correctly, but I want to achieve something like this:

import abc

from typing import TypedDict

class AProcessorResult(TypedDict):
  param1: str
  param2: int

class BProcessorResult(TypedDict):
  paramA: str
  paramB: str
  paramC: float


class IProcessor(abc.ABC):

  def process(self) -> <Dictionary mypy-checked against processor result structure>:
    raise NotImplementedError


class AProcessor(IProcessor):

  def process(self) -> <Dictionary mypy-checked against AProcessorResult structure>:
    result: AProcessorResult = {
      'param1': 'value1',
      'param2': 0
    }
    return result


class BProcessor(IProcessor):

  def process(self) -> <Dictionary mypy-checked against BProcessorResult structure>:
    result: BProcessorResult = {
      'param1': 'value1',
      'param2': 1
    }
    return result


def main() -> None:

  aProcessor: AProcessor = AProcessor()
  aProcessor.process()                    # <- Should be successful during the MyPy check

  bProcessor: BProcessor = BProcessor()
  bProcessor.process()                    # <- Should fail during the MyPy check


if __name__ == '__main__':
  main()

If I return ordinary Dict's from the "process" methods, MyPy complains about the type incompatibility:

Incompatible return value type (got "AProcessorResult", expected "Dict[Any, Any]")

I don't want to use dataclasses at this point, because the dictionaries will be "dataclassified" in a later stage anyway and require some additional dict processing before.

Is there a neat way to achieve this in a most abstract and generic way?

I am using Python 3.8.


Solution

  • You can use generics to achieve that. Basically, you define IProcess to return some generic type and define which concrete type it is using IProcessor[ConcreteType].

    import abc
    
    from typing import TypedDict, Any, Generic, TypeVar
    
    T = TypeVar("T")
    
    
    class AProcessorResult(TypedDict):
      param1: str
      param2: int
    
    class BProcessorResult(TypedDict):
      paramA: str
      paramB: str
      paramC: float
    
    
    class IProcessor(abc.ABC, Generic[T]):
    
      def process(self) -> T:
        raise NotImplementedError
    
    
    class AProcessor(IProcessor[AProcessorResult]):
    
      def process(self) -> AProcessorResult:
        result: AProcessorResult = {
          'param1': 'value1',
          'param2': 0
        }
        return result
    
    
    class BProcessor(IProcessor[BProcessorResult]):
    
      def process(self) -> BProcessorResult:
        result: BProcessorResult = {
          'param1': 'value1',
          'param2': 1
        }
        return result
    
    
    def main() -> None:
    
      aProcessor: AProcessor = AProcessor()
      aProcessor.process()                    # <- Should be successful during the MyPy check
    
      bProcessor: BProcessor = BProcessor()
      bProcessor.process()                    # <- Should fail during the MyPy check
    
    
    if __name__ == '__main__':
      main()
    

    if you want to be more strict and force the return type to be always a mapping type, you can bound the type var as

    Mapping
    from typing import Mapping
    T = TypeVar("T", bound=Mapping[str, Any])
    

    You can playaround with those solutions here