36

How do you make Optional attr's of a dataclass?

from dataclasses import dataclass

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: # what to write here?
    
kennys_stuff = {
    'knife': True,
    'fork': True
}

print(CampingEquipment(**kennys_stuff))

I tried field(init=False), but it gave me:

TypeError: CampingEquipment.__init__() missing 1 required positional argument: 'missing_flask_size'

By Optional I mean __dict__ may contain the key "missing_flask_size" or not. If I set a default value then the key will be there and it shouldn't be in some cases. I want to check its type if it is there.

I tried moving the field(init=False) to the type location (after the colon) so I could make it more explicit as to the thing I wanted optional would be the key and not the value.

So I want this test to pass:

with pytest.raises(AttributeError):
    ce = CampingEquipment(**kennys_stuff)
    print(ce.missing_flask_size)
mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Back2Basics
  • 7,406
  • 2
  • 32
  • 45
  • 1
    Got it, so it looks like a current answer already addresses this issue, in that case. Also, wondering what’s the type of the optional field? If user were to pass it in to consutector I mean. – rv.kvetch Jan 23 '22 at 12:52
  • There is a lot of good information but the existing answers don't look like they answer the question. It could be that a data class is more like sql which have column names for everything in the table or not. – Back2Basics Jan 24 '22 at 07:50
  • 1
    in Python `3.10+` you could have optional types with `Optional[X]` or `X | None` (or `Union[X, None]`). – iraj jelodari Nov 04 '22 at 16:48

4 Answers4

48

It's not possible to use a dataclass to make an attribute that sometimes exists and sometimes doesn't because the generated __init__, __eq__, __repr__, etc hard-code which attributes they check.

However, it is possible to make a dataclass with an optional argument that uses a default value for an attribute (when it's not provided).

from dataclasses import dataclass
from typing import Optional

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: Optional[int] = None
    
kennys_stuff = {
    'knife':True,
    'fork': True
}

print(CampingEquipment(**kennys_stuff))

And it's possible to make a dataclass with an argument that's accepted to __init__ but isn't an actual field. So you could do something like this:

from dataclasses import dataclass, InitVar
from typing import Optional

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: InitVar[Optional[int]] = None

    def __post_init__(self, missing_flask_size):
        if missing_flask_size is not None:
            self.missing_flask_size = missing_flask_size

If you really want classes to either to have that attribute present or not have it at all, you could subclass your dataclass and make a factory function that creates one class or the other based on whether that missing_flask_size attribute is present:

from dataclasses import dataclass

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool


@dataclass
class CampingEquipmentWithFlask:
    missing_flask_size: int


def equipment(**fields):
    if 'missing_flask_size' in fields:
        return CampingEquipmentWithFlask(**fields)
    return CampingEquipment(**fields)
    

kennys_stuff = {
    'knife':True,
    'fork': True
}

print(CampingEquipment(**kennys_stuff))

If you really wanted to (I wouldn't recommend it though), you could even customize the __new__ of CampingEquipment to return an instance of that special subclass when that missing_flask_size argument is given (though then you'd need to set init=False and make your own __init__ as well on that class).

Abhijeet Kasurde
  • 3,937
  • 1
  • 24
  • 33
Trey Hunner
  • 10,975
  • 4
  • 55
  • 114
7

A field object is supposed to be used with =, like a default value, not : like an annotation.

Specifying init=False for a field means the caller can't pass in a value for it at all. init=False fields are supposed to be set in __post_init__, like this:

@dataclass
class Example:
    a: int
    b: int
    c: int = field(init=False)

    def __post_init__(self):
        self.c = self.a + self.b

print(Example(1, 2).c) # prints 3

If you want to make it optional for the caller to provide a value, you can set a default value. If the caller doesn't provide a value, the default will be used instead:

@dataclass
class Example:
    a: int
    b: int
    c: int = -1

print(Example(1, 2).c) # prints -1
print(Example(1, 2, 3).c) # prints 3
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • 1
    I'd also mention `field(default=...)` and in the case of mutable types, `field(default_factory=...)`, additionally as it's unclear what the type of the last field in the OP was. – rv.kvetch Jan 22 '22 at 05:26
-1

I was also looking for the same and find a workaround. Simply pop the item that you don't want if conditional. like this

@dataclass
class Element:
    type: str
    src: str
    scale: Scale 
    duration: float = -1
    start: float = 0
    extra_time: float = 0
    cache: bool = True

    def get_data(self) -> None:
        if self.type != "image":
            self.__dict__.pop('scale')
            return self.__dict__
        if self.type == "image":
            return self.__dict__

Simply pop the item that you don't want with conditional and call this method get_data(). Hope this will help.

  • This will modify the instance data itself whenever `get_data` is called; critically, calling `get_data` twice when the `type` is not `"image"` will fail because there is no `scale` to `pop` the second time! – MisterMiyagi Jan 18 '23 at 15:26
-2

Just add a value to optional field

@dataclass
class CampingEquipment:
   knife: bool
   fork: bool
   missing_flask_size: int = field(default=None)

# CampingEquipment(knife=True, fork=True, missing_flask_size=None)

See this post too How to create an optional field in a dataclass that is inherited?

Franz Kurt
  • 1,020
  • 2
  • 14
  • 14
  • 1
    That's not how `field` is supposed to be used. – user2357112 Jan 22 '22 at 02:17
  • But after edit, it’s still not right. Because dataclasses relies on type annotations to determine fields, so right now you are defining a class (static) variable, not a dataclass field. – rv.kvetch Jan 23 '22 at 12:50