Search code examples
pythonpython-typingmetaclasstype-alias

How can I get IDEs to recognize a desired type name for a statically declared, dynamically created class in Python?


Problem:

I am working on a library with, e.g., support for a UInt5 type, an Int33 type, etc. The library is a little more complicated than this, but for the sake of example creating a UInt12 type might go

def makeUInt(size:int) -> type[UInt]:
   class NewUInt(UInt):
       # Do stuff with size
   NewUInt.__name__ = f"UInt{size}"
   return NewUInt
   
UInt12 = makeUInt(12)
an_example_number = UInt12(508)

My IDE's (VS Code) IntelliSense feature then recognizes the type of an_example_number as UInt, rather than UInt12.

The Rub:

I do not expect dynamically declared types to be picked up by type-hinting. However, I have clearly specified UInt12 as a type alias, and in fact if I subclass instead of type-alias by going

def makeUInt(size:int) -> type[UInt]:
   class NewUInt(UInt):
       # Do stuff with size
   NewUInt.__name__ = f"UInt{size}"
   return NewUInt
   
class UInt12(makeUInt(12)): pass
an_example_number = UInt12(508)

everything works as intended, so clearly on some level the dynamic declaration can be coerced into something the IDE understands.

For example, I could, hypothetically, have UInt keep a register of created classes and prevent UInt12(makeUInt(12)) from actually subclassing. This is obviously not an ideal workaround, though.

The Ask:

How can I (preferably in Python 3.8) retain the advantage of dynamically creating types while getting the IDE to understand my preferred nomenclature for instances of those types?

The end-use case is that I want to provide certain types explicitly without redeclaring the # Do stuff with size information every time, so that common types like UInt16, UInt32, etc. can be declared in my library and receive hinting, whereas more uncommon types like UInt13 will be declared by users as needed and not necessarily receive hinting.

Back-of-the-box

def makeUInt(size:int) -> type[UInt]:
   class NewUInt(UInt):
       # Do stuff with size
   NewUInt.__name__ = f"UInt{size}"
   return NewUInt
   
UInt12 = makeUInt(12)
an_example_number = UInt12(508)

I wanted an_example_number to show up as a UInt12 by the type-hinter. It shows up as UInt.


Solution

  • Creating subclasses explicitly would probably work better in your case, as you have seen.

    Instead of creating a class factory function, define the common behaviour of all derived classes in the base class and make specifics depend on class variables defined by subclasses.

    In an example which is similar to yours, this could look like:

    class UInt:
        size: int  # to be defined by subclasses
    
        def __init__(self, value: int):
            if value >= 2 ** self.size:
                raise ValueError("Too big")
            self._value = value
    
        def __repr__(self):
            return f"{self.__class__.__name__}({self._value})"
    
    
    class UInt12(UInt):
        size = 12
    
    
    an_example_number = UInt12(508)
    print(an_example_number)  # UInt12(508)