Type Hint Dynamically Created Subclasses

205 views Asked by At

Simplified Example

class A:
    def __init__(self): ...

class BBuilder:
    # To make this simpler, we won't have any arguments

    def build(self): # -> WhatTypeHintDoIPutHere

        class B(A):
            def __init__(self):
                super().__init__()

        return B

my_b_builder: BBuilder = BBuilder()
BBuilt = my_b_builder.build() # BBuilt: WhatTypeHintDoIPutHere
my_b = BBuilt() # my_b: WhatTypeHintDoIPutHere

Question

How do I correctly type hint my_b and BBuilt? Without removing the BBuilder and without replacing the BBuilder with static subclass definitions.

What I tried

Tried typehinting my_b as:

  • Type[A] - incorrect because my_b is an instance

  • Type[B] - incorrect because my_b is an instance

  • BBuilt - mypy complained - you cannot use variables in typehints

  • Using TypeAliases BBuiltAlias: TypeAlias = BBuilt; my_b: BBuiltAlias - you cannot use variables in typehints

  • Using type keyword type BBuiltAlias = BBuilt; my_b: BBuiltAlias - you cannot use variables in typehints

Editing the BBuilder to not be a dataclass doesn't change anything
Also experimented with TypeVars - no success. Attempts: 1 2 3
Also tried asking ChatGPT - no success

Afterword

If you really need it here is the original code along with some example usage of the classes. As a side question: Is it a bad coding practice to do this? The original code is easy to edit and easy to use.

Off-topic: My failed attempt of working around this problem here

1

There are 1 answers

0
JakuWorksOfficial On

Even though this is possible, dynamically creating classes (according to many people) may bring some nasty problems. There probably are other ways to solve your problem without dynamic class definitions!

The answer turned out to be extremely simple.

Example

from typing import Type

# To make this a little simpler, we will not define any arguments for the init functions.
# It would not be that hard if you wanted to introduce arguments


class ABase:  # We want to subclass ABase and overwrite its init
    def __init__(self) -> None:  # We want to call this init in our dynamically created class!
        print("ABase Init")
        ...  # Imagine lots of code here

    def uncool(self) -> None:  # This method is uncool. We want our final subclass to overwrite it and make it cooler
        print("Uncool (we should not see this message because its uncool and this method will be overwritten)")

    def a_cool(self) -> None:  # This method is cool! We want our final subclass to support it!
        print("A_Cool Is there really no other way to solve your problem than creating classes dynamically?")


class BReal(ABase):
    def __init__(self) -> None:
        super().__init__()
        print("BReal Init")
        ...  # Imagine lots of code here

    def b_cool(self) -> None:  # This method is also cool! we want our final subclass to support it!
        print("B_Cool")

    def uncool(self) -> None:
        print("Uncool is now cool because it has been overwritten! So we can now see its message!")

    # Define all the methods and functionality you want the final dynamically-created Subclass to support
    # Basically treat BReal almost as if it WAS the final dynamically-created Subclass


class BClassBuilder:

    def build(self) -> Type[BReal]:  # Since the B class is 100% compatible with BReal. There is no problem with using the Type[BReal] typehint
        class B(BReal): ...

        return B  # Yay! B is now dynamically created

        # KEEP IN MIND THIS IS JUST SOME BASIC PROOF-OF-CONCEPT CODE
        # I KEPT THIS AS SIMPLE AS POSSIBLE
        # But don't be fooled, this simple design took me a day to figure out


our_builder: BClassBuilder = BClassBuilder()
BBuilt: Type[BReal] = our_builder.build()  # Since the B class is 100% compatible with BReal. There is no problem with using the `Type[BReal]` typehint

print("\nTesting Inits")
our_b: BReal = BBuilt()  # Instance of our dynamically created B class!

print("\nTesting Methods")
our_b.a_cool()  # InteliSense WORKED
our_b.b_cool()  # InteliSense WORKED
our_b.uncool()  # InteliSense WORKED

"""OUTPUT

Testing Inits
ABase Init
BReal Init

Testing Methods
A_Cool Is there really no other way to solve your problem than creating classes dynamically?
B_Cool
Uncool is now cool because it has been overwritten! So we can now see its message!
"""  # EVERYTHING WORKS AND THERE ARE 0 COMPLAINTS FROM PYLANCE AND MYPY

Explanation

I don't think much explanation is necessary due to the number of comments in the example. I will give a short explanation of what is happening:

  1. First we define the class we want to subclass as ABase

  2. Then we statically create a subclass of ABase named BReal ('real' because this is really the part where you write the code). Our dynamically created class will simply subclass the BReal and therefore inherit all of its methods and whatever else.

  3. In the example now we add some means of dynamically defining a class. I chose a Class Builder (this is probably not a real term, nor a professional term).

  4. Defining that dynamically created subclass of A is very simple. Let's name it B. class B(BReal): ... We use ... on purpose because there is no real code we should write here.

Note for people newer to python who want B to support arguments in its init:
Since the way we defined B makes it a 100% subclass of BReal without any differences, it also inherits BReal's init! So if you want B to support arguments, edit BReal's init. Leave B's init undefined!

  1. Now simply use the code you've written to dynamically define a class and save it to a variable.
    Please read the code of the Example if you want to see how to typehint that variable and an instance of B.

Even though this is possible, dynamically creating classes (according to many people) may bring some nasty problems. There probably are other ways to solve your problem without dynamic class definitions!

As an off-topic: I decided to remove dynamic class definitions in favour of something much simpler from my code: code