16

In one application I have code which generates dynamic classes which reduces the amount of duplicated code considerably. But adding type-hints for mypy checking resulted in an error. Consider the following example code (simplified to focus on the relevant bits):

class Mapper:

    @staticmethod
    def action() -> None:
        raise NotImplementedError('Not yet implemnented')


def magic(new_name: str) -> type:

    cls = type('%sMapper' % new_name.capitalize(), (Mapper,), {})

    def action() -> None:
        print('Hello')

    cls.action = staticmethod(action)
    return cls


MyCls = magic('My')
MyCls.action()

Checking this with mypy will result in the following error:

dynamic_type.py:15: error: "type" has no attribute "action"
dynamic_type.py:21: error: "type" has no attribute "action"

mypy is obviously unable to tell that the return-value from the type call is a subclass of Mapper, so it complains that "type" has not attribute "action" when I assign to it.

Note that the code functions perfectly and does what it is supposed to but mypy still complains.

Is there a way to flag cls as being a type of Mapper? I tried to simply append # type: Mapper to the line which creates the class:

cls = type('%sMapper' % new_name.capitalize(), (Mapper,), {})  # type: Mapper

But then I get the following errors:

dynamic_type.py:10: error: Incompatible types in assignment (expression has type "type", variable has type "Mapper")
dynamic_type.py:15: error: Cannot assign to a method
dynamic_type.py:15: error: Incompatible types in assignment (expression has type "staticmethod", variable has type "Callable[[], None]")
dynamic_type.py:16: error: Incompatible return value type (got "Mapper", expected "type")
dynamic_type.py:21: error: "type" has no attribute "action"
exhuma
  • 20,071
  • 12
  • 90
  • 123

1 Answers1

7

One possible solution is basically to:

  1. Type your magic function with the expected input and output types
  2. Leave the contents of your magic function dynamically typed with judicious use of Any and # type: ignore

For example, something like this would work:

class Mapper:
    @staticmethod
    def action() -> None:
        raise NotImplementedError('Not yet implemnented')


def magic(new_name: str) -> Mapper:

    cls = type('%sMapper' % new_name.capitalize(), (Mapper,), {})

    def action() -> None:
        print('Hello')

    cls.action = staticmethod(action)  # type: ignore
    return cls  # type: ignore


MyCls = magic('My')
MyCls.action()

It may seem slightly distasteful to leave a part of your codebase dynamically typed, but in this case, I don't think there's any avoiding it: mypy (and the PEP 484 typing ecosystem) deliberately does not try and handle super-dynamic code like this.

Instead, the best you can do is to cleanly document the "static" interface, add unit tests, and keep the dynamic portions of your code confined to as small of region as possible.

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • I think this will have to do ;) – exhuma Jan 17 '18 at 13:47
  • 10
    Actually, the definition of the "magic" function should have the return type `typing.Type[Mapper]`. That way the `type: ignore` can be removed on the `return` line. – exhuma Sep 19 '18 at 08:57