GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Switching to Dataclasses by aelkheir · Pull Request #5334 · python-telegram-bot/python-telegram-bot

Conversation

@aelkheir

@aelkheir aelkheir commented Aug 23, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Closes #5279
Discussed Priorly: #2698

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

zotabee reacted with thumbs up emoji zotabee reacted with eyes emoji
All reactions
  • 👍 1 reaction
  • 👀 1 reaction
aelkheir added 7 commits August 19, 2026 14:58
@aelkheir
Add typing_extensions
f5bdffd
@aelkheir
Set ruff typing-extensions=false
d6c35bd
@aelkheir
Add tg{dataclass,field} helpers
ae4210c
@aelkheir
Do ground work in TelegramObject
e20a074
@aelkheir
Add runtime support for specifier parameter alias
b4bbfa3
@aelkheir
Some typing improvements to _utils/dataclass.py
0026d93
@aelkheir
Migrate classes direclty under telegram/
12abacf
@aelkheir aelkheir changed the title Dataclasses WIP: Switching to Dataclasses Aug 23, 2026
aelkheir added 22 commits August 23, 2026 20:54
@aelkheir @github-actions
Add chango fragment for PR #5334
d19ccdc
@aelkheir
Finish up classes under telegram/**/
ea65eea
Except `inputmedia.py` classes and `Bot`
@aelkheir
Migrate Bot and ExtBot
8218317
@aelkheir
Remove test_slot_behaviour() where its automated
60c1ea0
That makes all TOs, other application classes still have their
tests
@aelkheir
Add missing default None to optional fields
cf8bea0
@aelkheir
Fix some enum copy-paste typos
edb225e
@aelkheir
Override dataclasses generated __hash__()
9e6d558
dataclasses generated `hash()` does not include `self.__class__`
so this is to preserve old behaviour of different classes, having
same field level hashes, not being equal
@aelkheir
Handle annotation replacements in generated __init__
2d88fd0
@aelkheir
Remove __[get/set]state__ that dataclasses generate
b17b6b8
@aelkheir
Update two tests that relied on uneeded type coercion
bfc7812
@aelkheir
Fix a test_chatmemberupdated failure
9c53a39
Note: this failure revealed a minor breaking change in how `__eq__()`
works, worth documenting and adding to changelog later
@aelkheir
Update test_telegramobject to use @tg_dataclass()
0a33d49
@aelkheir
Remove old "deepcopy preserves freezing" test
37705e0
Old test for a now breaking change unfortunately.
@aelkheir
Deprecate test_telegramobject test_subclasses_have_api_kwargs
92dd955
@aelkheir
Define _CredentialsBase.*_hash aliases as fields
057fde2
@aelkheir
Handle all the [un]freezing in test suite
fbd5b8b
@aelkheir
Migrate _inputmedia classes
cf6485b
@aelkheir
Simplify passportelementerrors migration
908a07b
@aelkheir
Restore TO hashing behaviour and add tests for it
bf850c9
I temporarily broke old TO.__hash__ behaviour that used identity hashing
when two objects had no _id_attrs (i.e empty tuple). Not sure why it was
that way since empty id attrs mean their equall in terms of __eq__ (yes
we did issue a warning but still), so they should have same hash.

Anyways that should be restored now because otherwis it would be a
breaking change.
@aelkheir
Restore equality for direct TO instances
557c9b9
@aelkheir
Fix minor bugs in some field definitions
2aa3421
@aelkheir
Document all breaking changes in changelog
01bb8c5
aelkheir added 3 commits September 23, 2026 00:40
@aelkheir
Merge remote-tracking branch 'upstream/master' into dataclasses
bd568f7
 Conflicts:
	tests/_files/test_animation.py
	tests/_files/test_audio.py
	tests/_files/test_chatphoto.py
	tests/_files/test_document.py
	tests/_files/test_photo.py
	tests/_files/test_sticker.py
	tests/_files/test_video.py
	tests/_files/test_videonote.py
	tests/_files/test_videoquality.py
	tests/_files/test_voice.py
	tests/test_forum.py
@aelkheir
Fix some test_poll failures
2389cae
Same breaking change caused by a bug in old `__eq__()`.
tl;dr previously __eq__ allowed parent == child # True
@aelkheir
Unify TelegramObject comparison tests after removing warnings
ed5bc39
@aelkheir
aelkheir marked this pull request as ready for review September 23, 2026 17:29
@aelkheir

aelkheir commented Sep 23, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member Author

So I had this drafted for a bit of time now, I intended to send it earlier on to get your feedback on it, especially the logic around the after-processing of generated __init__ and whether that path seems acceptable to you.

Then I was tempted to walk till (near) the finish line and see how (simple v.s complicated) it will turn to be.


Future Stability Policy handling
arg: InitVar and _arg = field(init=False)

This did not age well.

from dataclasses import InitVar, dataclass, field


@dataclass(frozen=True)
class Example:
    value: InitVar[int | None] = None
    _value: int | None = field(init=False, default=None)

    def __post_init__(self, value: int | None, /):
        object.__setattr__(self, "_value", value)

    @property
    def value(self) -> int | None:
        print("value is deprecated and will be removed")
        return self._value


a = Example()
print(a._value)  # output: <property object at 0x7ff25e629e40>

b = Example(10)
print(b._value)  # output: 10

When not providing an explicit argument for value, the default None is overwritten by the property object.
A relevant CI issue also is that, both linting (ruff) and type checking (mypy) report:
error: Name "value" already defined/Redefinition of unused \value``

So i tried giving this alias Field specifier parameters a shot:

alias is an optional str parameter that provides an alternative name for the field. This alternative name is used in the synthesized init method.

The outcome of it is pretty neat and less verbose, however, and just like converter we have to provide runtime impl

Aliased parameter MWE
# No runtime support fo alias, this is just to demonstrate type checkers support
from dataclasses import MISSING, dataclass, field
from typing import Any, TypeVar, dataclass_transform

_T = TypeVar("_T")


def tg_field(
    *,
    default: Any = MISSING,
    alias: str | None = None,
) -> Any:
    return field(default=default)


@dataclass_transform(field_specifiers=(tg_field,))
def tg_dataclass(cls: type[_T]) -> type[_T]:
    return dataclass(frozen=True)(cls)


@tg_dataclass
class Example:
    _value: int | None = tg_field(alias="value", default=None)

    @property
    def value(self) -> int | None:
        print("value is deprecated and will be removed")
        return self._value


# Wroks in runtime
c = Example(_value=10)

# TypeError: Example.__init__() got an unexpected keyword argument 'value'
d = Example(value=10)

Its well supported in type checkers that common IDEs use

IDE Support Pyright Neovim with Pyright

VSCode Pylance
image

Pycharm, should be supported from 2024.3 onwards. https://youtrack.jetbrains.com/issue/PY-73832/Pycharm-doesnt-properly-recognizes-aliases-for-Pydantic-dataclass
image

mypy also support aliases running it through the above MWE gives:
Unexpected keyword argument "_value" for "Example"; did you mean "value"? [call-arg]

I honestly don't know what to do about temp unfreezing ...

I just did nothing. Excluding test files, there was very minimal unfreezing of objects outside of old __init__s, so I just object.__seattr__ these. For tests, mutations use a combination of a new with _unfrozen(obj) helper and object.__seattr__

CI

Sadly pylint and mypy have a bunch of false positives that we'll have to fight some way.
pylint: pylint-dev/pylint#4899, pylint-dev/pylint#7437
mypy: python/mypy#17547


I still got some polishing to do, specifically I want to:

  • Remove the enum converters and simplify with functool.partial
  • Add a TO._set(). object.__setattr__ was still a one-liner but it takes too much horizontal space
  • Add deprecation notes to the changelog, mostly parameter order changes
  • Add tests for _utils/dataclass.py
  • Update Checklist for PRs
  • Adapt test_official as needed (non needed)
  • Experiment with no longer tampering with the generated __init__ signatures and annotations and instead make TO._de_json() dataclasses-first, meaning to look up the correct annotation from dataclasses.fields(obj) directly rather than inpecting __init__. (For later)
  • Apply similar treatment to test_official (For later)

The last two points can further simplify _utils/dataclass.py, however the current implementation of the helper is the most truthful about what __init__ actually accepts but also fights python and standard dataclasses behaviour.

I'm going to be working on these items in the following days, would be great to get some initial review since tests/ are passing now

Doesn't have to be the whole thing, just _utils/dataclass.py and the changelog.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@aelkheir aelkheir changed the title WIP: Switching to Dataclasses Switching to Dataclasses Sep 23, 2026
harshil21
harshil21 reviewed Oct 11, 2026
View reviewed changes

@harshil21 harshil21 left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First round of reviews! Overall, I'm pretty happy with the implementation so far. I only looked at TGO.py and dataclasses.py in this round.

Experiment with no longer tampering with the generated init signatures and annotations and instead make TO._de_json() dataclasses-first,

I would probably leave this for a future PR (in the interest of time), I think we have right now can work fine.

One thing I would double check is if backward / forward compatibility isn't affected by this. I know we have tests, but can't hurt to check if they work with these changes..

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml
Comment on lines +6 to +7
- Direct instances of :class:`~telegram.TelegramObject` are frozen by default.
- Loading a pickled :class:`~telegram.TelegramObject` will return a frozen instance by default.

@harshil21 harshil21 Sep 30, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already existing behaviour?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@aelkheir aelkheir Oct 11, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True for subclasses
But if you initialize a TelegramObject directly it used to come unfrozen.
We had a comment there saying it was to make subclasses not have to call unfrozen in every single one of their inits

For pickling, previously unpickling a (unfrozen when pickled) object would load it unfozen just like it was. Now it will be loaded frozen regardless.

Thanks I'll now rephrase that second point i can see it can be unclear

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

harshil21 reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread src/telegram/_utils/dataclass.py
"""Special marker indicating that an entry was deleted."""


def to_sequence_annotation(annotation: object) -> object:

@harshil21 harshil21 Sep 30, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a docstring showing why this is needed would be nice

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

aelkheir reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread src/telegram/_utils/dataclass.py

from telegram._utils.de_json import unwrap_optional

_T = TypeVar("_T")

@harshil21 harshil21 Oct 11, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use the Self type now that we're dropping 3.10 support

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

aelkheir reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread src/telegram/_utils/dataclass.py
return cls

aliased_init.__signature__ = signature # type: ignore[attr-defined]
cls.__init__ = aliased_init # type: ignore[method-assign]

@harshil21 harshil21 Oct 11, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means that during initialization, the for loop of aliased_init is going to run every time checking every field. If we replace has_aliases with:

alias_pairs = tuple(
    (dataclass_field.name, alias)
    for dataclass_field in fields.values()
    if dataclass_field.init
    and (alias := dataclass_field.metadata.get(ALIAS_KEY)) is not None
)

and then just do the for loop with the alias pairs then that should be slightly faster.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/telegram/_utils/dataclass.py
def tg_dataclass(
*,
eq: bool = True,
) -> Callable[[type[_T]], type[_T]]:

@harshil21 harshil21 Oct 11, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put a docstring for this and tg_field

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

aelkheir reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread src/telegram/_utils/dataclass.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread src/telegram/_telegramobject.py
Comment on lines +137 to +138
for dataclass_field in dataclasses.fields(self):
converter = dataclass_field.metadata.get(CONVERTER_KEY)

@harshil21 harshil21 Oct 11, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can do a similar thing here like my previous comment about aliases: cache the fields which need a converter and only iterate over those, instead of every field

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/telegram/_telegramobject.py

if TYPE_CHECKING:
# FIXME: not sure what to do about this
# mypy doesn't recognize converters currently

@harshil21 harshil21 Oct 11, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: it's time to migrate to ty and drop mypy

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@harshil21 harshil21 mentioned this pull request Oct 23, 2026
Full Support for Bot API 10.1 #5261
Open
22 tasks
aelkheir added 5 commits October 25, 2026 01:56
@aelkheir
Replace per class enum converters with central functools.partial
e936fb8
@aelkheir
Adapt test_enum_types regexes to look for dc fields
3f0c8b5
@aelkheir
Refactor scattered object.__setattr__ into new TO._set()
45858c5
@aelkheir
Add a deprecation note to PassportElementError docstring
cf10f4c
@aelkheir
Add deprecation notes to changelog fragment
f6a635f
@harshil21 harshil21 mentioned this pull request Nov 11, 2026
Add DeserializationTestBase and de_json meta-tests #5401
Draft
3 tasks done
aelkheir added 3 commits November 11, 2026 16:18
@aelkheir
Use correct (API confirming) annotations for duration fields
9518d6e
@aelkheir
Add tests
3fb094e
Hallelujah
@aelkheir
Make test_official green again (as green as master)
1a1ae76

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@harshil21 harshil21 harshil21 left review comments

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Switching to Dataclasses

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@aelkheir @harshil21
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.