pythonpython-dataclasses

python dataclasses with optional attributes


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)

Solution

  • 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(CampingEquipment):
        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(equipment(**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).