Dispatch
- Chapter
- 6 / 12
- Time
- 4 min
- Code
- 12
If nodes help prepare data, Dispatch answers a different question: "where does this event go, and who gets to handle it?"
When the bot is small, you barely think about it. You have @bot.on.message(...), you have a few handlers, everything feels simple. But once the project grows beyond one file, dispatch becomes a big deal.
The shortest mental model#
This model is enough to stay oriented:
Dispatchcollects routing- inside it live
Router - inside routers live
View - inside views live handlers
So the path is roughly:
Dispatch -> Router -> View -> Handler
You do not need more than that to get started.
What bot.on really is#
When you write:
@bot.on.message(...)
async def handler(...): ...bot.on is the dispatch.
And bot.on.message is the message view of the main router inside that dispatch.
In other words:
bot.onis your bot's main dispatchbot.on.messageis where message handlers gobot.on.callback_queryis where callback query handlers go
That understanding is more than enough for early projects.
Why Dispatch exists at all#
Because keeping the whole bot in one file becomes painful very quickly.
Almost every bot eventually grows into something like:
- separate handlers for start
- separate handlers for admin tools
- separate handlers for payments
- separate handlers for inline buttons
If all of that stays in one bot.py, the file becomes hard to navigate. Dispatch solves that by letting you split the bot into logical pieces and then assemble them together.
A minimal dispatch#
from telegrinder import Dispatch, Message
from telegrinder.rules import Argument, Command, IsBot
dp = Dispatch(name="chat-utilities")
@dp.message(IsBot())
async def bot_message_handler(message: Message) -> None:
# A normal handler, just registered in a local dispatch instead of bot.on.
await message.answer("Hey bot!")
@dp.message(
Command(
"repeat",
Argument("text"),
Argument("times", validators=[lambda s: int(s) if s.isdigit() else None], optional=True),
)
)
async def command_handler(message: Message, text: str, times: int = 5) -> None:
# times will contain the parsed number or the default value.
await message.answer(", ".join([text] * times))That dp can already live in its own file and be plugged into the main bot later.
Loading a dispatch into the bot#
Suppose the code above lives in handlers/chat_utilities.py.
Then your main bot can look like this:
from handlers import chat_utilities
from telegrinder import API, Telegrinder, Token
api = API(Token("your-token-here"))
bot = Telegrinder(api)
# Load routers and error handlers from the external dispatch.
bot.on.load(chat_utilities.dp)
bot.run_forever()This is usually the moment when things start feeling much more maintainable: the bot can be assembled from pieces instead of being written as one giant script.
Where routers fit in#
In the current telegrinder API, dispatch works through routers.
A dispatch has:
main_routerrouters, the queue of loaded routers- views such as
message,callback_query,inline_query,media_group,event_error,raw
When you write:
@bot.on.message(Text("/start"))
async def start(...): ...you are effectively registering on:
bot.on.main_router.messageSo the main router is already there. In day-to-day bot code you just do not always need to touch it explicitly.
What a View is#
A view is an object for one specific kind of event.
The most common ones are:
messagecallback_queryinline_querymedia_groupevent_errorraw
Each view contains:
- a view-level filter
- a list of handlers
- middleware
- a waiter machine
A beginner-friendly way to think about it is: a view is just a shelf where you put handlers of one event type.
When using Router directly is helpful#
Sometimes you want to separate a clear logical area. For example, an admin zone.
Then creating a router explicitly makes sense:
from telegrinder import Dispatch, Message, Router
from telegrinder.rules import Text
admin_router = Router(name="admin")
@admin_router.message(Text("/ban"))
async def ban_handler(message: Message) -> None:
# All admin-related logic can stay inside this router.
await message.answer("Admin action")
admin = Dispatch(router=admin_router, name="admin-dispatch")Then load it like any other dispatch:
bot.on.load(admin)This becomes very nice when the project has clear domains:
- admin
- payments
- onboarding
- games
- moderation
load_many and load_from_dir#
As the number of pieces grows, bot assembly can become even cleaner.
Load several dispatches at once#
bot.on.load_many(users.dp, payments.dp, admin.dp)This is useful when you already imported the modules yourself and just want to combine them in one place.
Auto-load from a directory#
bot.on.load_from_dir("handlers", recursive=True)Here telegrinder:
- walks over Python files
- imports them
- looks for global
Dispatchinstances - loads them into the main dispatch
This is convenient for blueprint-style project layouts.
Minimal idea:
# handlers/start.py
from telegrinder import Dispatch
dp = Dispatch(name="start")# bot.py
bot.on.load_from_dir("handlers", recursive=True)What happens when an event is processed#
Without going too deep into internals, the flow is roughly:
Dispatch.feed()receivesAPIandUpdate- a
Contextis created - dispatch-level middleware runs
- dispatch iterates through its routers
- each router checks matching views
- the matching view runs handlers
- if something fails, it can be processed by
event_error
You do not need to memorize every detail. It is enough to understand that dispatch is not just "a list of functions", but a fairly structured pipeline.
A practical project layout#
Here is a very good minimal starting point:
mybot/
bot.py
handlers/
start.py
admin.py
payments.py
keyboards/
menu.py
nodes/
db.py
rules/
is_admin.pyThis kind of layout scales much more calmly than one huge file with hundreds of lines.
What to remember#
Dispatchis about routing and code separation- routers live inside dispatch, and views live inside routers
bot.on.message(...)is basically registration onmain_router.messageload,load_many, andload_from_dirhelp assemble multi-file bots- if your bot starts growing, split it earlier rather than later