5

The following code creates a dataclass Obj with an int field n with default value 0.

from dataclasses import dataclass, field

@dataclass
class Obj:
    n: int = field(default_factory=int)
    
a = Obj()
print(a.n)
a.n = 0

Now, add an explicit __init__ constructor:

@dataclass
class Obj:
    n: int = field(default_factory=int)
        
    def __init__(self): # explicit constructor
        pass

It now generates this error claiming that the Obj object has no attribute named n:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [6], in <module>
      8         pass
     10 a = Obj()
---> 11 print(f'a.n = {a.n}')

AttributeError: 'Obj' object has no attribute 'n'

I thought maybe the explicit __init__ would override whatever field() is doing, but if we change from parameter default_factory to default, it works again:

@dataclass
class Obj:
    n: int = field(default=3)
        
    def __init__(self):
        pass
a.n = 3

This behavior appears in both Python 3.8 and 3.10.

8
  • 3
    This module provides a decorator and functions for automatically adding generated special methods such as __init__() and __repr__() to user-defined classes. so no wonder why you get attribute error when overriding __init__ again Commented Feb 4, 2022 at 13:01
  • 1
    Try instead using Pydantic as it servers dataclasses functionalities and much more than that Commented Feb 4, 2022 at 13:04
  • These comments are helpful @sudden_appearance -- but would be more helpful as an answer to the Q. (I would gladly upvote such an answer.) Commented Feb 4, 2022 at 13:13
  • @ybressler_simon ok got it Commented Feb 4, 2022 at 13:23
  • 1
    I wanted field() to provide a default value for the field whether or not I wrote my own custom constructor. One of the nice things about dataclasses is that you can see the fields and default values all in one organized place, instead of having initialization scattered throughout a constructor. I didn't realize that the way field() worked was by putting code in the auto-generated constructor. It's confusing to me because you can give default values in other ways while writing a custom constructor (e.g., just writing = 3 after the field name, or using the parameter default). Commented Feb 5, 2022 at 17:51

2 Answers 2

7

field doesn't really "do" anything; it just provides information that the dataclass decorator uses to define an __init__ that creates and initializes the n attribute. When you define your own __init__ method instead, it's your responsibility to make sure the field is initialized according to the definition provided by field. (The same goes for the other methods that dataclass would define.)

Sign up to request clarification or add additional context in comments.

2 Comments

I suppose my confusion is that field() DOES have an effect when you use default instead of default_factory, even when providing a custom __init__. But I'll chalk this up to some undocumented behavior in dataclasses that probably should not be relied on. Thanks!
Sort of. n isn't really an instance attribute initially; it's a class attribute that implements the descriptor protocol (sort of a property, but different). The definition of the descriptor does make use of the default value, but there's still no instance attribute named n unless __init__ defines it.
3

As I told in comments, the default meaning of dataclasses is to generate special methods just by using decorators. Python docs say

This module provides a decorator and functions for automatically adding generated special methods such as __init__() and __repr__() to user-defined classes.

So as far as you are overriding __init__ method again, you will get an AttributeError

Instead try using very popular Pydantic library, as it serves same functions and features that dataclasses do and serves a lot more powerful things such as validation and custom fields (EmailStr and so on), json parsing and others

Edit

The example of similar pydantic model

from pydantic import BaseModel, Field


class Obj(BaseModel):
    n: int = Field(default_factory=int)

    def __init__(self):
        super(Obj, self).__init__()
        ...


m1 = Obj()

print(m1.n)  # 0

and with default value

from pydantic import BaseModel, Field


class Obj(BaseModel):
    n: int = Field(5)

    def __init__(self):
        super(Obj, self).__init__()
        ...


m1 = Obj()

print(m1.n)  # 5

Although it might seem kind of similar, pydantic's Field provide much more kwargs for describing the value:

def Field(
    default: Any = Undefined,
    *,
    default_factory: Optional[NoArgAnyCallable] = None,
    alias: str = None,
    title: str = None,
    description: str = None,
    exclude: Union['AbstractSetIntStr', 'MappingIntStrAny', Any] = None,
    include: Union['AbstractSetIntStr', 'MappingIntStrAny', Any] = None,
    const: bool = None,
    gt: float = None,
    ge: float = None,
    lt: float = None,
    le: float = None,
    multiple_of: float = None,
    max_digits: int = None,
    decimal_places: int = None,
    min_items: int = None,
    max_items: int = None,
    unique_items: bool = None,
    min_length: int = None,
    max_length: int = None,
    allow_mutation: bool = True,
    regex: str = None,
    discriminator: str = None,
    repr: bool = True,
    **extra: Any,
) -> Any:

1 Comment

What would make this answer even more helpful is an example using Pydantic. (A lot of people are going to come across this answer, it would be great to give them the correct answer + with code as context) :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.