In 3.10 you can specify keyword-only arguments to dataclasses. See
https://docs.python.org/3.10/library/dataclasses.html, search for kw_only.
This doesn't address Thomas's issue in any way, but I thought I'd
mention it.
Eric
On 9/20/2021 3:36 PM, Paul Bryan wrote:
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
<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
<mailto:python-ideas@python.org>
To unsubscribe send an email to python-ideas-le...@python.org
<mailto:python-ideas-le...@python.org>
https://mail.python.org/mailman3/lists/python-ideas.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/
<https://mail.python.org/archives/list/python-ideas@python.org/message/MORDB6OKAGE2OKG5GIUTEIHZ2TYS4YB3/>
Code of Conduct: http://python.org/psf/codeofconduct/
<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/
_______________________________________________
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/LICU7STMPISDXDA4ZOK7E4IQQMBIGTL7/
Code of Conduct: http://python.org/psf/codeofconduct/