[{"data":1,"prerenderedAt":719},["ShallowReactive",2],{"blog-en-telegram-bot-python-2024":3},{"id":4,"title":5,"body":6,"date":705,"description":706,"extension":707,"meta":708,"navigation":162,"path":709,"readTime":710,"seo":711,"slug":712,"stem":713,"tags":714,"__hash__":718},"blogEn\u002Fen\u002Fblog\u002Ftelegram-bot-python-2024.md","How to Build a Telegram Bot with Python in 2024 — Complete Guide",{"type":7,"value":8,"toc":692},"minimark",[9,14,23,28,61,65,68,82,86,124,127,135,139,216,220,317,321,389,393,485,489,561,565,609,613,668,672,679,688],[10,11,13],"h1",{"id":12},"how-to-build-a-telegram-bot-with-python-in-2024","How to Build a Telegram Bot with Python in 2024",[15,16,17,18,22],"p",{},"Telegram bots are one of the fastest ways to automate business processes or build a full product. In this article I'll show you how to build a ",[19,20,21],"strong",{},"production-ready bot"," with a database, proper architecture, and deployment from scratch.",[24,25,27],"h2",{"id":26},"what-well-build","What We'll Build",[29,30,31,39,45,52,58],"ul",{},[32,33,34,35,38],"li",{},"Bot on ",[19,36,37],{},"Aiogram 3"," (async, modern API)",[32,40,41,44],{},[19,42,43],{},"PostgreSQL"," for data storage",[32,46,47,51],{},[48,49,50],"code",{},".env","-based configuration",[32,53,54,57],{},[19,55,56],{},"Docker"," for deployment",[32,59,60],{},"FSM (finite state machine) for multi-step scenarios",[24,62,64],{"id":63},"why-aiogram-3","Why Aiogram 3",[15,66,67],{},"Aiogram 3 is the de facto standard for serious Python bots:",[29,69,70,73,76,79],{},[32,71,72],{},"Fully async (asyncio)",[32,74,75],{},"Routers and middlewares like FastAPI",[32,77,78],{},"Convenient FSM for dialogs",[32,80,81],{},"Active maintenance and good documentation",[24,83,85],{"id":84},"setup-and-project-structure","Setup and Project Structure",[87,88,93],"pre",{"className":89,"code":90,"language":91,"meta":92,"style":92},"language-bash shiki shiki-themes github-light github-dark","pip install aiogram==3.* sqlalchemy asyncpg python-dotenv\n","bash","",[48,94,95],{"__ignoreMap":92},[96,97,100,104,108,111,115,118,121],"span",{"class":98,"line":99},"line",1,[96,101,103],{"class":102},"sScJk","pip",[96,105,107],{"class":106},"sZZnC"," install",[96,109,110],{"class":106}," aiogram==3.",[96,112,114],{"class":113},"sj4cs","*",[96,116,117],{"class":106}," sqlalchemy",[96,119,120],{"class":106}," asyncpg",[96,122,123],{"class":106}," python-dotenv\n",[15,125,126],{},"Project structure:",[87,128,133],{"className":129,"code":131,"language":132},[130],"language-text","bot\u002F\n├── handlers\u002F\n│   ├── __init__.py\n│   ├── common.py      # \u002Fstart, \u002Fhelp\n│   └── user.py        # user scenarios\n├── middlewares\u002F\n│   └── database.py    # inject session into handlers\n├── models\u002F\n│   └── user.py        # SQLAlchemy models\n├── keyboards\u002F\n│   └── reply.py\n├── config.py\n└── main.py\n","text",[48,134,131],{"__ignoreMap":92},[24,136,138],{"id":137},"configuration","Configuration",[87,140,144],{"className":141,"code":142,"language":143,"meta":92,"style":92},"language-python shiki shiki-themes github-light github-dark","# config.py\nfrom pydantic_settings import BaseSettings\n\nclass Settings(BaseSettings):\n    BOT_TOKEN: str\n    DATABASE_URL: str\n    ADMIN_ID: int\n\n    class Config:\n        env_file = \".env\"\n\nsettings = Settings()\n","python",[48,145,146,151,157,164,170,176,182,188,193,199,205,210],{"__ignoreMap":92},[96,147,148],{"class":98,"line":99},[96,149,150],{},"# config.py\n",[96,152,154],{"class":98,"line":153},2,[96,155,156],{},"from pydantic_settings import BaseSettings\n",[96,158,160],{"class":98,"line":159},3,[96,161,163],{"emptyLinePlaceholder":162},true,"\n",[96,165,167],{"class":98,"line":166},4,[96,168,169],{},"class Settings(BaseSettings):\n",[96,171,173],{"class":98,"line":172},5,[96,174,175],{},"    BOT_TOKEN: str\n",[96,177,179],{"class":98,"line":178},6,[96,180,181],{},"    DATABASE_URL: str\n",[96,183,185],{"class":98,"line":184},7,[96,186,187],{},"    ADMIN_ID: int\n",[96,189,191],{"class":98,"line":190},8,[96,192,163],{"emptyLinePlaceholder":162},[96,194,196],{"class":98,"line":195},9,[96,197,198],{},"    class Config:\n",[96,200,202],{"class":98,"line":201},10,[96,203,204],{},"        env_file = \".env\"\n",[96,206,208],{"class":98,"line":207},11,[96,209,163],{"emptyLinePlaceholder":162},[96,211,213],{"class":98,"line":212},12,[96,214,215],{},"settings = Settings()\n",[24,217,219],{"id":218},"main-entry-point","Main Entry Point",[87,221,223],{"className":141,"code":222,"language":143,"meta":92,"style":92},"# main.py\nimport asyncio\nfrom aiogram import Bot, Dispatcher\nfrom aiogram.fsm.storage.memory import MemoryStorage\nfrom handlers import common, user\nfrom config import settings\n\nasync def main():\n    bot = Bot(token=settings.BOT_TOKEN)\n    dp = Dispatcher(storage=MemoryStorage())\n\n    dp.include_router(common.router)\n    dp.include_router(user.router)\n\n    await dp.start_polling(bot)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n",[48,224,225,230,235,240,245,250,255,259,264,269,274,278,283,289,294,300,305,311],{"__ignoreMap":92},[96,226,227],{"class":98,"line":99},[96,228,229],{},"# main.py\n",[96,231,232],{"class":98,"line":153},[96,233,234],{},"import asyncio\n",[96,236,237],{"class":98,"line":159},[96,238,239],{},"from aiogram import Bot, Dispatcher\n",[96,241,242],{"class":98,"line":166},[96,243,244],{},"from aiogram.fsm.storage.memory import MemoryStorage\n",[96,246,247],{"class":98,"line":172},[96,248,249],{},"from handlers import common, user\n",[96,251,252],{"class":98,"line":178},[96,253,254],{},"from config import settings\n",[96,256,257],{"class":98,"line":184},[96,258,163],{"emptyLinePlaceholder":162},[96,260,261],{"class":98,"line":190},[96,262,263],{},"async def main():\n",[96,265,266],{"class":98,"line":195},[96,267,268],{},"    bot = Bot(token=settings.BOT_TOKEN)\n",[96,270,271],{"class":98,"line":201},[96,272,273],{},"    dp = Dispatcher(storage=MemoryStorage())\n",[96,275,276],{"class":98,"line":207},[96,277,163],{"emptyLinePlaceholder":162},[96,279,280],{"class":98,"line":212},[96,281,282],{},"    dp.include_router(common.router)\n",[96,284,286],{"class":98,"line":285},13,[96,287,288],{},"    dp.include_router(user.router)\n",[96,290,292],{"class":98,"line":291},14,[96,293,163],{"emptyLinePlaceholder":162},[96,295,297],{"class":98,"line":296},15,[96,298,299],{},"    await dp.start_polling(bot)\n",[96,301,303],{"class":98,"line":302},16,[96,304,163],{"emptyLinePlaceholder":162},[96,306,308],{"class":98,"line":307},17,[96,309,310],{},"if __name__ == \"__main__\":\n",[96,312,314],{"class":98,"line":313},18,[96,315,316],{},"    asyncio.run(main())\n",[24,318,320],{"id":319},"the-start-handler","The \u002Fstart Handler",[87,322,324],{"className":141,"code":323,"language":143,"meta":92,"style":92},"# handlers\u002Fcommon.py\nfrom aiogram import Router\nfrom aiogram.filters import CommandStart\nfrom aiogram.types import Message\n\nrouter = Router()\n\n@router.message(CommandStart())\nasync def cmd_start(message: Message):\n    await message.answer(\n        f\"Hey {message.from_user.first_name}! 👋\\n\\n\"\n        \"I'm here to automate your tasks.\"\n    )\n",[48,325,326,331,336,341,346,350,355,359,364,369,374,379,384],{"__ignoreMap":92},[96,327,328],{"class":98,"line":99},[96,329,330],{},"# handlers\u002Fcommon.py\n",[96,332,333],{"class":98,"line":153},[96,334,335],{},"from aiogram import Router\n",[96,337,338],{"class":98,"line":159},[96,339,340],{},"from aiogram.filters import CommandStart\n",[96,342,343],{"class":98,"line":166},[96,344,345],{},"from aiogram.types import Message\n",[96,347,348],{"class":98,"line":172},[96,349,163],{"emptyLinePlaceholder":162},[96,351,352],{"class":98,"line":178},[96,353,354],{},"router = Router()\n",[96,356,357],{"class":98,"line":184},[96,358,163],{"emptyLinePlaceholder":162},[96,360,361],{"class":98,"line":190},[96,362,363],{},"@router.message(CommandStart())\n",[96,365,366],{"class":98,"line":195},[96,367,368],{},"async def cmd_start(message: Message):\n",[96,370,371],{"class":98,"line":201},[96,372,373],{},"    await message.answer(\n",[96,375,376],{"class":98,"line":207},[96,377,378],{},"        f\"Hey {message.from_user.first_name}! 👋\\n\\n\"\n",[96,380,381],{"class":98,"line":212},[96,382,383],{},"        \"I'm here to automate your tasks.\"\n",[96,385,386],{"class":98,"line":285},[96,387,388],{},"    )\n",[24,390,392],{"id":391},"fsm-multi-step-dialogs","FSM — Multi-step Dialogs",[87,394,396],{"className":141,"code":395,"language":143,"meta":92,"style":92},"from aiogram.fsm.context import FSMContext\nfrom aiogram.fsm.state import State, StatesGroup\n\nclass OrderForm(StatesGroup):\n    waiting_name    = State()\n    waiting_phone   = State()\n    waiting_comment = State()\n\n@router.message(Command(\"order\"))\nasync def start_order(message: Message, state: FSMContext):\n    await state.set_state(OrderForm.waiting_name)\n    await message.answer(\"What's your name?\")\n\n@router.message(OrderForm.waiting_name)\nasync def got_name(message: Message, state: FSMContext):\n    await state.update_data(name=message.text)\n    await state.set_state(OrderForm.waiting_phone)\n    await message.answer(\"Your phone number:\")\n",[48,397,398,403,408,412,417,422,427,432,436,441,446,451,456,460,465,470,475,480],{"__ignoreMap":92},[96,399,400],{"class":98,"line":99},[96,401,402],{},"from aiogram.fsm.context import FSMContext\n",[96,404,405],{"class":98,"line":153},[96,406,407],{},"from aiogram.fsm.state import State, StatesGroup\n",[96,409,410],{"class":98,"line":159},[96,411,163],{"emptyLinePlaceholder":162},[96,413,414],{"class":98,"line":166},[96,415,416],{},"class OrderForm(StatesGroup):\n",[96,418,419],{"class":98,"line":172},[96,420,421],{},"    waiting_name    = State()\n",[96,423,424],{"class":98,"line":178},[96,425,426],{},"    waiting_phone   = State()\n",[96,428,429],{"class":98,"line":184},[96,430,431],{},"    waiting_comment = State()\n",[96,433,434],{"class":98,"line":190},[96,435,163],{"emptyLinePlaceholder":162},[96,437,438],{"class":98,"line":195},[96,439,440],{},"@router.message(Command(\"order\"))\n",[96,442,443],{"class":98,"line":201},[96,444,445],{},"async def start_order(message: Message, state: FSMContext):\n",[96,447,448],{"class":98,"line":207},[96,449,450],{},"    await state.set_state(OrderForm.waiting_name)\n",[96,452,453],{"class":98,"line":212},[96,454,455],{},"    await message.answer(\"What's your name?\")\n",[96,457,458],{"class":98,"line":285},[96,459,163],{"emptyLinePlaceholder":162},[96,461,462],{"class":98,"line":291},[96,463,464],{},"@router.message(OrderForm.waiting_name)\n",[96,466,467],{"class":98,"line":296},[96,468,469],{},"async def got_name(message: Message, state: FSMContext):\n",[96,471,472],{"class":98,"line":302},[96,473,474],{},"    await state.update_data(name=message.text)\n",[96,476,477],{"class":98,"line":307},[96,478,479],{},"    await state.set_state(OrderForm.waiting_phone)\n",[96,481,482],{"class":98,"line":313},[96,483,484],{},"    await message.answer(\"Your phone number:\")\n",[24,486,488],{"id":487},"database-integration","Database Integration",[87,490,492],{"className":141,"code":491,"language":143,"meta":92,"style":92},"# models\u002Fuser.py\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nfrom sqlalchemy import BigInteger, String, DateTime, func\n\nclass Base(DeclarativeBase):\n    pass\n\nclass User(Base):\n    __tablename__ = \"users\"\n\n    id:         Mapped[int]    = mapped_column(BigInteger, primary_key=True)\n    username:   Mapped[str | None] = mapped_column(String(64))\n    first_name: Mapped[str]    = mapped_column(String(128))\n    created_at: Mapped[DateTime] = mapped_column(server_default=func.now())\n",[48,493,494,499,504,509,513,518,523,527,532,537,541,546,551,556],{"__ignoreMap":92},[96,495,496],{"class":98,"line":99},[96,497,498],{},"# models\u002Fuser.py\n",[96,500,501],{"class":98,"line":153},[96,502,503],{},"from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\n",[96,505,506],{"class":98,"line":159},[96,507,508],{},"from sqlalchemy import BigInteger, String, DateTime, func\n",[96,510,511],{"class":98,"line":166},[96,512,163],{"emptyLinePlaceholder":162},[96,514,515],{"class":98,"line":172},[96,516,517],{},"class Base(DeclarativeBase):\n",[96,519,520],{"class":98,"line":178},[96,521,522],{},"    pass\n",[96,524,525],{"class":98,"line":184},[96,526,163],{"emptyLinePlaceholder":162},[96,528,529],{"class":98,"line":190},[96,530,531],{},"class User(Base):\n",[96,533,534],{"class":98,"line":195},[96,535,536],{},"    __tablename__ = \"users\"\n",[96,538,539],{"class":98,"line":201},[96,540,163],{"emptyLinePlaceholder":162},[96,542,543],{"class":98,"line":207},[96,544,545],{},"    id:         Mapped[int]    = mapped_column(BigInteger, primary_key=True)\n",[96,547,548],{"class":98,"line":212},[96,549,550],{},"    username:   Mapped[str | None] = mapped_column(String(64))\n",[96,552,553],{"class":98,"line":285},[96,554,555],{},"    first_name: Mapped[str]    = mapped_column(String(128))\n",[96,557,558],{"class":98,"line":291},[96,559,560],{},"    created_at: Mapped[DateTime] = mapped_column(server_default=func.now())\n",[24,562,564],{"id":563},"dockerfile","Dockerfile",[87,566,569],{"className":567,"code":568,"language":563,"meta":92,"style":92},"language-dockerfile shiki shiki-themes github-light github-dark","FROM python:3.12-slim\n\nWORKDIR \u002Fapp\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\nCMD [\"python\", \"main.py\"]\n",[48,570,571,576,580,585,590,595,599,604],{"__ignoreMap":92},[96,572,573],{"class":98,"line":99},[96,574,575],{},"FROM python:3.12-slim\n",[96,577,578],{"class":98,"line":153},[96,579,163],{"emptyLinePlaceholder":162},[96,581,582],{"class":98,"line":159},[96,583,584],{},"WORKDIR \u002Fapp\n",[96,586,587],{"class":98,"line":166},[96,588,589],{},"COPY requirements.txt .\n",[96,591,592],{"class":98,"line":172},[96,593,594],{},"RUN pip install --no-cache-dir -r requirements.txt\n",[96,596,597],{"class":98,"line":178},[96,598,163],{"emptyLinePlaceholder":162},[96,600,601],{"class":98,"line":184},[96,602,603],{},"COPY . .\n",[96,605,606],{"class":98,"line":190},[96,607,608],{},"CMD [\"python\", \"main.py\"]\n",[24,610,612],{"id":611},"deploying-to-vps","Deploying to VPS",[87,614,616],{"className":89,"code":615,"language":91,"meta":92,"style":92},"git clone your-repo && cd bot\ncp .env.example .env  # fill in tokens\ndocker compose up -d\n",[48,617,618,639,654],{"__ignoreMap":92},[96,619,620,623,626,629,633,636],{"class":98,"line":99},[96,621,622],{"class":102},"git",[96,624,625],{"class":106}," clone",[96,627,628],{"class":106}," your-repo",[96,630,632],{"class":631},"sVt8B"," && ",[96,634,635],{"class":113},"cd",[96,637,638],{"class":106}," bot\n",[96,640,641,644,647,650],{"class":98,"line":153},[96,642,643],{"class":102},"cp",[96,645,646],{"class":106}," .env.example",[96,648,649],{"class":106}," .env",[96,651,653],{"class":652},"sJ8bj","  # fill in tokens\n",[96,655,656,659,662,665],{"class":98,"line":159},[96,657,658],{"class":102},"docker",[96,660,661],{"class":106}," compose",[96,663,664],{"class":106}," up",[96,666,667],{"class":113}," -d\n",[24,669,671],{"id":670},"summary","Summary",[15,673,674,675,678],{},"A properly built Telegram bot is not just a script with ",[48,676,677],{},"if '\u002Fstart'",". It's a full application with routers, middlewares, a database and CI\u002FCD. Aiogram 3 gives you all the tools for that.",[15,680,681,682,687],{},"Need help building a bot? ",[683,684,686],"a",{"href":685},"\u002Fen#contact","Get in touch",".",[689,690,691],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":92,"searchDepth":153,"depth":153,"links":693},[694,695,696,697,698,699,700,701,702,703,704],{"id":26,"depth":153,"text":27},{"id":63,"depth":153,"text":64},{"id":84,"depth":153,"text":85},{"id":137,"depth":153,"text":138},{"id":218,"depth":153,"text":219},{"id":319,"depth":153,"text":320},{"id":391,"depth":153,"text":392},{"id":487,"depth":153,"text":488},{"id":563,"depth":153,"text":564},{"id":611,"depth":153,"text":612},{"id":670,"depth":153,"text":671},"2024-11-15","Step-by-step guide to building a production-ready Telegram bot using Aiogram 3, PostgreSQL and Docker deployment. From zero to a working product.","md",{},"\u002Fen\u002Fblog\u002Ftelegram-bot-python-2024","12 min",{"title":5,"description":706},"telegram-bot-python-2024","en\u002Fblog\u002Ftelegram-bot-python-2024",[715,716,717,43,56],"Python","Telegram","Aiogram","O40LKFlUrPTY9Gmb-f6n4KzmMy0HpcXjTp6pMbH80as",1781783026351]