Switching to Dataclasses

オープン
#5,279 コメント 7 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
25/100
issue の種類
リファクタリング
明瞭さ
説明が足りない
活発さ
静か
技術スタック
python

調査の方向性

Read the earlier discussion in issue #2698, then review the dataclasses Field documentation and the mentioned TO and TelegramObject internals. Map the affected transformations, inheritance behavior, class constants, and tests before choosing an approach; done requires an agreed migration design and a consistent implementation across the affected objects and tests.

索引モデルが issue の本文から書いたものです。

説明

🛠 breaking 🛠 refactor

Python dataclasses has been something this library has always eyed keenly. A lot of the work and discussion was done earlier in #2698, and a number of things have changed since that issue was created, which has once again opened the door for us to now actually switch to dataclasses.

Reiterating the main benefits we get from the switch:

  1. No more slots and __init__ boilerplate
  2. Letting dataclass machinery handle equality comparison for us
  3. Drastically less maintenance overhead, even when writing tests, since we don't need slots and equality tests.

Implementation

I'll go over things which were discussed in the issue and whether they are still applicable:

Minor details
  1. Immutability: The solution we went for is rolling our own version of freezing, via a _frozen attribute. Dataclasses have their own version via the frozen=True argument. Here are the pros/cons to using that:

    1. ➖ IISC, dataclasses does not have a way to temporarily unfreeze an object, which means using object.__setattr__ as a workaround, specially in tests.
    2. ➖/➕ There's been a few minor issues when mixing slots and frozen: https://github.com/python/cpython/issues/105936, though this has been fixed in later versions of python 3.13 and above? If users encounter this somehow below 3.13, we may just have to ask them to upgrade, but the scenario in that issue should not happen in our library.
    3. ➕ Initializing new classes is at 3-10x faster[^1] if we use the dataclass approach. I had codex write a script for that. The results were surprising since I was expecting the opposite..

Personally, I'm slightly in favour of using the dataclass frozen=True approach because of the "free" performance gain.

  1. Equality comparison: Dataclasses also has its own comparison method, using compare=True, which removes the need for our manual _id_attrs logic. I currently don't see any reason why we shouldn't use their approach. The only annoyance is a little more boilerplate to write if you don't want to include a field for equality comparsion. You have to do a field(eq=False). With frozen=True, the __hash__ method will also be handled for us and will check the attributes we want it to check.

  2. __repr__: Dataclasses can also do this for us, but now that we have our own approach, which customizes the look by dropping None values and empty containers, we can just stick to using that.

  3. __slots__: One of the showstoppers previously, we no longer have that issue since we only support 3.10+.

  4. __match_args__: Since we don't have a use for them, we should disable them globally for all TelegramObjects.

  5. Some class constants will need to be annotated with ClassVar, specially the Final ones.

  6. We should mention that dataclasses.asdict and replace is not explicitly supported by our API and users should use to_dict.

  7. Replacing __slots__ with fields() when wanting to get all attributes, this means almost all of our internal TO methods and even tests would need to change.

Major details
  1. Handling of Init only fields and transform like fields:

At the moment, we have a couple of classes which call other functions in the __init__, like calling to_timedelta, or parse_sequence_arg. For these kind of classes, I propose using the metadata field in Field. This metadata will contain the function for transformation.

TO.__post_init__ will be defined on the base class which will automatically run every time after the regular __init__. It's at this time we will iterate through any metadata fields and apply the transformations.

An additional use of the metadata field is to use it for deprecated arguments / attributes, e.g. duration can be an InitVar, and then we can use a regular @property to raise warnings. The metadata field can be used to store the value in _duration for example.

Side: We can't type hint the argument and attribute differently in dataclasses, so I propose to make the Sequence accepting arguments as tuple, since users are more likely to access attributes rather than initializing a class with them.

  1. Inheritance: This is still a problem as mentioned in https://github.com/python-telegram-bot/python-telegram-bot/issues/2698#issuecomment-1237415687.
    Possible solutions:

    i. The best solution imo is still making the defaulted inherited field as keyword only. This means that _BaseMedium, _BaseThumbedMedium, etc would make all of its default fields as kw only, which in turn changes the signature of classes which inherit them.
    This is of course a breaking change without a deprecation period, but the stability policy does outline an extreme case where we can't provide compatibility. Moreover, most of these classes are not typically instantiated by users.

    ii. Non-breaking: Override the __init__ generated from the dataclass, define a static list of positional arguments expected, and then check the passed arguments to check if they were passed positionally in the right order. I had GPT 5.5 do this, but I felt it was too black magicky and was over ~100 lines. It could work, but it can also be a maintainence burden if it's buggy.

    iii. Inline inherited arguments: This is basically removing the internal classes and all of its attributes and copying them over to the subclasses. That way we can control what order the class is instantiated with. Not a big fan of this either..


There could be 2nd order effects which I haven't forseen so far, but this should be the bulk of the changes.

What does the dev team feel about this?

[^1]: Benched on my uv installed python 3.14.6.

主要言語
Python
スター
29.5k
フォーク
6.2k
平均マージ
2日 1時間
マージ済み PR(30日)
14

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

python-telegram-bot/python-telegram-bot のほかの issue

python-telegram-bot/python-telegram-bot の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。