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/

Reply via email to