Angry robot logo

Mumbling about computers

Rust's Default in Python

Posted on under [ ]

In Rust, I've used and liked quite a bit the Default trait, which lets you instantiate a struct without providing all the members, as a short example:

#[derive(Debug, Default)]
struct MyStruct {
    value: i32,
}

fn main() {
    let default_struct: MyStruct = Default::default();
    println!("{:?}", default_struct); // Output: MyStruct { value: 0 }
}

A very important factor for me, is that this is recursive, as long as all members of each struct implement Default.

I had not found a similar thing in Python, but today I learned about dacite, which, in combination with dataclasses' default_factory can implement something very similar.

import dacite
from dataclasses import dataclass, field

@dataclass
class Cron:
    name: str
    command: str


@dataclass
class BackupConfig:
    enabled: bool
    bucket: str

    @staticmethod
    def default() -> "BackupConfig":
        return BackupConfig(enabled=False, bucket="default")

@dataclass
class HostConfig:
    backup: BackupConfig

    @staticmethod
    def default() -> "HostConfig":
        return HostConfig(
            backup=BackupConfig.default(),
        )

@dataclass
class HostData:
    config: HostConfig = field(default_factory=HostConfig.default)
    cron: list[Cron] = field(default_factory=list)

    @staticmethod
    def from_dict(data: dict) -> "HostData":
        return dacite.from_dict(data_class=HostData, data=data, config=dacite.Config(strict=True))


print(HostData.from_dict({}))
print(HostData.from_dict({"cron": [{"name": "do something", "command": "ls"}]}))

Which outputs

HostData(config=HostConfig(backup=BackupConfig(enabled=False, bucket='default')), cron=[])
HostData(config=HostConfig(backup=BackupConfig(enabled=False, bucket='default')), cron=[Cron(name='do something', command='ls')])

This is very different, in my opinion, from adding default values to every property.

By having default values (enabled: bool = False) it is possible to perform a partial instantiation, which, for me, is particularly troublesome when parsing user data -- I want the object to be entirely defined, or to entirely fall back to a default; I never want the object to be partially defined.

Default is also something you conciously implement; certain types do not have sane default values, such as BasicAuth(user, password) or Cron.

By using Type.default(), you are being explicit about instantiating a default instance of an object.