Search code examples
pythonpython-3.xpython-dataclasses

python dataclass `__init_subclass__` does not load the fields from subclass


Please check the code below

import dataclasses


@dataclasses.dataclass
class A:
    a: int = 10

    def __init_subclass__(cls, **kwargs):
        for f in dataclasses.fields(cls):
            print("     ", cls, f.name)


print("Defining Subclass B")


@dataclasses.dataclass
class B(A):
    b: int = 100


print("Defining Subclass C")


@dataclasses.dataclass
class C(B):
    c: int = 1000

The output is

Defining Subclass B
      <class '__main__.B'> a
Defining Subclass C
      <class '__main__.C'> a
      <class '__main__.C'> b

I was expecting

Defining Subclass B
      <class '__main__.B'> a
      <class '__main__.B'> b
Defining Subclass C
      <class '__main__.C'> a
      <class '__main__.C'> b
      <class '__main__.C'> c

Clearly the dunder method __init_subclass__ does not have the idea of fields in subclass yet as it is not loaded. Please let me know how can I get the expected output.


Solution

  • So, a decorator works the following way:

    @some_decorator
    class A:
        pass
    

    Is equivalent to:

    class A:
       pass
    A = some_decorator(A)
    

    So, the dataclass decorator which adds the __dataclass_fields__ attribute to the class object hasn't run when __init_subclass__ runs, because the entire class objects has been created already.

    You could manually inspect the __annotations__ (which is what dataclass relies on to create field objects anyway). Note, you will have to manually inspect all the classes in the MRO, taking care of doing it in reverse order and guarding against potential classes that lack annotations, so something like:

    import dataclasses
    
    
    @dataclasses.dataclass
    class A:
        a: int = 10
    
        def __init_subclass__(cls, **kwargs):
            for klass in  reversed(cls.mro()):
                # in case a base class lacks annotations, e.g. object
                annotations = getattr(klass, '__annotations__', {})
                for name, value in annotations.items():
                    print("     ", cls, name, value)
    
    print("Defining Subclass B")
    
    
    @dataclasses.dataclass
    class B(A):
        b: int = 100
    
    
    print("Defining Subclass C")
    
    
    @dataclasses.dataclass
    class C(B):
        c: int = 1000
    

    Could work for you.

    The above prints the following output for me:

    Defining Subclass B
          <class '__main__.B'> a <class 'int'>
          <class '__main__.B'> b <class 'int'>
    Defining Subclass C
          <class '__main__.C'> a <class 'int'>
          <class '__main__.C'> b <class 'int'>
          <class '__main__.C'> c <class 'int'>