I'm in favour of keyword-only arguments in dataclasses; however accepting arbitrary **kwargs seems pretty exotic to me. As Eric has suggested, this seems like a job for some kind of wrapper or decorator.
On Mon, 2021-09-20 at 14:28 +0000, thomas.d.mc...@gmail.com wrote: > Sorry for the double post, if the first one passed... I typed Enter > too soon by accident :( > > TL;DR: Add a `strict` keyword option to the dataclass constructor > which, by default (True), would keep the current behavior, but > otherwise (False) would generate an __init__ that accepts arbitrary > **kwargs and that passes them to an eventual __post_init__. > > Use case: I'm developing a client for a public API (that I don't > control). I'd like this client to be well typed so my users don't > have to constantly refer to the documentation for type information > (or just to know which attributes exist in an object). So I turned to > dataclasses (because they're fast and lead to super clean/clear > code). > > @dataclass > class Language: > iso_639_1: Optional[str] > name: Optional[str] > > Then my endpoint can look like this > > def get_language() -> Language: > result = requests.get(...) > return Language(**result.json) > > That's fine but it poses a problem if the API, the one I have no > control over, decides overnight to add a field to the Language model, > say 'english_name'. No change in the API number because to them, > that's not a breaking change (I would agree). Yet every user of my > client will see "TypeError: __init__() got an unexpected keyword > argument 'english_name'" once this change goes live and until I get a > chance to update the client code. Other clients return plain dicts or > dict wrappers with __get_attr__ functionality (but without > annotations so what's the point). Those wouldn't break. > > I've looked around for solutions and what I found > (https://stackoverflow.com/questions/55099243/python3-dataclass-with-kwargsasterisk > ) ranged from "you'll have to redefine the __init__, so really you > don't want dataclasses" to "define a 'from_kwargs' classmethod that > will sort the input dict into two dicts, one for the __init__ and one > of extra kwargs that you can do what you want with". > > Since I'm pretty sure I _do_ want dataclasses, that leaves me with > the second solution: the from_kwargs classmethod. I like the idea but > I'm not a fan of the execution. First, it means my dataclasses don't > work like regular ones, since they need this special factory. Second, > it does something that's pretty trivial to do with **kwargs, as we > can use **kwargs unpacking to sort parameters instead of requiring at > least 2 additional function calls (from_kwargs() and > dataclass.fields()), a loop over the dataclass fields and the > construction of yet another dict (all of which has a performance > cost). > > > My proposal would be to add a `strict=True` default option to the > dataclass constructor. the default wouldn't change a thing to the > current behavior. But if I declare: > > @dataclass(strict=False) > class Language: > iso_639_1: Optional[str] > name: Optional[str] > > > Then the auto-generated __init__ would look like this: > > def __init__(self, iso_639_1, name, **kwargs): > ... > self.__post_init__(..., **kwargs) # if dataclass has a > __post_init__ > > > This would allow us to achieve the from_kwargs solution in a much > less verbose way, I think. > > > @dataclass(strict=False) > class Language: > iso_639_1: Optional[str] > name: Optional[str] > > extra_info: dict = field(init=False) > > def __post_init__(self, **kwargs) > if kwargs: > logger.info( > f'The API returned more keys than expected for model > {self.__class__.__name__}: {kwargs.keys()}. ' > 'Please ensure that you have installed the latest > version of the client or post an issue @ ...' > ) > self.extra_info = kwargs > > > I'm not married to the name `strict` for the option, but I think the > feature is interesting, if only to make dataclasses *optionally* more > flexible. You don't always have control over the attributes of the > data you handle, especially when it comes from external APIs. Having > dataclasses that don't break when the attributes evolves can be a > great safeguard. > > Outside of my (somewhat specific, I'll admit) use-case, it would also > allow dataclasses to be used for types that are inherently flexible. > Imagine: > > @dataclass(strict=False) > class SomeTranslatableEntitiy: > name: Optional[str] > name_translations: dict[str, str] = field(init=False) > > def __post_init__(self, **kwargs) > self.name_translations = { > k: kwargs.pop(k) > for k, v in kwargs.keys() > if k.startswith('name_') # e.g: 'name_en', 'name_fr' > } > > Thanks for reading :) > _______________________________________________ > Python-ideas mailing list -- python-ideas@python.org > To unsubscribe send an email to python-ideas-le...@python.org > https://mail.python.org/mailman3/lists/python-ideas.python.org/ > Message archived at > https://mail.python.org/archives/list/python-ideas@python.org/message/MORDB6OKAGE2OKG5GIUTEIHZ2TYS4YB3/ > Code of Conduct: http://python.org/psf/codeofconduct/
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-le...@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/HBMPXFFTDOQRMHIG4XL5N6OXPB7QZ5WS/ Code of Conduct: http://python.org/psf/codeofconduct/