Migrating from v1 to v2
v2 is a complete reboot of Telethon v1. Because a lot of the library has suffered radical changes, there are no plans to provide “bridge” methods emulating the old interface. Doing so would take a lot of extra time and energy, and it’s honestly not fun.
What this means is that your v1 code very likely won’t run in v2. Sorry. I hope you can use this opportunity to shake up your dusty code into a cleaner design, too.
The common theme in v2 could be described as “no bullshit”.
v1 had grown a lot of features. A lot of them did a lot of things, at all once, in slightly different ways. Semver allows additions, so v2 will start out smaller and grow in a controlled manner.
Custom types were a frankestein monster, combining both raw and manually-defined properties in hacky ways. Type hinting was an unmaintained disaster. Features such as file IDs, proxies and a lot of utilities were pretty much abandoned.
The several attempts at making v2 a reality over the years starting from the top did not work out. A bottom-up approach was needed. So a full rewrite was warranted.
TLSharp was Telethon’s seed. Telethon v0 was needed to learn Python at all. Telethon v1 was necessary to learn what was a good design, and what wasn’t. This inspired grammers, a Rust re-implementation with a thought-out design. Telethon v2 completes the loop by porting grammers back to Python, now built with years of experience in the Telegram protocol.
It turns out static type checking is a very good idea for long-running projects. So I strongly encourage you to use mypy when developing code with Telethon v2. I can guarantee you will into far less problems.
Without further ado, let’s take a look at the biggest changes. This list may not be exhaustive, but it should give you an idea on what to expect. If you feel like a major change is missing, please open an issue.
Complete project restructure
The public modules under the telethon
now make actual sense.
The root
telethon
package contains the basics like theClient
andRpcError
.telethon.types
contains all the types, for your tpye-hinting needs.telethon.events
contains all the events.telethon.events.filters
contains all the event filters.telethon.session
contains the session storages, should you choose to build a custom one.telethon.errors
is no longer a module. It’s actually a factory object returning new error types on demand. This means you don’t need to wait for new library versions to be released to catch them.
Note
Be sure to check the documentation for telethon.errors
to learn about error changes.
Notably, errors such as FloodWaitError
no longer have a .seconds
field.
Instead, every value for every error type is always .value
.
This was also a good opportunity to remove a lot of modules that were not supposed to public in their entirety:
.crypto
, .extensions
, .network
, .custom
, .functions
, .helpers
, .hints
, .password
, .requestiter
, .sync
, .types
, .utils
.
TelegramClient renamed to Client
You can rename it with as
during import if you want to use the old name.
Python allows using namespaces via packages and modules.
Therefore, the full name telethon.Client
already indicates it’s from telethon
, so the old Telegram
prefix was redundant.
No telethon.sync hack
You can no longer import telethon.sync
to have most calls wrapped in asyncio.loop.run_until_complete()
for you.
Raw API is now private
v2 aims to comply with Semantic Versioning. This is impossible because Telegram is a live service that can change things any time. But we can get pretty close.
In v1, minor version changes bumped Telegram’s layer. This technically violated semver, because they were part of a public module.
To allow for new layers to be added without the need for major releases, telethon._tl
is instead private.
Here’s the recommended way to import and use it now:
from telethon import _tl as tl
was_reset = await client(tl.functions.account.reset_wall_papers())
if isinstance(chat, tl.abcs.User):
if isinstance(chat, tl.types.UserEmpty):
return
# chat is tl.types.User
There are three modules (four, if you count core
, which you probably should not use).
Each of them can have an additional namespace (as seen above with account.
).
tl.functions
contains every TL definition treated as a function. The naming convention now follows Python’s, and aresnake_case
.tl.abcs
contains every abstract class, the “boxed” types from Telegram. You can use these for your type-hinting needs.tl.types
contains concrete instances, the “bare” types Telegram actually returns. You’ll probably use these withisinstance()
a lot.
Most custom types
will also have a private _raw
attribute with the original value from Telegram.
Raw API has a reduced feature-set
The string representation is now on object.__repr__()
, not object.__str__()
.
All types use __slots__ to save space. This means you can’t add extra fields to these at runtime unless you subclass.
The .stringify()
methods on all TL types no longer exists.
Instead, you can use a library like beauty-print.
The .to_dict()
method on all TL types no longer exists.
The same is true for .to_json()
.
Instead, you can use a library like json-pickle or write your own:
def to_dict(obj):
if obj is None or isinstance(obj, (bool, int, bytes, str)): return obj
if isinstance(obj, list): return [to_dict(x) for x in obj]
if isinstance(obj, dict): return {k: to_dict(v) for k, v in obj.items()}
return {slot: to_dict(getattr(obj, slot)) for slot in obj.__slots__}
Lesser-known methods such as TLObject.pretty_format
, serialize_bytes
, serialize_datetime
and from_reader
are also gone.
The remaining methods are:
Serializable.constructor_id()
class-method, to get the integer identifier of the corresponding type constructor.Serializable.from_bytes()
class-method, to convert serializedbytes
back into the class.object.__bytes__()
instance-method, to serialize the instance intobytes
the way Telegram expects.
Functions are no longer a class with attributes.
They serialize the request immediately.
This means you cannot create request instance and change it later.
Consider using functools.partial()
if you want to reuse parts of a request instead.
Functions no longer have an asynchronous .resolve()
.
This used to let you pass usernames and have them be resolved to InputPeer automatically (unless it was nested).
Changes to start and client context-manager
You can no longer start()
the client.
Instead, you will need to first connect()
and then start the interactive_login()
.
In v1, the when using the client as a context-manager, start()
was called.
Since that method no longer exists, it now instead only connect()
and disconnect()
.
This means you won’t get annoying prompts in your terminal if the session was not authorized. It also means you can now use the context manager even with custom login flows.
The old sign_in()
method also sent the code, which was rather confusing.
Instead, you must now request_login_code()
as a separate operation.
The old log_out()
was also renamed to sign_out()
for consistency with sign_in()
.
The old is_user_authorized()
was renamed to is_authorized()
since it works for bot accounts too.
Unified client iter and get methods
The client no longer has client.iter_...
methods.
Instead, the return a type that supports both await
and async for
:
messages = await client.get_messages(chat, 100)
# or
async for message in client.get_messages(chat, 100):
...
Note
Client.get_messages()
no longer has funny rules for the limit
either.
If you await
it without limit, it will probably take a long time to complete.
This is in contrast to v1, where get
defaulted to 1 message and iter
to no limit.
Removed client methods and properties
No client.parse_mode
property.
Instead, you need to specify how the message text should be interpreted every time.
In send_message()
, use text=
, markdown=
or html=
.
In send_file()
and friends, use one of the caption
parameters.
No client.loop
property.
Instead, you can use asyncio.get_running_loop()
.
No client.conversation()
method.
Instead, you will need to design your own FSM.
The simplest approach could be using a global states
dictionary storing the next function to call:
from functools import partial
states = {}
@client.on(events.NewMessage)
async def conversation_entry_point(event):
if fn := state.get(event.sender.id):
await fn(event)
else:
await event.respond('Hi! What is your name?')
state[event.sender.id] = handle_name
async def handle_name(event):
await event.respond('What is your age?')
states[event.sender.id] = partial(handle_age, name=event.text)
async def handle_age(event, name):
age = event.text
await event.respond(f'Hi {name}, I am {age} too!')
del states[event.sender.id]
No client.kick_participant()
method.
This is not a thing in Telegram. It was implemented by restricting and then removing the restriction.
The old client.edit_permissions()
was renamed to Client.set_participant_restrictions()
.
This defines the restrictions a banned participant has applied (bans them from doing those things).
Revoking the right to view messages will kick them.
This rename should avoid confusion, as it is now clear this is not to promote users to admin status.
For administrators, client.edit_admin
was renamed to Client.set_participant_admin_rights()
for consistency.
You can also use the aliases on the Participant
, types.Participant.set_restrictions()
and types.Participant.set_admin_rights()
.
Note that a new method, Client.set_chat_default_restrictions()
, must now be used to set a chat’s default rights.
You can also use the alias on the Group
, types.Group.set_default_restrictions()
.
No client.download_profile_photo()
method.
You can simply use Client.download()
now.
Note that download()
no longer supports downloading contacts as .vcard
.
No client.set_proxy()
method.
Proxy support is no longer built-in.
They were never officially maintained.
This doesn’t mean you can’t use them.
You’re now free to choose your own proxy library and pass a different connector to the Client
constructor.
This should hopefully make it clear that most connection issues when using proxies do not come from Telethon.
No client.set_receive_updates
method.
It was not working as expected.
No client.catch_up()
method.
You can still configure it when creating the Client
, which was the only way to make it work anyway.
No client.action()
method.
No client.takeout()
method.
No client.qr_login()
method.
No client.edit_2fa()
method.
No client.get_stats()
method.
No client.edit_folder()
method.
No client.build_reply_markup()
method.
No client.list_event_handlers()
method.
These are out of scope for the time being. They might be re-introduced in the future if there is a burning need for them and are not difficult to maintain. This doesn’t mean you can’t do these things anymore though, since the Raw API is still available.
Telethon v2 is committed to not exposing the raw API under any public API of the telethon
package.
This means any method returning data from Telegram must have a custom wrapper object and be maintained too.
Because the standards are higher, the barrier of entry for new additions and features is higher too.
Removed or renamed message properties and methods
Messages no longer have raw_text
or message
properties.
Instead, you can access the types.Message.text
,
text_markdown
or text_html
.
These names aim to be consistent with caption_markdown
and caption_html
.
In v1, messages coming from a client used that client’s parse mode as some sort of “global state”.
Based on the client’s parse mode, v1 message.text
property would return different things.
But not all messages did this!
Those coming from the raw API had no client, so text
couldn’t know how to format the message.
Overall, the old design made the parse mode be pretty hidden. This was not very intuitive and also made it very awkward to combine multiple parse modes.
The forward
property is now forward_info
.
The forward_to
method is now simply forward()
.
This makes it more consistent with the rest of message methods.
The is_reply
, reply_to_msg_id
and reply_to
properties are now replied_message_id
.
The get_reply_message
method is now get_replied_message()
.
This should make it clear that you are not getting a reply to the current message, but rather the message it replied to.
The to_id
, via_input_bot
, action_entities
, button_count
properties are also gone.
Some were kept for backwards-compatibility, some were redundant.
The click
method no longer exists in the message.
Instead, find the right buttons
to click on.
The download
method no longer exists in the message.
Instead, use download
on the message’s file
.
HMMMM WEB_PREVIEW VS LINK_PREVIEW… probs use link. we’re previewing a link not the web
Event and filters are now separate
Event types are no longer callable and do not have filters inside them.
There is no longer nested class Event
inside them either.
Instead, the event type itself is what the handler will actually be called with.
Because filters are separate, there is no longer a need for v1 @events.register
either.
It also means you can combine filters with &
, |
and ~
.
Filters are now normal functions that work with any event. Of course, this doesn’t mean all filters make sense for all events. But you can use them in an unified manner.
Filters no longer support asynchronous operations, which removes a footgun.
This was most commonly experienced when using usernames as the chats
filter in v1, and getting flood errors you couldn’t handle.
In v2, you must pass a list of identifiers.
This means getting those identifiers is up to you, and you can handle it in a way that is appropriated for your application.
See also
In-depth explanation for Updates.
Behaviour changes in events
Events produced by the client itself will now also be received as updates.
This means, for example, that your events.NewMessage
handlers will run when you use Client.send_message()
.
This is needed to properly handle updates.
In v1, there was a backwards-compatibility hack that flagged results from the client as their “own”. But in some rare cases, it was possible to still receive messages sent by the client itself in v1. The hack has been removed so now the library will consistently deliver all updates.
events.StopPropagation
no longer exists.
In v1, all handlers were always called.
Now handlers are called in order until the filter for one returns True
.
The default behaviour is that handlers after that one are not called.
This behaviour can be changed with the check_all_handlers
flag in Client
constructor.
events.CallbackQuery
has been renamed to events.ButtonCallback
and no longer also handles “inline bot callback queries”.
This was a hacky workaround.
events.MessageRead
no longer triggers when the contents of a message are read, such as voice notes being played.
Albums in Telegram are an illusion. There is no “album media”. There is only separate messages pretending to be a single message.
events.Album
was a hack that waited for a small amount of time to group messages sharing the same grouped identifier.
If you want to wait for a full album, you will need to wait yourself:
pending_albums = {} # global for simplicity
async def gather_album(event, handler):
if pending := pending_albums.get(event.grouped_id):
pending.append(event)
else:
pending_albums[event.grouped_id] = [event]
# Wait for other events to come in. Adjust delay to your needs.
# This will NOT work if sequential updates are enabled (spawn a task to do the rest instead).
await asyncio.sleep(1)
events = pending_albums.pop(grouped_id, [])
await handler(events)
@client.on(events.NewMessage)
async def handler(event):
if event.grouped_id:
await gather_album(event, handle_album)
else:
await handle_message(event)
async def handle_album(events):
... # do stuff with events
async def handle_message(event):
... # do stuff with event
Note that the above code is not foolproof and will not handle more than one client. It might be possible for album events to be delayed for more than a second.
Note that messages that do not belong to an album can be received in-between an album.
Overall, it’s probably better if you treat albums for what they really are:
separate messages sharing a grouped_id
.
Streamlined chat, input_chat and chat_id
The same goes for sender
, input_sender
and sender_id
.
And also for get_chat
, get_input_chat
, get_sender
and get_input_sender
.
Yeah, it was bad.
Instead, events with chat information now always have a .chat
, with at least the .id
.
The same is true for the .sender
, as long as the event has one with at least the user identifier.
This doesn’t mean the .chat
or .sender
will have all the information.
Telegram may still choose to send their min
version with only basic details.
But it means you don’t have to remember 5 different ways of using chats.
To replace the concept of “input chats”, v2 introduces types.PeerRef
.
A “peer” represents either a User
, Group
or Channel
, much like Telegram’s Peer.
A “peer reference” represents just enough information to reference that peer without relying on Telethon’s cache.
This is the most efficient way to call methods like Client.send_message()
too.
The concept of “marked IDs” also no longer exists.
This means v2 no longer supports the -
or -100
prefixes on identifiers.
Using the raw Peer to wrap the identifiers is gone, too.
Instead, you’re strongly encouraged to use types.PeerRef
instances.
The concepts of of “entity” or “peer” are unified to peer. Overall, dealing with users, groups and channels should feel a lot more natural.
See also
In-depth explanation for Peers, users and chats.
Other methods like client.get_peer_id
, client.get_input_entity
and client.get_entity
are gone too.
While not directly related, client.is_bot
is gone as well.
You can use Client.get_me()
or read it from the session instead.
The telethon.utils
package is gone entirely, so methods like utils.resolve_id
no longer exist either.
Session cache no longer exists
At least, not the way it did before.
The v1 cache that allowed you to use just chat identifiers to call methods is no longer saved to disk.
Sessions now only contain crucial information to have a working client. This includes the server address, authorization key, update state, and some very basic details.
To work around this, you can use types.PeerRef
, which is designed to be easy to store.
This means your application can choose the best way to deal with them rather than being forced into Telethon’s session.
See also
In-depth explanation for Sessions.
StringSession no longer exists
If you need to serialize the session data to a string, you can use something like jsonpickle.
Or even the built-in pickle
followed by base64
or just bytes.hex()
.
But be aware that these approaches probably will not be compatible with additions to the Session
.