# python-telegram talks to Telegram as a user, not as a bot

> python-telegram is a Python binding for TDLib, TDLib, so it signs in with a phone number over MTProto and can do anything a real client can, rather than going through the HTTP Bot API. Its distinguishing practical detail is that four wheels bundle the TDLib binary itself, statically linked against OpenSSL and zlib, so a Linux or macOS install needs nothing from the system.

**alexander-akhmetov/python-telegram** — Python client for the Telegram's tdlib

- Repository: https://github.com/alexander-akhmetov/python-telegram
- Stars: 724 · Forks: 136
- Language: Python
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/alexander-akhmetov-python-telegram

## It is TDLib, so it is a user and not a bot

python-telegram is a Python API for the tdlib library, and it helps you build your own Telegram clients.

The distinction that defines the library is stated in three lines. `tdlib` connects to Telegram over MTProto, the same protocol the official apps use. This library signs in as a full Telegram account with a phone number, and it can do what a regular client can do. And it is not a wrapper around the HTTP Bot API.

That last line is the one to hold on to, because the bot API is the obvious way to talk to Telegram from Python and it comes with real restrictions. A bot API token gives you a bot identity, with its own API surface and its own limits. A TDLib client is a user, with the user's chats, contacts, media and history, authenticated with a phone number and a code.

The escape hatch is stated too. If you only need a bot, python-telegram-bot is a better fit. You can still sign in as a bot here by passing `bot_token` instead of `phone`.

So the library serves the case where a bot identity is not enough but a full client is more than you want to build. The examples in the repository make that clear: send_message, echo_bot, bot_login, get_me, get_me_non_blocking_login, chat_stats, clear_group_messages, get_instant_view and a shared utils module.

The project is MIT licensed, Python, with the last push on 2026-10-19 and releases 2.0.0 from 2026-08-23, 1.0.0 from 2026-07-24 and 0.19.0 from 2024-06-23, so the 1.0 line is recent and the 2.0 line is where it is now.

## The wheels carry TDLib, so there is no build step

The installation is one command, with a platform caveat stated up front:

```shell
pip install python-telegram
```

This library requires Python 3.10 or newer, and Windows is not supported.

The part that makes the install painless is the TDLib bundling. Four wheels bundle a `tdlib` binary, and each one works on Python 3.10 through 3.14:

| Platform | Wheel | Needs at least |
| --- | --- | --- |
| Linux x86_64 | `manylinux_2_28_x86_64` | glibc 2.28 |
| Linux aarch64 | `manylinux_2_28_aarch64` | glibc 2.28 |
| macOS arm64 | `macosx_11_0_arm64` | macOS 11 |
| macOS x86_64 | `macosx_10_15_x86_64` | macOS 10.15 |

Those binaries link OpenSSL and zlib statically, so they need nothing from the system. That is the whole reason the four-wheel table matters: TDLib is normally a C++ project you compile yourself, and these wheels remove that step for the common platforms.

Anywhere else, pip installs the source distribution, which carries no binary, and on musl, on 32-bit ARM, or with an older glibc, you compile `tdlib` yourself.

The Dockerfile shows the same principle applied to a container, and the comment explains why the flag is there. Without --only-binary, pip falls back to the binary-free sdist and the image ships an install that raises TDLibNotFoundError on first use. So the image installs with `python3 -m pip install --only-binary=:all: python-telegram` on a python:3.14.6-slim-trixie base.

## The lookup order for TDLib is documented so you can override it

The library looks for the TDLib library in a defined order, and it logs which one it used.

First the `library_path` argument. Second the `PYTHON_TELEGRAM_TDLIB_PATH` environment variable. Third a system-wide `tdjson`. Fourth the bundled binary.

The consequence of that order is worth understanding. A system-wide `tdlib` therefore wins over the bundled one, so if you have one installed on the machine it will be used in preference to the wheel's copy, and if it is a different version you will not get a warning, just different behaviour.

To point at a specific build, pass its path. The file is called `libtdjson.so` on Linux and `libtdjson.dylib` on macOS:

```python
tg = Telegram(
    # ...
    library_path="/usr/local/lib/libtdjson.so",
)
```

And if nothing can be found, `TDJson()` raises `TDLibNotFoundError`, which is an `OSError` subclass whose message names the platform, the paths tried and the overrides.

That error is the practical reason to read this section before you start. A failure to find the library on an unsupported platform is not a generic import error, it is a named exception that tells you which paths were searched, and the project has clearly put effort into making the message useful rather than merely raising something.

The build configuration explains where the bundled binary comes from. The libtdjson binary is not in git; hatch_build.py adds it to the wheel and retags it, driven by PYTHON_TELEGRAM_TDLIB_TARGET and a tdlib.lock file in the repository root.

## Every call returns an AsyncResult because TDLib is asynchronous

The basic example shows the whole shape of the API in one block, and the first thing to understand is the constructor.

```python
from telegram.client import Telegram
from telegram.text import Spoiler

tg = Telegram(
    api_id=123456,
    api_hash="api_hash",
    phone="+31611111111",  # you can pass 'bot_token' instead
    database_encryption_key="changekey123",
    files_directory="/tmp/.tdlib_files/",
)
tg.login()
```

The api_id and api_hash come from registering a new Telegram application, and the phone is the account you sign in as, with the comment pointing at the bot_token alternative. The database_encryption_key and files_directory are TDLib's own local storage parameters, and both matter: the encryption key protects the local TDLib database, and the files directory is where downloaded media lands.

Then the calls, and every one of them returns an `AsyncResult`:

```python
# The chat must be in the tdlib database before you can send a message to it.
# `get_chats` loads up to `limit` chats from the main chat list.
result = tg.get_chats(limit=100)
result.wait()

chat_id = 123456789
result = tg.send_message(chat_id, Spoiler("Hello world!"))

# `tdlib` is asynchronous, so `python-telegram` always returns an `AsyncResult` object.
# You can receive a result with the `wait` method of this object.
result.wait()
print(result.update)

tg.stop()  # You must call `stop` at the end of the script.
```

The reason for the AsyncResult is that TDLib is asynchronous, and the result object carries the update payload once the call completes. The comment about the chat needing to be in the TDLib database before sending is a real practical constraint, since the chat list has to be loaded first.

And the closing line is the one people forget. You must call `stop` at the end of the script, because TDLib holds a database connection and a local state that is not flushed otherwise.

The `Spoiler` class from telegram.text is a small illustration of the type handling, since a formatted message is passed as an object rather than as a string.

## call_method is the escape hatch for everything TDLib can do

The convenience methods cover the common operations, and the raw door is call_method:

```python
tg.call_method("getUser", params={"user_id": user_id})
```

That calls any tdlib method directly, with the method name and a params dictionary. The consequence is that the library is not a closed API surface, and anything TDLib exposes is reachable from Python even if this library has no typed wrapper for it.

The forwarding rules are documented for the parameters that matter. `send_message` passes `topic_id`, `reply_to`, `options` and `reply_markup` to tdlib as they are, so a message goes into a forum topic with a call like `tg.send_message(chat_id, "Hello", topic_id={"@type": "messageTopicForum", "forum_topic_id": 2})`. And a tdlib that does not know one of these fields ignores it and sends the message without it, with no error.

That last sentence is the part to keep in mind. Forwarding unknown parameters to a native library and letting it ignore them is convenient, but it means a typo in a parameter name, or a field the local TDLib version does not support, produces a message without the feature rather than an exception. If a forum topic or a reply silently does not appear, the version of TDLib in use is the first thing to check.

The other integration is the text formatting layer, with telegram.text providing classes like Spoiler, so a formatted message is a typed object rather than a string with markup in it.

## Conclusion

Adopt python-telegram if you need a full Telegram client in Python rather than a bot, since TDLib gives you the whole user API over MTProto and the wheels remove the usual build step. Skip it if a bot is all you need, because the documentation points you at python-telegram-bot for that, and skip it on Windows, which is not supported. Verify first that your platform is one of the four with a bundled binary, since musl, 32-bit ARM and older glibc fall back to a source distribution that carries no binary, and that you have registered an application to get an api_id and api_hash.

## FAQ

### What is the difference between python-telegram and python-telegram-bot?

python-telegram is a Python API for TDLib, so it signs in as a full Telegram account with a phone number over MTProto, the same protocol the official apps use, and can do what a regular client can do. It is not a wrapper around the HTTP Bot API, and the documentation says that if you only need a bot, python-telegram-bot is a better fit, though you can still pass a bot_token instead of a phone here.

### Does installing python-telegram require compiling TDLib?

Not on the common platforms. Four wheels bundle a tdlib binary, for Linux x86_64 and aarch64 needing glibc 2.28, and macOS arm64 needing macOS 11 and macOS x86_64 needing macOS 10.15, and those binaries link OpenSSL and zlib statically. Anywhere else, or on musl, 32-bit ARM or an older glibc, pip installs a source distribution with no binary and you compile tdlib yourself.

### How does python-telegram find the TDLib library at runtime?

In order: the `library_path` argument, the `PYTHON_TELEGRAM_TDLIB_PATH` environment variable, a system-wide `tdjson`, and then the bundled binary, and the library logs which one it used. If nothing is found, `TDJson()` raises `TDLibNotFoundError`, an OSError subclass whose message names the platform, the paths tried and the overrides.

## Sources

- [alexander-akhmetov/python-telegram on GitHub](https://github.com/alexander-akhmetov/python-telegram)
- [Issues](https://github.com/alexander-akhmetov/python-telegram/issues)
- [License: MIT](https://github.com/alexander-akhmetov/python-telegram/blob/main/LICENSE)
- [README](https://github.com/alexander-akhmetov/python-telegram/blob/main/README.md)
- [Releases](https://github.com/alexander-akhmetov/python-telegram/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/alexander-akhmetov-python-telegram
