pythonpython-typing

Type of a returned class


Update: as chepner points out in the comments, creating the class in the function is a bad idea, it performs ten to eighty times slower than other solutions. See the performance comparison in my self-answer, which also shows how to do the typing.


I do this:

def get_an_x():
    class X:
        foo = 1
        bar = 'Hello'
        baz = None
    return X

get_an_x returns a class X, which serves as a plain structure.

What is the type of this class X? Doing def get_an_x() -> type: ... (that what type(X) returns) will not work, then PyCharms's type checker does not detect the attributes of X. Annotating the attributes with ClassVar[...] also does not help.

I have seen this answer, but doing def get_an_x() -> type[X]: ... does not work, because I define the class inside the function, and it is not seen from outside.


Solution

  • Updated Answer

    Creating a class in a function and returning it, is a bad idea. It is very slow. There are other ways to do this:

    ### INITIALISATION
    
    
    ## CLASS INSTANCE
    # Structural type info, fastest (except simple tuples).
    
    class MyInstance:
      __slots__ = ['foo', 'bar']
      def __init__(self, foo: int, bar: str):
        self.foo = foo
        self.bar = bar
    
    
    ## DATA CLASS
    # https://docs.python.org/3/library/dataclasses.html
    # Structural type info, fast.
    
    from dataclasses import dataclass
    
    @dataclass
    class MyDataclass:
      foo: int
      bar: str
    
    
    ## SIMPLE NAMESPACE
    # https://docs.python.org/3/library/types.html#types.SimpleNamespace
    # No structural type info, fast.
    # The structure can change, it is useful when working with JSON, see e.g.
    # https://medium.com/@shashank_iyer/
    #   simplify-json-access-with-simplenamespace-e91f5a09345b
    
    from types import SimpleNamespace
    
    
    ## NAMED TUPLE
    # https://docs.python.org/3/library/typing.html#typing.NamedTuple
    # Structural type info, slightly slower than above solutions.
    # It is a tuple under the hood.
    
    from typing import NamedTuple
    
    MyNamedTuple = NamedTuple('MyNamedTuple', foo=int, bar=str)
    
    
    ## CLASS WITH PROTOCOL (not recommened)
    # https://docs.python.org/3/library/typing.html#typing.Protocol
    # Structural type info, 10 to 80 times slower than above solutions!
    
    from typing import Protocol
    
    class MyClass(Protocol):
      foo: int
      bar: str
    
    def make_my_class(fooparam: int, barparam: str) -> MyClass:
      class my_class:
        foo = fooparam
        bar = barparam
      return my_class
    
    
    
    ### USAGE
    
    
    ## PERFORMANCE
    
    from timeit import timeit
    
    def performance(it, does, times=1_000_000):
      timing = timeit(does, number=times)
      print(f"{it:<12} {timing:6.3f}")
    
    performance('instance', lambda: MyInstance(1, 'Hello'))
    performance('dataclass', lambda: MyDataclass(1, 'Hello'))
    performance('namespace', lambda: SimpleNamespace(foo=1, bar='Hello'))
    performance('namedtuple', lambda: MyNamedTuple(foo=1, bar='Hello'))
    performance('class', lambda: make_my_class(fooparam=1, barparam='Hello'))
    
    # For comparision, the performance of simple datatypes
    performance('tuple', lambda: (1, 'Hello'))
    performance('list', lambda: [1, 'Hello'])
    performance('dict', lambda: {'foo': 1, 'bar': 'Hello'})
    performance('set', lambda: {1, 'Hello'})
    
    
    ## RESULTS
    
    # instance      0.478
    # dataclass     0.568
    # namespace     0.483
    # namedtuple    0.796
    # class        14.904  # varies between 10 and 25 seconds
    # tuple         0.344
    # list          0.591
    # dict          0.666
    # set           0.707
    

    Original Answer

    Protocol, suggested by abel1502, seems to do what I want:

    from typing import Protocol
    
    class X(Protocol):
      foo: str
      bar: int
      baz: None
    
    def get_an_x() -> X:
    
      class x:
        foo = 'the answer'
        bar = 42
        baz = None
    
      return x
    

    Now PyCharms type checker detects wrong types and non-existing or unknown attributes:

    # These are ok
    
    x: X = get_an_x()
    
    foo: str = x.foo
    bar: int = x.bar
    baz: None = x.baz
    
    # These are type errors
    
    unknown = x.unknownattribute
    wrong_type: int = x.foo
    wrong_x: str = get_an_x()
    
    def get_a_wrong_x() -> X:
    
      class x:
        foox = 'the answer'
        bar = '42'
    
      return x