Type Hints for Dynamic Class Creation

Posted on Sun, 15 Mar 2020 in Python

Type hints in Python are cool. Projects of any size can benefit from them. Actually for projects bigger than 50-60 modules or with more than 5 developers involved type hints become crucial to keep its code quality at a reasonable level. And it looks like it is my favorite topic to write about.

But sometimes, its realization in Python is counterintuitive or even confusing. You have to write a code that has no sense but typing, for example, in case of dynamic type creation and necessity of using cast function. Well, technically all type hints in Python have no sense but typing. However, in the vast majority of cases, type annotation adds clarity to the code, not confusion.

Long story short. Recently one of my colleges has implemented a new feature in our project. As part of its design there is quite a lot of function that looks like that:

from typing import Type


class BaseClass:
    pass


def create_subclass(**args) -> Type[BaseClass]:
    return type(
        'SubClass',
        (BaseClass,),
        args
    )

The only problem with this code is that PyCharm marks it as not correct type-wise: Expected type 'Type[BaseClass]', got 'type' instead. Autocompletion works. But this highlighted block stops me every time I see it. mypy doesn't find any problems with that code showing Success: no issues found in 1 source file. That looks right.

It looks right, but mypy does nothing with that code. I made an additional experiment with it. Even if I put WrongBase instead of BaseClass in type function, it doesn't find any problem.

I thought there was a typed alternative for type function. Like one we have for named tuples. There is no such thing in the typing library. Probably the main reason is that type in this context isn't used often.

However, the construction itself looks like its type can be calculated automatically. At least it can be true for some cases. Why? Because of the type function definition. We have a tuple with base types. So, as a result, we get a subtype of them.

This statement makes sense only if you have only one base class and do not add extra methods to the subclass. Before evaluating type function you don't have a subclass. It is impossible to annotate the result. In the simplest case (one base, no extra things changing class interface) it is possible to say that the result has the type equals to the base class.

Sometimes even if you have some changes, it is Ok to say that the type of this construction is equal to the base class. Often type is used to make some new classes with a predefined interface.

However in the case of multiple base classes it hard to say what the type hint should be used. I don't know how to write a type hint for construction with two or more superclasses. Union?

And type creates a new class. You can bounce methods that change the public interface. This construction can get a list of basses that have been built dynamically. And it makes automatic type hints next to impossible here.

So we have to find a workaround. The only way I've found is to use the cast function. It solves the problem because we have only one base class and can use it as a cast parameter. But it looks a bit strange in the code.

from typing import Type, cast


class BaseClass:
    pass

def create_subclass(**args) -> Type[BaseClass]:
    return cast(
        Type[BaseClass], 
        type('SubClass', (BaseClass,), args)
    )

The cast function is similar to dynamic_cast in C++: let's pretend that this is an instance of A and prey that it is always true. But in Python, we don't worry about it. Firstly, it does nothing, just mark its argument with a given type. Secondly, if something has the required attribute it matches. It can cause errors and hard-to-find bugs, but it's a different story. And it is the way how Python and other interpreted languages work.

There are a lot of typing hint constructions that look strange. Mainly because of the optionality of typing in Python. However, type hinting solidifies big Python projects. I can't stress enough how important is to use them in your everyday work. Yes, it will fight common sense and dynamic Python nature from time to time, but it worth using them.