Type hint for dataclasses changes subclass variable

My understanding of the expected behaviour of a variable overwritten in a subclass is that:

  1. subclass().var should == the subclass.var not the baseclass.var
  2. type hints have no impact on Python code execution

But in the code below (abstracted from real live code):

# weird.py

from dataclasses import dataclass

@dataclass
class Base():
    var = 5
    def upload(self):
        print('inside base')

@dataclass
class Sub(Base):
    var = 10
    def upload(self):
        print(f"inside sub before super -> {self.var}")
        super().upload()
        print(f"inside sub after super -> {self.var}")


Sub().upload()

Running this:

#% python weird.py
#inside sub before super → 10
#inside base
#inside sub after super → 10

The above is what I expect.

But if I type hint var inside Base class

class Base():
    var: int = 5
    ....

then self.var shows the Base class var value of 5 not the Sub class value of 10.

#% python weird.py
#inside sub before super → 5
#inside base
#inside sub after super → 5

I’d appreciate if someone can explain why this is so. It’s not what I would “expect”.

I’m using Python 3.11

Thank you.

This is definitely confusing. It’s due to how the dataclass decorator works: it relies on type annotations to define the fields of the class, and also the default values for those fields.

In your first version, you’re defining a dataclass with no fields, because there is no type annotation on var. You can verify this by trying to create an instance with a different value: Base(var=10) will raise an error because its __init__ method doesn’t have that argument.

When you define var with a type annotation, the metaprogramming of @dataclass turns that into part of the initializer, with a default value that you’ve provided. When you ask for Sub() it inherits the initializer with the default value [1] and sets var to 5. You can also verify that Sub.var (the class attribute) is still 10.


  1. I believe it actually calls the empty initializer for Sub but then uses super() to call Base.__init__ ↩︎

@jamestwebber Thanks for the explanation. It’s a gotcha to keep in mind.