Skip to main content

Declarative, developer-friendly library for building Telegram bots

Project description

TeleKit

PyPI Python PyPI Downloads

Telekit

Telekit is a declarative, developer-friendly library for building Telegram bots. It gives developers a dedicated Sender for composing and sending messages and a Chain for handling dialogue between the user and the bot. The library also handles inline keyboards and callback routing automatically, letting you focus on the bot's behavior instead of repetitive tasks.

import telekit

class MyStartHandler(telekit.Handler):
    @classmethod
    def init_handler(cls):
        cls.on.command('start').invoke(cls.handle_start)

    def handle_start(self):
        self.chain.sender.set_text("Hello!")
        self.chain.sender.set_photo("robot.png")
        self.chain.send()

telekit.Server("BOT_TOKEN").polling()

Send "Hello!" with a photo on /start

Telekit comes with a built-in DSL, allowing developers to create fully interactive bots with minimal code. It also integrates Jinja, giving you loops, conditionals, expressions, and filters to generate dynamic content.

@ main {
    title   = "🎉 Fun Facts Quiz";
    message = "Test your knowledge with 10 fun questions!";

    buttons {
        question_1("Start Quiz");
    }
}

See the full example

Even in its beta stage, Telekit accelerates bot development, offering typed command parameters, text styling via Bold(), Italic(), built-in emoji game results for 🎲 🎯 🏀 ⚽ 🎳 🎰, and much more out of the box. Its declarative design makes bots easier to read, maintain, and extend.

Key features:

  • Declarative bot logic with chains for effortless handling of complex conversations
  • Ready-to-use DSL for FAQs and other interactive scripts
  • Automatic handling of message formatting via Sender and callback routing
  • Deep Linking support with type-checked Command Parameters for flexible user input
  • Built-in Permission and Logging system for user management
  • Seamless integration with pyTelegramBotAPI
  • Fast to develop and easy-to-extend code

GitHub PyPI Telegram Community

Contents

Overview

Telekit is a library for building Telegram bots where dialogs look like normal method calls. No bulky state machines. No scattered handlers.

The idea is simple: you point to the next step — Telekit calls it when the user replies.

Entries

No state machines. Just tell Telekit which method should handle the next user message.

def handle(self):
    self.chain.sender.set_text("👋 Hello! What is your name?")
    self.chain.set_entry_text(self.handle_name)
    self.chain.send()

def handle_name(self, name: str):
    self.chain.sender.set_text(f"Nice to meet you, {name}!")
    self.chain.send()

The handle method sends a message and registers handle_name as the next step using set_entry_text. When the user replies, Telekit automatically calls handle_name and passes the user's message as a plain str argument.

That's it. No enums. No manual state tracking. No boilerplate.

Inline Keyboards

Buttons can either return a value or call a method directly.

Choice keyboard — map button labels to values. The selected value is passed straight into your handler:

self.chain.set_inline_choice(
    self.on_choice,
    choices={
        "Option 1": "Value 1",
        "Option 2": "Value 2",
        "Option 3": [3, "Yes, it's an array"],
    }
)

def on_choice(self, choice: str | list):
    print(f"{choice!r}") # "Value 1", "Value 2" or [3, "Yes, it's an array"]

Inside on_choice, you receive exactly what you defined in choices: a string, list, number, function — anything.

Callback keyboard — each button calls its own method:

self.chain.set_inline_keyboard({
    "« Back": self.display_previous_page,
    "Next »": self.display_next_page,
})

Useful for pagination, navigation, or menus.

Command Parameters

Telekit can parse and validate command parameters for you.

from telekit.parameters import *

class GreetHandler(telekit.Handler):
    @classmethod
    def init_handler(cls) -> None:
        cls.on.command("greet", params=[Int(), Str()]).invoke(cls.handle)

    def handle(self, age: int | None = None, name: str | None = None):
        if age is None or name is None:
            self.chain.sender.set_text("Usage: /greet <age> <name>")
        else:
            self.chain.sender.set_text(f"Hello, {name}! You are {age} years old. Next year you'll turn {age + 1} 😅")
        self.chain.send()

Now /greet 64 "Alice Reingold" or /greet 128 Dracula are parsed automatically.

[!INFO] If arguments are invalid or missing, you simply receive None and decide how to respond.

Dialogue

Dialogs are built as a chain of steps. Each method waits for the user before continuing.

class DialogueHandler(telekit.Handler):

    @classmethod
    def init_handler(cls) -> None:
        cls.on.text("hello", "hi", "hey").invoke(cls.handle_hello)

    def handle_hello(self) -> None:
        self.chain.sender.set_text("👋 Hello! What is your name?")
        if self.user.first_name:
            self.chain.set_entry_suggestions([self.user.first_name])
        self.chain.set_entry_text(self.handle_name)
        self.chain.send()

    def handle_name(self, name: str) -> None:
        self.user_name = name
        self.chain.sender.set_text("Nice! How are you feeling today?")
        self.chain.set_entry_text(self.handle_feeling)
        self.chain.send()

    def handle_feeling(self, feeling: str) -> None:
        self.chain.sender.set_text(f"Got it, {self.user_name.title()}! You feel: {feeling}")
        self.chain.set_inline_keyboard({"↺ Restart": self.handle_hello})
        self.chain.send()

How it works:

  • The handler reacts to "hello", "hi", or "hey" (lowercase, UPPERCASE, or mixed).
  • handle_hello asks for the user's name.
  • set_entry_suggestions attaches the user's Telegram first_name as a suggestion button.
  • handle_name stores the name in self.user_name.
  • handle_feeling completes the flow and adds a "↺ Restart" button that routes back to the beginning.

It looks like regular Python. And reads like it too.

Sender

Want to add an image, document or an effect in a single line?

self.chain.sender.set_effect(Effect.HEART) # Add effect to message. Use enum or string
self.chain.sender.set_photo("robot.png") # Attach photo. URL, file_id, or path
self.chain.sender.set_document("README.md") # Attach document. URL, file_id, or path
self.chain.sender.set_text_as_document("Hello, this is a text document!") # Convert string to text document
self.chain.sender.send_chat_action(ChatAction.TYPING) # Send chat action. Use enum or string

[!NOTE] Telekit automatically decides whether to use 'bot.send_message' or 'bot.send_photo' based on the content

Styles

Telekit lets you describe formatting as objects instead of writing raw HTML or Markdown.

from telekit.styles import *

def handle(self) -> None:
    self.chain.sender.set_text(
        Bold("Text style examples:\n"),
        Stack(
            Bold("Bold text"),
            Italic("Italic text"),
            Bold(Italic("Bold + italic")),
            Link("Link", url="https://example.com"),
            BotLink("Deep link", username="MyBot", start="promo_42"),
            start="- {{index}}. ",
            sep=".\n",
        )
    )
    self.chain.send()

You describe structure. Telekit generates HTML or MarkdownV2 automatically:

<b>Text style examples:</b>

- 1. <b>Bold text</b>.
- 2. <i>Italic text</i>.
- 3. <b><i>Bold + italic</i></b>.
- 4. <a href="https://example.com">Link</a>.
- 5. <a href="https://t.me/MyBot?start=promo_42">Deep link</a>

No manual escaping. No broken formatting because of one missing character.

Telekit DSL

If you prefer not to write dialog logic in Python, you can use the built-in DSL with Jinja support.

import telekit

class QuizHandler(telekit.TelekitDSL.Mixin):
    @classmethod
    def init_handler(cls) -> None:
        cls.analyze_string(script)
        cls.on.command("start").invoke(cls.start_script)

script = """
$ timeout {
    time = 20; // 20 sec.
}

@ main {
    title   = "🎉 Fun Facts Quiz";
    message = "Test your knowledge with 10 fun questions!";

    buttons {
        next("Start Quiz");
    }
}

@ question_1 {
    title   = "🐶 Question 1";
    message = "Which animal is the fastest on land?";
    buttons {
        _lose("Elephant");
        next("Cheetah");       // correct answer
        _lose("Horse");
        _lose("Lion");
    }
}

/* ... */
"""

telekit.Server(BOT_TOKEN).polling()

Key features of the Telekit DSL:

  • Scene-based architecture
  • Anonymous scenes
  • Automatic navigation stack management
  • Input handling
  • Images support and link buttons
  • Template variables
  • Custom variables
  • Hooks (Python API integration)
  • Jinja template engine
Click to see what you can do with the DSL
Telekit Example 7 Telekit Example 8

You can find a full quiz example and DSL reference in the repository.

Example Bot

You can launch an example bot by running the following code:

import telekit

telekit.example(YOUR_BOT_TOKEN)

It includes example commands, dialogs, keyboards, and style usage.

Why Telekit

  • No FSM — just chains.
  • Declarative, behavior-focused bot logic with minimal boilerplate.
  • Automatic callback routing and input handling.
  • Styles API for rich text (Bold, Italic, Links) with automatic escaping.
  • Deep linking and typed command parameters.
  • Built-in DSL for menus, FAQs, and simple bots.
  • Zero-code Obsidian Canvas mode.
  • Seamless integration with pyTelegramBotAPI.

Telekit doesn't try to be everything.
It tries to make Telegram bot development easier.

[!TIP] If you're interested and want to learn more, check out the Tutorial


Changes in version 2.3.0b2

Inline Buttons

Name Description
StaticButton A non-interactive inline button that performs no action when pressed.
AnswerButton A button that responds to a callback query with a notification or alert without executing custom logic.
  • AnswerButton and its subclasses (AlertButton, NotificationButton) now support the persistent parameter (Defauts to True):
    • True — non-blocking hint (does not affect further interactions)
    • False — terminates interaction after click

Traits

Name Description
TrackHandoffOrigin Tracks which handler transferred control to this one via handoff().
PaginatedChoice Adds a paginated inline keyboard for choosing from a list of items.

TrackHandoffOrigin

TrackHandoffOrigin adds three members to any handler that inherits it:

Member Description
handoff_origin The handler instance that handed off to this one, or None if invoked directly.
is_handed_off True if this handler was reached via handoff(), False otherwise.
handoff_back() Transfer control back to the origin handler via self.handoff_origin.handle()
handoff_back_or(handler) Like handoff_back(), but falls back to handler on fail.

Set TRACK_HANDOFF_ORIGIN = False on any subclass to opt out of tracking.

class MyHandler(TrackHandoffOrigin, telekit.Handler):

    def handle(self):
        ...
        self.chain.set_inline_keyboard({
            "« Back": self.handoff_back_or(StartHandler)
        })
        self.chain.edit()

PaginatedChoice

Renders a paginated inline keyboard from any dict or iterable. Navigation buttons (« Back, Next ») are added automatically.

Member Description
paginated_choice(choices, on_choice, ...) Display a paginated choice keyboard.
PAGINATED_CHOICE_BACK_LABEL Label for the back navigation button. Defaults to « Back.
PAGINATED_CHOICE_NEXT_LABEL Label for the next navigation button. Defaults to Next ».
PAGINATED_CHOICE_PAGE_LABEL Label template for the page indicator button. Supports {page} and {pages} placeholders. Set to None to hide. Defaults to {page} / {pages}.
self.chain.sender.set_title("🔤 What is your initial?")
self.chain.sender.set_message("Pick the first letter of your name")
self.chain.sender.set_remove_text(False)

self.paginated_choice(
    choices="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    on_choice=self.handle_letter,
    row_width=5
)

Override labels per handler to localise or restyle navigation:

class MyHandler(PaginatedChoice, telekit.Handler):
    PAGINATED_CHOICE_BACK_LABEL = "⬅️ Назад"
    PAGINATED_CHOICE_NEXT_LABEL = "Далі ➡️"
    PAGINATED_CHOICE_PAGE_LABEL = None  # hide page indicator
(Click to see the result)
Example

choices accepts a dict[str, T], or any Iterable[T]. If only one item is present, on_choice is called immediately without rendering a keyboard.

Utils

Name Description
load_env Load all key-value pairs from a .env file into a dictionary.
read_env_var Read a single variable by name from a .env file.
compose_keyboard Merge multiple button groups into a single inline keyboard with computed row_width.

Environment Utils

  • read_token and read_canvas_path now support reading from .env files. Pass ".env" to use the default key, or ".env:KEY" to specify a custom one:
read_token(".env")                 # reads TOKEN
read_token(".env:BOT_TOKEN")       # reads BOT_TOKEN

read_canvas_path(".env")           # reads CANVAS_PATH
read_canvas_path(".env:MY_CANVAS") # reads MY_CANVAS

Inline Keyboard Utils

compose_keyboard combines multiple button groups into a single keyboard and automatically calculates row_width for each group.

Each group is laid out independently using its corresponding width from widths. A width of -1 means "all buttons in one row" (i.e. len(group)).

Param Type Description
groups dict[str, Any] One or more button groups
widths Iterable[int] Row width per group or single value for all
Returns Type
keyboard dict[str, Any]
row_width tuple[int, ...]
# row_width → (1, 3, 3, 3, 2)
# layout:
#   |    🆕 Create    |
#   |  1  |  2  |  3  |
#   |  4  |  5  |  6  |
#   |  7  |  8  |  9  |
#   | « Back | Next » |

keyboard, row_width = compose_keyboard(
    {"🆕 Create": "create"},
    {str(n): str(n) for n in range(1, 10)},
    {"« Back": "back", "Next »": "next"},
    widths=(-1, 3, -1), # or (1, 3, 2)
)
self.chain.set_inline_choice(keyboard, row_width)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

telekit-2.3.0b2.tar.gz (121.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

telekit-2.3.0b2-py3-none-any.whl (143.3 kB view details)

Uploaded Python 3

File details

Details for the file telekit-2.3.0b2.tar.gz.

File metadata

  • Download URL: telekit-2.3.0b2.tar.gz
  • Upload date:
  • Size: 121.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.11

File hashes

Hashes for telekit-2.3.0b2.tar.gz
Algorithm Hash digest
SHA256 0cd8bd4a2ce6869024af28d0a749fe0032006fe7a409b6373772ffd9892cd02d
MD5 f8707bcb3440770c170f0b9258bf458a
BLAKE2b-256 48dca34bf742c53fc40b139047fb407c6f3ba0d009377b3bbf2adce37229e3fc

See more details on using hashes here.

File details

Details for the file telekit-2.3.0b2-py3-none-any.whl.

File metadata

  • Download URL: telekit-2.3.0b2-py3-none-any.whl
  • Upload date:
  • Size: 143.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.11

File hashes

Hashes for telekit-2.3.0b2-py3-none-any.whl
Algorithm Hash digest
SHA256 d44036628a9a654181533d0cfc93179aded5dfbbb3c93cd9f10ce6275b6fe7cb
MD5 2962b36a710b6b5dc624466003340ebe
BLAKE2b-256 97cfc16027c0ff98beaa1652fa5d7285e8f6bab4e5cc505c7fa6084c00d65cb1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page