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')]