Веб-разработка и REST API на Python
Веб и API
Если коротко, то "веб-разработка на Python" — это про то, как принять HTTP-запрос, обработать данные, обратиться к базе или внешнему сервису и вернуть ответ пользователю или другой системе. На практике это путь от простого обработчика "/health" до полноценных API-платформ с авторизацией, логированием и мониторингом.
Для уверенного старта полезно держать в голове простую схему:
- Протокол и транспорт (HTTP, методы, статусы) — см. HTTP как основа веб-интеграций; smoke-test эндпоинтов из CLI — утилита curl, curl / fetch — примеры.
- Серверная логика (Flask/FastAPI, маршруты, валидация).
- Интеграции (внешние API, почта, мессенджеры).
- Эксплуатация (конфигурация, безопасность, деплой, наблюдаемость).
Именно такую последовательность мы используем в статье: от понятной базы к практическим production-паттернам.
Экосистема веба в Python
Python занимает устойчивую позицию среди ведущих языков для реализации серверной стороны веб-приложений и интеграционных решений. Это обусловлено не столько производительностью интерпретатора (где Python уступает компилируемым языкам), сколько зрелостью экосистемы, простотой чтения кода, богатством библиотек и широкой поддержкой сообщества. Веб-стек Python охватывает как микросервисную архитектуру, так и монолитные приложения, от внутренних инструментов до публичных API-платформ.
Важно сразу развести два смежных, но различных направления:
- Веб-разработка — создание приложений, взаимодействующих с пользователями посредством HTTP-протокола, чаще всего через браузер (HTML-интерфейс, формы, AJAX-запросы и т.п.).
- Работа с API — как на стороне провайдера (реализация собственного API), так и на стороне клиента (интеграция с внешними сервисами через их интерфейсы).
Оба направления используют один и тот же набор базовых технологий — HTTP, JSON, URI, методы запросов — но с разной семантикой и набором требований. Например, приложение с пользовательским интерфейсом требует управления состоянием (сессиями, куками), обработки файлов, валидации форм, работы с шаблонами. API же, по современным канонам, стремится к statelessness (отсутствию состояния), предсказуемости структуры ответов и строгому соблюдению HTTP-семантики.
В данной главе рассматриваются три ключевых аспекта:
- создание веб-приложений на базе микрофреймворка Flask — как иллюстрация принципов построения HTTP-серверов на Python;
- организация клиентской работы с внешними API через библиотеку requests;
- отправка уведомлений по различным каналам — электронная почта и Telegram — как неотъемлемая часть практически любого серверного приложения.
Flask
Flask — это микрофреймворк для создания веб-приложений на Python. Слово "микро" здесь не означает "неполноценный" — оно указывает на отсутствие жёстких требований к структуре проекта, отсутствие встроенного ORM, системы аутентификации или административного интерфейса "из коробки". Вместо этого Flask предоставляет минимальное, но расширяемое ядро, позволяющее разработчику гибко комбинировать компоненты под конкретную задачу.
Такой подход делает Flask идеальным инструментом для обучения, прототипирования, создания микросервисов и API, а также небольших внутренних приложений — особенно в тех случаях, когда не требуется "тяжёлая артиллерия" вроде Django.
Основы WSGI
В основе Flask лежит WSGI — Web Server Gateway Interface. Это спецификация, описывающая стандарт взаимодействия между веб-сервером (например, Apache, Nginx, Gunicorn) и Python-приложением.
WSGI определяет, что приложение — это вызываемый объект (callable), принимающий два аргумента:
environ— словарь, содержащий переменные окружения, заголовки запроса, метод, URI и другие метаданные;start_response— callable, который приложение вызывает для передачи HTTP-статуса и заголовков ответа.
Пример минимального WSGI-приложения (без использования Flask):
def application(environ, start_response):
status = '200 OK'
headers = [('Content-Type', 'text/plain; charset=utf-8')]
start_response(status, headers)
return [b'Hello from WSGI!']
Разбор фрагмента:
- Функция
application(...)реализует минимальный WSGI-совместимый обработчик. statusиheadersформируют HTTP-метаданные ответа.start_response(...)передаёт серверу статус и заголовки.- Возвращаемый список байтов (
[b'...']) содержит тело ответа.
Запустить такое приложение можно через встроенный сервер wsgiref.simple_server — но только для целей отладки. В продакшене используются серверы вроде Gunicorn или uWSGI, которые поддерживают многопроцессность, таймауты, логирование и другие production-требования.
Flask берёт на себя реализацию WSGI-интерфейса: объект Flask сам является callable, и его можно передать в WSGI-сервер напрямую. При этом разработчик не работает с environ напрямую — вместо этого Flask предоставляет высокоуровневые абстракции — request, session, g, current_app, которые уже содержат разобранные и удобные для использования данные.
Создание простого сервера
Начнём с канонического примера — "Hello, World!" на Flask.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Привет, Вселенная IT!'
if __name__ == '__main__':
app.run(debug=True)
Этот код делает следующее:
- Создаёт экземпляр приложения
Flask. Аргумент__name__используется Flask для определения корневого пути приложения — это необходимо для поиска шаблонов, статических файлов и относительных импортов. - Декоратор
@app.route('/')связывает URI-путь/с функциейindex. Такой механизм называется маршрутизацией (routing). - При вызове
app.run(debug=True)запускается встроенный сервер Werkzeug — надёжный и функциональный сервер для разработки, но не предназначенный для использования в production.
debug=True включает автоматическую перезагрузку при изменении кода и интерактивный отладчик при возникновении исключения. Это чрезвычайно удобно при разработке, но представляет серьёзную уязвимость в production (возможность выполнения произвольного кода через отладчик). Поэтому в продакшене debug всегда выключен, а приложение запускается через Gunicorn/uvicorn и проксируется через Nginx — готовые конфиги proxy; упаковка в образ — Dockerfile для Python.
Маршрутизация и обработка GET/POST
Маршрутизация в Flask основана на сопоставлении URI-путей с функциями-обработчиками (view functions). Помимо статических путей вроде /about, Flask поддерживает динамические сегменты:
@app.route('/user/<username>')
def show_user_profile(username):
return f'Профиль пользователя: {username}'
Здесь <username> — это плейсхолдер, значение которого передаётся в функцию как аргумент. Тип сегмента можно ограничить — <int:user_id>, <uuid:token>, <path:subpath> — что обеспечивает валидацию на уровне маршрутизатора.
Более того, один и тот же путь может обрабатывать разные HTTP-методы:
Код ITЗагрузка примера кода…
Объект request — глобальный, но потокобезопасный контекстный объект, содержащий данные текущего запроса — метод, заголовки, тело (form, json, files), куки, параметры строки запроса (args). Например:
request.args.get('page')— извлечение?page=2;request.json— разобранный JSON из тела POST-запроса (при Content-Type:application/json);request.files['avatar']— загруженный файл.
Flask не навязывает способ обработки данных — разработчик самостоятельно решает, как валидировать и преобразовывать входные данные. Для серьёзных проектов рекомендуется использование библиотек вроде Pydantic или marshmallow для строгой схемной валидации.
Шаблоны, статика, сессии
Хотя Flask часто используется для API, он изначально предполагает поддержку рендеринга HTML через механизм шаблонов. По умолчанию используется шаблонизатор Jinja2, который позволяет встраивать логику (условия, циклы) и выражения в HTML, при этом сохраняя чёткое разделение между кодом и представлением.
Пример шаблона templates/profile.html:
Код ITЗагрузка примера кода…
Разбор фрагмента:
{{ user.name }}выводит значение переменной из контекста шаблона.- Блок
{% if ... %}условно показывает часть HTML для администратора. - Цикл
{% for role in user.roles %}строит список ролей динамически. - Такой шаблон отделяет серверную логику от представления.
Рендеринг в приложении:
from flask import render_template
@app.route('/profile/<int:user_id>')
def profile(user_id):
user = get_user_by_id(user_id) # условная функция
return render_template('profile.html', user=user)
Разбор фрагмента:
- Обработчик получает
user_idиз URL и загружает данные пользователя. render_template(...)рендерит HTML-шаблон с переданным объектомuser.- Ключ
userстановится доступным в Jinja как переменнаяuser.
Ключевые особенности Jinja2:
- экранирование HTML по умолчанию (защита от XSS);
- наследование шаблонов (
{% extends 'base.html' %}) — позволяет вынести общую структуру (шапку, подвал) в базовый шаблон; - фильтры (
{{ name|upper }},{{ price|round(2) }}); - макросы — переиспользуемые блоки шаблонного кода.
Статические файлы — CSS, JS, изображения — размещаются в папке static/. Flask автоматически обслуживает их по пути /static/..., например:
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
Функция url_for() генерирует URL по имени маршрута или статического ресурса. Это предпочтительнее жёстко прописанных путей: при изменении структуры URL достаточно изменить только декоратор @route, а не все ссылки в шаблонах.
Сессии позволяют сохранять данные между запросами. Flask хранит сессию в подписанной (signed) cookie — то есть данные сериализуются, шифруются HMAC-ключом и отправляются клиенту. Сервер не хранит состояние — только секретный ключ для проверки подписи. Это обеспечивает безопасность от подделки (при условии, что SECRET_KEY неизвестен атакующему), но накладывает ограничения на объём данных (ограничение размера cookie — ~4 КБ).
Код ITЗагрузка примера кода…
Для хранения больших объёмов данных или при требовании server-side sessions (например, для logout всех сессий) используются расширения — Flask-Session, Flask-Login с поддержкой Redis, Memcached или баз данных.
REST API
Flask одинаково хорошо подходит как для full-stack приложений, так и для реализации RESTful API. Для этого достаточно возвращать из обработчиков структурированные данные в формате JSON.
Минимальный API-эндпоинт:
from flask import jsonify
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
user = {
'id': user_id,
'name': 'Алиса',
'email': 'alice@example.com'
}
return jsonify(user)
Разбор фрагмента:
- Эндпоинт принимает
user_idкак параметр пути. - Словарь
userформирует структуру данных для ответа API. jsonify(user)сериализует объект в JSON и выставляет корректный MIME-тип.
Функция jsonify() делает три вещи:
- сериализует объект Python (dict, list, str, int и т.п.) в JSON;
- устанавливает заголовок
Content-Type: application/json; - гарантирует, что ответ возвращается с кодом 200 (если явно не указано иное).
Для более сложных сценариев (валидация входных данных, сериализация моделей, обработка ошибок) используются расширения, например Flask-RESTful или Flask-apispec (интеграция с OpenAPI/Swagger). Однако даже без них можно построить соответствующий REST-канонам API, если соблюдать следующие принципы:
- идемпотентность — GET, PUT, DELETE должны давать предсказуемый результат при повторных вызовах;
- статус-коды — 200 — успешное выполнение, 201 — создано (POST), 204 — нет содержимого (DELETE), 400 — ошибка клиента, 401/403 — аутентификация/авторизация, 404 — не найдено, 500 — ошибка сервера;
- URI должны именовать ресурсы —
/users,/users/123,/users/123/posts, а не/getUser,/deleteUser; - тело запроса для создания/изменения — JSON в
application/json.
Пример полноценного CRUD-эндпоинта:
Код ITЗагрузка примера кода…
Обратите внимание: даже при ошибке (400, 404) мы возвращаем JSON — это ожидаемое поведение для API-клиентов. Пустой ответ с 204 при удалении также соответствует рекомендациям RFC 7231.
Работа с API
В современной инфраструктуре крайне редко встречаются полностью изолированные приложения. Даже внутренние сервисы взаимодействуют друг с другом через API. Python, благодаря своей выразительной синтаксической лёгкости и зрелой экосистеме, стал де-факто стандартом для написания интеграционных скриптов, ETL-процессов, автоматизации и микросервисов, обращающихся к сторонним системам.
Ключевой инструмент в этой области — библиотека requests. Её появление в 2011 году стало поворотным моментом: до этого стандартный urllib требовал многословного и непрозрачного кода для даже самых простых задач. requests ввела принцип "HTTP for Humans" — интуитивный, последовательный, отказоустойчивый интерфейс, ставший эталоном для библиотек во многих языках.
HTTP-запросы — requests
Основной объект в requests — функции высшего уровня (get, post, put, delete и др.), каждая из которых инкапсулирует выполнение одного HTTP-запроса и возвращает объект Response. Рассмотрим базовый сценарий:
import requests
response = requests.get('https://api.github.com/users/octocat')
print(response.status_code) # → 200
print(response.headers['Content-Type']) # → application/json; charset=utf-8
print(response.text[:200]) # первые 200 символов тела ответа
Разбор фрагмента:
requests.get(...)выполняет HTTP-запрос к GitHub API.status_codeпозволяет быстро определить успешность вызова.headers[...]показывает метаданные ответа, включая тип контента.text[:200]полезен для диагностики, когда нужно быстро посмотреть начало payload.
Обратите внимание на несколько важных моментов:
- Автоматическое управление соединением. В отличие от
urllib,requestsне требует ручного открытия/закрытия соединений. Под капотом используется пул соединений черезurllib3, что повышает производительность при множественных запросах к одному хосту. - Кодировка определяется автоматически — на основе заголовка
Content-Typeи анализа содержимого (response.encodingможно переопределить, если обнаружена ошибка). - Тело ответа доступно в нескольких форматах —
text(строка),content(байты),json()(разобранный JSON),iter_content()(для потоковой обработки больших ответов).
Однако такой "быстрый старт" скрывает риски. В production-коде недопустимо оставлять запросы без явного указания таймаутов.
# Плохо: запрос может повиснуть на неопределённое время
requests.get('https://slow-service.example.com')
# Хорошо: явно задаём лимиты
response = requests.get(
'https://api.example.com/data',
timeout=(3.05, 27) # (connect_timeout, read_timeout) в секундах
)
Таймауты — это обязательный элемент отказоустойчивости. Если внешний сервис не отвечает, ваше приложение не должно блокироваться. Рекомендуется использовать разные значения для подключения (обычно 1–5 сек) и чтения (зависит от ожидаемого объёма данных: 10–60 сек для больших выгрузок). Значение None (бесконечный таймаут) допустимо только в offline-скриптах, где отказ внешней системы не критичен.
Для повторяющихся запросов к одному API рекомендуется использовать объект Session:
session = requests.Session()
session.headers.update({'User-Agent': 'IT-Universe-Client/1.0'})
session.auth = ('user', 'pass') # или другой механизм аутентификации
# Все запросы через session наследуют заголовки и учётные данные
response1 = session.get('https://api.example.com/v1/resource/1')
response2 = session.get('https://api.example.com/v1/resource/2')
Разбор фрагмента:
requests.Session()создаёт объект с общими настройками и пулом соединений.headers.update(...)задаёт постоянные заголовки для всех запросов сессии.session.auth = (...)включает базовую аутентификацию по умолчанию.- Повторные
session.get(...)выполняются эффективнее за счёт переиспользования соединений.
Преимущества Session:
- единый пул соединений для всех запросов (уменьшает накладные расходы на TLS-рукопожатия);
- централизованное управление заголовками, аутентификацией, настройками (включая таймауты по умолчанию через
session.request); - возможность подключения middleware через
session.mount()(например, для автоматических повторов).
Авторизация — OAuth, токены, заголовки
Большинство публичных API требуют идентификации клиента. Основные схемы:
- API-ключи — самый простой вариант. Ключ передаётся в заголовке или параметре запроса:
headers = {'Authorization': 'Token abc123xyz'}
# или
params = {'api_key': 'abc123xyz'}
requests.get(url, headers=headers)
- HTTP Basic Auth — учётные данные в заголовке
Authorization: Basic base64(login:password). Вrequests:
from requests.auth import HTTPBasicAuth
requests.get(url, auth=HTTPBasicAuth('user', 'pass'))
# или короче:
requests.get(url, auth=('user', 'pass'))
- Bearer-токены (JWT, OAuth2 access tokens) — наиболее распространённый стандарт сегодня:
headers = {'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'}
- OAuth 2.0 — протокол делегированной авторизации.
requestsсам по себе не реализует OAuth, но интегрируется сrequests-oauthlib, который поддерживает все grant types (authorization code, client credentials, refresh token и др.).
Когда выбирать API-ключ, а когда JWT и OAuth — авторизация в интеграционных сценариях.
Частая ошибка — хранение токенов и ключей в коде. В production они должны извлекаться из переменных окружения, менеджеров секретов (HashiCorp Vault, AWS Secrets Manager) или конфигурационных файлов вне VCS. Для локальной разработки удобно использовать .env и python-dotenv:
# .env
GITHUB_TOKEN=ghp_abc123...
# app.py
import os
from dotenv import load_dotenv
load_dotenv()
headers = {'Authorization': f'Bearer {os.getenv("GITHUB_TOKEN")}'}
Важно — при использовании токенов, имеющих срок жизни (например, access token в OAuth2), необходимо предусматривать механизм обновления — либо через refresh token, либо через повторную аутентификацию. Это делается на уровне приложения, часто с кэшированием валидного токена в памяти.
Обработка ответов — JSON, статус-коды, исключения
После получения Response необходимо выполнить несколько обязательных шагов:
- Проверить статус-код. Успешный HTTP-запрос — не всегда успешная бизнес-операция.
if response.status_code == 200:
data = response.json()
elif response.status_code == 404:
raise ResourceNotFoundError()
elif response.status_code == 429:
# Too Many Requests — нужно сделать паузу
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
else:
response.raise_for_status() # бросит HTTPError для 4xx/5xx
Метод response.raise_for_status() — удобный способ выбросить исключение requests.HTTPError для всех неуспешных кодов. Однако он не различает 4xx (ошибка клиента) и 5xx (ошибка сервера), поэтому в продакшене часто требуется более тонкая обработка.
- Безопасное чтение JSON. Не все ответы содержат валидный JSON — даже при коде 200. Например, при перегрузке сервер может вернуть HTML-страницу с сообщением об ошибке. Поэтому:
try:
data = response.json()
except ValueError as e: # в requests 2.x это requests.JSONDecodeError, наследник ValueError
logging.error(f"Невозможно разобрать JSON: {response.text[:500]}")
raise
- Валидация структуры ответа. Даже если JSON корректен, он может не соответствовать ожидаемой схеме (например, поле
emailотсутствует). Здесь помогают схемные валидаторы:
from pydantic import BaseModel, HttpUrl
from typing import List
class User(BaseModel):
id: int
login: str
avatar_url: HttpUrl
email: str | None = None
try:
user = User.model_validate(data)
except ValidationError as e:
logging.error(f"Некорректная структура пользователя: {e}")
Пример — получение данных с GitHub API
Рассмотрим комплексный пример — получение списка репозиториев пользователя GitHub с обработкой пагинации, ошибок и таймаутов.
Код ITЗагрузка примера кода…
Ключевые особенности реализации:
- Использование
Sessionдля единообразия заголовков и эффективности соединений; - Явная обработка Rate Limit через заголовки
X-RateLimit-*— критически важно для публичных API; - Пагинация через параметры и заголовок
Link(стандарт RFC 5988); - Логирование ошибок с контекстом (номер страницы);
- Выборка только необходимых полей — минимизация зависимости от структуры ответа;
- Отказ от
response.raise_for_status()в "сыром" виде — вместо этого — кейс-обработка для 403 и 404.
Такой подход обеспечивает устойчивость к временным сбоям, соблюдение правил API и читаемость кода.
Отправка email и уведомлений
Концептуально, отправка электронного письма — это клиент-серверное взаимодействие по протоколу SMTP (Simple Mail Transfer Protocol). Python предоставляет стандартную библиотеку smtplib, реализующую клиентскую часть протокола. Однако сам по себе SMTP — лишь транспорт. Для корректной доставки необходимо соблюсти множество дополнительных требований — форматирование заголовков, кодирование тела, поддержка вложений, аутентификация, шифрование соединения и совместимость с антиспам-фильтрами.
smtplib — отправка писем
Минимальный пример отправки текстового письма через публичный SMTP-сервер (например, Gmail):
Код ITЗагрузка примера кода…
Разбор фрагмента:
MIMEMultipart()из модуляemailсоздаёт контейнер письма с заголовками и частями тела.MIMEText(..., "plain", "utf-8")формирует текстовую часть с явной кодировкой.smtplib.SMTP(..., 587)открывает SMTP-сеанс,starttls()переключает его на TLS.login(...)выполняет аутентификацию,send_message(...)отправляет письмо.- Обработка
SMTPExceptionпозволяет поймать сетевые и протокольные ошибки.
Модуль email — современный вариант через EmailMessage
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Отчет за день"
msg["From"] = "noreply@example.com"
msg["To"] = "team@example.com"
msg.set_content("Готово 12 задач, 2 в работе.")
msg.add_alternative("<h3>Готово 12 задач</h3><p>2 в работе.</p>", subtype="html")
Разбор фрагмента:
EmailMessage— современный API из stdlibemail, который проще старых MIME-классов.set_content(...)задаёт plain-text версию письма.add_alternative(..., subtype="html")добавляет HTML-версию того же письма.- Почтовый клиент сам выберет лучший формат для отображения.
Разберём ключевые компоненты:
-
Формирование письма
Простая строка недостаточна — письмо — это структура с заголовками (From,To,Subject,Date,Message-ID) и телом, возможно, в нескольких форматах (текст + HTML). Классыemail.mime.*позволяют строить такие структуры в соответствии со стандартами RFC 5322 и RFC 2045–2049 (MIME).MIMEMultipart: контейнер для нескольких частей (например,text/plainиtext/html);MIMEText: часть с текстом; параметры — содержимое, subtype (plainилиhtml), кодировка;MIMEImage,MIMEApplication— для вложений.
-
Шифрование соединения
Порт587используется для SMTP Submission с последующим переходом на защищённое соединение через командуSTARTTLS. Это предпочтительнее устаревшего нешифрованного порта 25 или порта 465 (SMTPS, устаревший, но ещё встречающийся).
Методstarttls()должен вызываться доlogin()— иначе учётные данные будут переданы в открытом виде. -
Аутентификация
Современные почтовые сервисы запрещают вход по паролю от аккаунта. Вместо этого используются:- Пароли приложений (Gmail, Yandex) — одноразовые токены, привязанные к конкретному клиенту;
- OAuth 2.0 — более безопасный, но сложный в реализации метод (требуется
requests-oauthlibи обработка flow).
-
Обработка ошибок
smtplibвыбрасывает иерархию исключений, наследующих отSMTPException:SMTPRecipientsRefused— сервер отверг получателя (например, несуществующий email);SMTPSenderRefused— проблема с отправителем (неподтверждённый домен, спам-репутация);SMTPDataError— ошибка при передаче тела письма.
В production важно логировать текст ошибки и код ответа сервера (
e.smtp_code,e.smtp_error), поскольку он позволяет точно диагностировать причину (например, 550 — недоставляемый адрес, 421 — временная перегрузка).
Персонализированные рассылки
Массовая отправка требует дополнительных мер:
- Throttling — ограничение скорости отправки, чтобы не превысить лимиты SMTP-сервера (например, 100 писем/час у бесплатного Gmail);
- Обработка ошибок на уровне получателя — отказ одного адресата не должен останавливать всю рассылку;
- Шаблонизация — динамическое формирование тела с подстановкой имён, ссылок, данных.
Пример шаблонизированной рассылки с отложенной повторной отправкой при временной ошибке:
Код ITЗагрузка примера кода…
Важные замечания:
- Использование
formataddr()гарантирует корректное экранирование имён (например, если вnameесть запятые или кавычки); MIMEMultipart('alternative')указывает почтовому клиенту выбрать наиболее подходящий формат (обычно HTML, если поддерживается);- Повторное подключение при
SMTPServerDisconnected— стандартная практика для long-running рассылок.
Для высоконагруженных сценариев рекомендуется выносить отправку в фоновую очередь (Celery, RQ) и использовать dedicated email-сервисы (SendGrid, Mailgun, Amazon SES), которые предоставляют API, аналитику и встроенную обработку отписок/bounces.
Очередь, state machine, hard/soft bounce, suppression list, webhooks ESP и SPF/DKIM/DMARC — в главе Email-рассылка как распределённая система.
Краткий разбор для system design — пример в карте system design.
Telegram-боты — python-telegram-bot
Telegram API предлагает два режима взаимодействия:
- Polling — клиент регулярно опрашивает сервер (
getUpdates); - Webhook — сервер Telegram отправляет обновления на ваш endpoint по HTTPS.
Для уведомлений чаще всего используется прямая отправка сообщений через sendMessage, без обработки входящих команд — то есть в режиме "бот как клиент уведомлений". Для этого достаточно знать chat_id получателя и токен бота.
Библиотека python-telegram-bot (v20+) предоставляет высокоуровневый асинхронный API поверх Telegram Bot API.
Установка:
pip install python-telegram-bot --pre # для v20+ (асинхронная версия)
Пример отправки текстового уведомления:
Код ITЗагрузка примера кода…
Ключевые возможности:
- HTML- и MarkdownV2-разметка — выделение, ссылки, код;
- Клавиатуры — inline-кнопки (
InlineKeyboardMarkup) для интерактивных уведомлений (например, "Подтвердить", "Отменить"); - Вложения — отправка изображений (
send_photo), документов (send_document), голосовых сообщений; - Ограничения Telegram — 30 сообщений/сек на chat, 1 сообщение/сек на пользователя в личных чатах. При превышении — ошибка
RetryAfter.
Для получения chat_id можно:
- Написать боту
/start; - Отправить POST-запрос к
https://api.telegram.org/bot<TOKEN>/getUpdatesи извлечьmessage.chat.idиз ответа.
Пример — уведомление об ошибках
Объединим рассмотренные подходы в реалистичный сценарий: мониторинг критических исключений в приложении и оповещение администратора.
Код ITЗагрузка примера кода…
Подключение хендлера:
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.addHandler(AlertHandler())
Такая архитектура обеспечивает:
- Градуированную доставку: мгновенное оповещение в Telegram → резервное письмо → локальный лог;
- Ограничение объёма: обрезка трассировки до 1000/4000 символов (ограничения Telegram/email-клиентов);
- Изоляцию сбоев: сбой в одном канале не блокирует другой;
- Интеграцию в стандартный logging-стек — ошибки можно логировать в файл, Sentry, Loki и одновременно отправлять администратору.
Практический маршрут внедрения
Чтобы перевести материал в рабочий проект, удобно идти итерациями:
- Шаг 1: Минимальный сервис
Поднимите Flask-приложение сGET /healthиGET /version. - Шаг 2: Первый ресурс API
Добавьте CRUD для сущности "users" с корректными HTTP-статусами. - Шаг 3: Клиент к внешнему API
Подключитеrequestsс явными таймаутами и обработкой 429/5xx. - Шаг 4: Нотификации
Настройте Telegram/email-оповещения только для критических ошибок. - Шаг 5: Эксплуатация
Запустите под Gunicorn за Nginx, вынесите секреты в env, включите мониторинг.

Метрики CPU, памяти и сетевой нагрузки хоста — в Практикуме Prometheus и Grafana; готовые запросы для /metrics — Prometheus + Grafana — запросы.
Такой маршрут помогает не "перепрыгивать" через уровни сложности и быстрее получить устойчивый результат.
Что ещё изучить по теме
- Сетевое программирование на Python — сокеты, TLS и справочник сетевых библиотек (requests, paramiko, aiohttp, dnspython и др.).
- Flask и веб-приложения — соседние паттерны Python-веба.
- Операционные системы и процессы — полезно для деплоя и диагностики.
- Веб-сайты и как они работают — архитектурный контекст фронтенд/бэкенд.
Практикум по production-модулям веба
Эти модули удобно добавлять поверх Flask/FastAPI в рабочем сервисе.
Celery и фоновые задачи
from celery import Celery
celery_app = Celery("worker", broker="redis://localhost:6379/0")
@celery_app.task
def send_email_task(user_id: int) -> None:
print(f"Send email for user={user_id}")
Разбор фрагмента:
Celery("worker", broker=...)создаёт приложение воркера и подключает брокер сообщений Redis.@celery_app.taskрегистрирует функцию как фоновую задачу.- HTTP-ручка может вызывать
send_email_task.delay(user_id), а выполнение произойдёт отдельно от веб-процесса.
Dramatiq как легковесная очередь
import dramatiq
from dramatiq.brokers.redis import RedisBroker
dramatiq.set_broker(RedisBroker(host="localhost", port=6379))
@dramatiq.actor
def recalc_stats(project_id: int) -> None:
print(f"Recalculate stats for project={project_id}")
Разбор фрагмента:
set_broker(...)задаёт общий брокер для всех actor-задач.@dramatiq.actorпревращает функцию в фоновую задачу Dramatiq.- В коде приложения задачу отправляют как
recalc_stats.send(project_id), а выполняет её отдельный worker.
Pydantic для контрактов API
from pydantic import BaseModel, EmailStr
class UserIn(BaseModel):
name: str
email: EmailStr
Разбор фрагмента:
UserInзадаёт схему входного payload для API.EmailStrвалидирует формат email до бизнес-логики и БД.- Схема помогает централизованно отбрасывать некорректные запросы и формировать предсказуемые ошибки.
HTTPX для сервис-сервис вызовов
import httpx
def call_billing(user_id: int) -> dict:
with httpx.Client(timeout=5.0) as client:
r = client.get(f"https://billing.internal/users/{user_id}")
r.raise_for_status()
return r.json()
Разбор фрагмента:
- Функция инкапсулирует интеграцию с внешним сервисом в одном месте.
timeout=5.0защищает сервис от зависания на сетевых вызовах.raise_for_status()переводит HTTP-ошибки в исключения, чтобы их можно было обработать в слое API.- Возврат
dictупрощает дальнейшую обработку данных в приложении.
Loguru для структурированных логов
from loguru import logger
logger.bind(service="users-api").info("User created id={}", 42)
Разбор фрагмента:
bind(service="users-api")добавляет контекстное поле ко всем следующим записям.info(...)пишет сообщение с форматированием через{}.- Такой стиль удобен для централизованного лог-анализа в ELK/Loki/Splunk.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.
В подборках
Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:
Веб-разработка — Python — о разделе, Социальные сети, Приложение с S3, PostgreSQL и ASP.NET Core Web API, Low-code и No-code платформы, Документация и практика ASP.NET (Microsoft Learn), ASP.NET - фреймворк для веб-приложений.