[issue44365] Bad dataclass post-init example

New submission from Micael Jarniac <micael@jarniac.com>: https://docs.python.org/3/library/dataclasses.html#post-init-processing https://github.com/python/cpython/blob/3.9/Doc/library/dataclasses.rst#post-... In the example, a base class "Rectangle" is defined, and then a "Square" class inherits from it. On reading the example, it seems like the Square class is meant to be used like:
square = Square(5)
Since the Square class seems to be supposed to be a "shortcut" to creating a Rectangle with equal sides. However, the Rectangle class has two required init arguments, and when Square inherits from it, those arguments are still required, so using Square like in the above example, with a single argument, results in an error:
square = Square(5) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __init__() missing 2 required positional arguments: 'width' and 'side'
To "properly" use the Square class, it'd need to be instantiated like so:
square = Square(0, 0, 5) square Square(height=5, width=5, side=5)
Which, in my opinion, is completely counter-intuitive, and basically invalidates this example. ---------- assignee: docs@python components: Documentation messages: 395427 nosy: MicaelJarniac, docs@python priority: normal severity: normal status: open title: Bad dataclass post-init example type: behavior versions: Python 3.8 _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Change by Karthikeyan Singaravelan <tir.karthi@gmail.com>: ---------- nosy: +eric.smith _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Eric V. Smith <eric@trueblade.com> added the comment: Agreed that that's not a good (or even workable) example. Thanks for pointing it out. I'll come up with something better. ---------- assignee: docs@python -> eric.smith versions: +Python 3.10, Python 3.11, Python 3.9 _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Eric V. Smith <eric@trueblade.com> added the comment: The example was added in https://github.com/python/cpython/pull/25967 When reviewing it, I think I missed the fact that the base class is a dataclass. The example and text make more sense if Rectangle isn't a dataclass. Still, I don't like the example at all. I think deleting it might be the best thing to do. Or maybe come up with a case where the base class is some existing class in the stdlib that isn't a dataclass. ---------- _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Micael Jarniac <micael@jarniac.com> added the comment: I'm trying to think of an example, and what I've thought of so far is having a base dataclass that has a `__post_init__` method, and another dataclass that inherits from it and also has a `__post_init__` method. In that case, the subclass might need to call `super().__post_init__()` inside its own `__post_init__` method, because otherwise, that wouldn't get called automatically. Something along those lines:
from dataclasses import dataclass, field
@dataclass ... class A: ... x: int ... y: int ... xy: int = field(init=False) ... ... def __post_init__(self) -> None: ... self.xy = self.x * self.y ... @dataclass ... class B(A): ... m: int ... n: int ... mn: int = field(init=False) ... ... def __post_init__(self) -> None: ... super().__post_init__() ... self.mn = self.m * self.n ... b = B(x=2, y=4, m=3, n=6) b B(x=2, y=4, xy=8, m=3, n=6, mn=18)
In this example, if not for the `super().__post_init__()` call inside B's `__post_init__`, we'd get an error `AttributeError: 'B' object has no attribute 'xy'`. I believe this could be an actual pattern that could be used when dealing with dataclasses. ---------- _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Eric V. Smith <eric@trueblade.com> added the comment: I'm not sure directly calling __post_init__ is a good pattern. Why would not calling __init__, like you would with any other class, not be the preferred thing to do? ---------- _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Micael Jarniac <micael@jarniac.com> added the comment: Well, at least for this example, to call `super().__init__()`, I'd need to provide it the two arguments it expects, `x` and `y`, otherwise it'd give an error:
TypeError: __init__() missing 2 required positional arguments: 'x' and 'y'
If I try calling it as `super().__init__(self.x, self.y)`, I get an infinite recursion error:
RecursionError: maximum recursion depth exceeded while calling a Python object
That's mostly why I've chosen to call `__post_init__` instead. And if we're dealing with `InitVar`s, they can nicely be chained like so:
from dataclasses import dataclass, field, InitVar
@dataclass ... class A: ... x: int ... y: InitVar[int] ... xy: int = field(init=False) ... ... def __post_init__(self, y: int) -> None: ... self.xy = self.x * y ... @dataclass ... class B(A): ... m: int ... n: InitVar[int] ... mn: int = field(init=False) ... ... def __post_init__(self, y: int, n: int) -> None: ... super().__post_init__(y) ... self.mn = self.m * n ... b = B(x=2, y=4, m=3, n=6) b B(x=2, xy=8, m=3, mn=18)
---------- _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Andrei Kulakov <andrei.avk@gmail.com> added the comment: How about this example: @dataclass class Rect: x: int y: int r=Rect(5,2) @dataclass class HyperRect(Rect): z: int def __post_init__(self): self.vol = self.x*self.y*self.z hr=HyperRect(5,2,3) print("hr.vol", hr.vol) Hyper Rectangle: https://en.wikipedia.org/wiki/Hyperrectangle ---------- nosy: +andrei.avk _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Eric V. Smith <eric@trueblade.com> added the comment: I was thinking about something like: @dataclass class FtpHelper(ftplib.FTP): my_host: str my_user: str lookup_password: InitVar[Callable] def __post_init__(self, lookup_password): super().__init__(host=self.my_host, user=self.my_user, passwd=lookup_password()) def get_password(): return "a password" ftp = FtpHelper(hostname, username, get_password) ---------- _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

Andrei Kulakov <andrei.avk@gmail.com> added the comment: It's a good example, but some readers might only have a vague idea (if any) of what FTP is, so a self contained example might be easier to digest? ---------- _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

da2ce7 <me@da2ce7.com> added the comment: I have made a slightly more comprehensive example. See file attached. Please consider for the updated documentation. ---------- nosy: +da2ce7 Added file: https://bugs.python.org/file50414/dataclass_inheritance_test.py _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

da2ce7 <me@da2ce7.com> added the comment: Upon Self Review, I think that this slightly updated version is a bit more illustrative. ---------- Added file: https://bugs.python.org/file50415/dataclass_inheritance_v2_test.py _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________

da2ce7 <me@da2ce7.com> added the comment: Amazingly, the original example needs a very small change to make it work as expected: @dataclass class Rectangle: height: float width: float @dataclass class Square(Rectangle): side: float height: float = field(init=False) width: float = field(init=False) def __post_init__(self) -> None: super().__init__(self.side, self.side) I discover this now, after playing around for a while. Attached is the simplified version of my expanded example testcase. ---------- Added file: https://bugs.python.org/file50416/dataclass_inheritance_v3_test.py _______________________________________ Python tracker <report@bugs.python.org> <https://bugs.python.org/issue44365> _______________________________________
participants (5)
-
Andrei Kulakov
-
da2ce7
-
Eric V. Smith
-
Karthikeyan Singaravelan
-
Micael Jarniac