From e2293d97f36e45ec7226c462bc492cb6567752f3 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 12 Feb 2026 22:38:23 +0000 Subject: [PATCH] gh-144764: Made dataclasses construct automatic docstrings lazily MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the default docstring construction to occur on-access through a descriptor. Verified with [tprof](https://github.com/adamchainz/tprof) and this script that generates 10k dataclasses: ```py from dataclasses import dataclass for i in range(10_000): @dataclass class Example: field1: int field2: str field3: float ``` **Before:** ``` $ tprof -t dataclasses._process_class example.py 🎯 tprof results: function calls total mean ± σ min … max dataclasses._process_class() 10000 5s 485μs ± 120μs 458μs … 6ms ``` After: ``` $ PYTHONPATH=Lib/ uvx tprof -t dataclasses._process_class example.py 🎯 tprof results: function calls total mean ± σ min … max dataclasses._process_class() 10000 3s 275μs ± 131μs 245μs … 6ms ``` The mean time spent in `_process_class()` has dropped from 485μs to 275μs, a ~42% time saving (admittedly skewed due to the small size of the dataclass). --- Lib/dataclasses.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 730ced7299865e..0a092d7a452154 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1209,23 +1209,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, if hash_action: cls.__hash__ = hash_action(cls, field_list, func_builder) - # Generate the methods and add them to the class. This needs to be done - # before the __doc__ logic below, since inspect will look at the __init__ - # signature. + # Generate the methods and add them to the class. func_builder.add_fns_to_class(cls) - if not getattr(cls, '__doc__'): - # Create a class doc-string. - try: - # In some cases fetching a signature is not possible. - # But, we surely should not fail in this case. - text_sig = str(inspect.signature( - cls, - annotation_format=annotationlib.Format.FORWARDREF, - )).replace(' -> None', '') - except (TypeError, ValueError): - text_sig = '' - cls.__doc__ = (cls.__name__ + text_sig) + if not cls.__doc__: + cls.__doc__ = _DocDescriptor() if match_args: # I could probably compute this once. @@ -1243,6 +1231,21 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, return cls +class _DocDescriptor: + def __get__(self, obj, owner): + # Create a class doc-string. + try: + # In some cases fetching a signature is not possible. + # But, we surely should not fail in this case. + text_sig = str(inspect.signature( + owner, + annotation_format=annotationlib.Format.FORWARDREF, + )).replace(' -> None', '') + except (TypeError, ValueError): + text_sig = '' + owner.__doc__ = (owner.__name__ + text_sig) + return owner.__doc__ + # _dataclass_getstate and _dataclass_setstate are needed for pickling frozen # classes with slots. These could be slightly more performant if we generated # the code instead of iterating over fields. But that can be a project for