Using __new__ in inherited dataclasses Using __new__ in inherited dataclasses python-3.x python-3.x

Using __new__ in inherited dataclasses


Just because the dataclass does it behind the scenes, doesn't mean you classes don't have an __init__(). They do and it looks like:

def __init__(self, person_id: int, country: Country):    self.person_id = person_id    self.country = country

When you create the class with:

CountryLinkFromISO2(123, 'AW')

that "AW" string gets passed to __init__() and sets the value to a string.

Using __new__() in this way is fragile and returning None from a constructor is fairly un-pythonic (imo). Maybe you would be better off making an actual factory function that returns either None or the class you want. Then you don't need to mess with __new__() at all.

@dataclassclass CountryLinkFromISO2(CountryLink):    @classmethod    def from_country_code(cls, person_id : int, iso2 : str):        if iso2 not in countries_by_iso2:            return None        return cls(person_id, countries_by_iso2[iso2])a = CountryLinkFromISO2.from_country_code(123, 'AW')

If for some reason it needs to work with __new__(), you could return None from new when there's no match, and set the country in __post_init__():

@dataclassclass CountryLinkFromISO2(CountryLink):    def __new__(cls, person_id : int, iso2 : str):        if iso2 not in countries_by_iso2:            return None        return super().__new__(cls)        def __post_init__(self):                self.country = countries_by_iso2[self.country]


The behaviour you see is because dataclasses set their fields in __init__, which happens after __new__ has run.

The Pythonic way to solve this would be to provide an alternate constructor. I would not do the subclasses, as they are only used for their constructor.

For example:

@dataclassclass CountryLink:    person_id: int    country: Country    @classmethod    def from_iso2(cls, person_id: int, country_code: str):        try:            return cls(person_id, countries_by_iso2[country_code])        except KeyError:            raise ValueError(f'invalid ISO2 country code {country_code!r}') from None    @classmethod    def from_iso3(cls, person_id: int, country_code: str):        try:            return cls(person_id, countries_by_iso3[country_code])        except KeyError:            raise ValueError(f'invalid ISO3 country code {country_code!r}') from Nonecountry_links = [ CountryLink.from_iso2(123, 'AW'),                  CountryLink.from_iso3(456, 'AFG'),                  CountryLink.from_iso2(789, 'AO')]