Python: уроки, справочник и задачи

Python requests post: отправка данных на сервер

Раздел: Модули и библиотеки Python · Обновлено 25.10.2026
Серверные стойки в серверной
Фото: Alexis Lê-Quôc from New York, United States, CC BY-SA 2.0

POST-запрос нужен, когда клиент передает данные серверу: отправляет форму, создает запись через API, загружает файл. В Python для этого почти всегда используют библиотеку requests, и отправить простой запрос можно одной строкой. Сложности начинаются дальше: чем data отличается от json, почему запрос "висит" без таймаута, как повторять запрос после сбоя и не создать при этом дубль заказа. Все это разберем по порядку на рабочих примерах.

Установка и первый запрос

Библиотека не входит в стандартную поставку, ее ставят через pip в виртуальное окружение проекта:

pip install requests

Минимальный POST-запрос выглядит так:

import requests

response = requests.post(
    "https://api.example.com/v1/notes",
    json={"title": "Купить хлеб", "done": False},
    timeout=10,
)
print(response.status_code)   # -> 201
print(response.json())        # -> {'id': 42, 'title': 'Купить хлеб', 'done': False}

Функция requests.post() возвращает объект Response. Самые нужные его атрибуты: status_code (число), ok (True для кодов меньше 400), headers (заголовки ответа, регистронезависимый словарь), text (тело как строка), content (тело как байты) и метод json(). Общий обзор библиотеки, включая GET-запросы и параметры URL, есть в статье про модуль requests.

Параметры data и json: в чем разница

Это самый частый источник ошибок при работе с API. Оба параметра отправляют тело запроса, но в разном формате и с разным заголовком Content-Type.

ПараметрЧто передатьФормат телаContent-Type
data=dictсловарьtitle=abc&done=0application/x-www-form-urlencoded
data=str или bytesготовое телокак естьне ставится, задайте сами
json=objсловарь, список, числоJSON-строкаapplication/json
files=dictфайлы и поляmultipartmultipart/form-data; boundary=...

HTML-форма на сайте отправляет данные как x-www-form-urlencoded, поэтому для имитации формы используйте data. Современные REST API почти всегда ждут JSON, для них используйте json.

payload = {"login": "anna", "tags": ["python", "web"]}

# форма: tags уйдет как tags=python&tags=web
r1 = requests.post(url, data=payload, timeout=10)

# JSON: {"login": "anna", "tags": ["python", "web"]}
r2 = requests.post(url, json=payload, timeout=10)

Типичная ошибка с вложенными данными

Если передать вложенный словарь в data, requests не превратит его в JSON. Сервер получит что-то вроде user=name&user=age, то есть только ключи вложенного словаря. Признак проблемы: API отвечает 400 или 415 (Unsupported Media Type), хотя данные "правильные". Решение: использовать json=.

Если по каким-то причинам JSON нужно сериализовать вручную (например, со своими настройками), передайте строку в data и выставьте заголовок сами:

import json

body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
r = requests.post(url, data=body,
                  headers={"Content-Type": "application/json; charset=utf-8"},
                  timeout=10)

Параметры ensure_ascii и работа с кириллицей в JSON подробно описаны в статье о том, как записать и прочитать JSON-файл.

Заголовки запроса

Заголовки передают словарем в параметре headers. Чаще всего это токен авторизации, тип принимаемого ответа и идентификатор клиента.

headers = {
    "Authorization": "Bearer <ваш_токен>",
    "Accept": "application/json",
    "User-Agent": "my-app/1.4",
}
r = requests.post(url, json=payload, headers=headers, timeout=10)
print(r.request.headers["Content-Type"])   # -> application/json

Несколько практических правил:

Отправка файлов

Файлы отправляют через параметр files. Ключ словаря это имя поля формы, значение это открытый в бинарном режиме файл или кортеж с подробностями.

from pathlib import Path

path = Path("report.pdf")
with path.open("rb") as f:
    r = requests.post(
        "https://api.example.com/v1/upload",
        files={"document": (path.name, f, "application/pdf")},
        data={"comment": "отчет за квартал"},   # обычные поля формы рядом с файлом
        timeout=(5, 60),
    )
r.raise_for_status()

Формат кортежа: (имя_файла, файловый_объект_или_байты, content_type). Можно добавить четвертый элемент, словарь дополнительных заголовков части. Несколько файлов в одном поле передаются списком пар:

files = [
    ("images", ("a.png", open("a.png", "rb"), "image/png")),
    ("images", ("b.png", open("b.png", "rb"), "image/png")),
]

Файл всегда открывайте в режиме "rb". В текстовом режиме на Windows могут исказиться переводы строк, а размер тела не совпадет с реальным. Requests собирает multipart-тело в памяти целиком, поэтому для файлов в сотни мегабайт нужен потоковый вариант: data=f без multipart, если API это поддерживает, или пакет requests-toolbelt с его MultipartEncoder.

Статус-коды и разбор ответа

Requests не выбрасывает исключение на ответы 4xx и 5xx: для библиотеки ответ 500 тоже успешно полученный ответ. Проверку нужно делать явно.

КодЗначение для POSTЧто делать
200 OKзапрос выполнен, в теле результатчитать r.json()
201 Createdресурс созданвзять id из тела или заголовок Location
202 Acceptedпринято в обработкуопрашивать статус задачи
204 No Contentуспех без телане вызывать r.json()
400, 422ошибка в данныхпоказать текст ошибки, не повторять
401, 403нет доступапроверить токен, не повторять
409конфликт, запись уже естьобработать как бизнес-ошибку
429слишком много запросовподождать Retry-After и повторить
500, 502, 503, 504сбой сервера или шлюзаповторить с паузой
r = requests.post(url, json=payload, timeout=10)

if r.status_code == 204:
    result = None
elif r.ok:
    result = r.json()
else:
    print(r.status_code, r.text[:500])   # сервер обычно объясняет ошибку в теле
    r.raise_for_status()                 # HTTPError для 4xx и 5xx

Метод r.json() выбрасывает requests.exceptions.JSONDecodeError, если тело пустое или не является JSON. Так бывает, когда прокси вместо JSON отдает HTML-страницу с ошибкой 502. Перед разбором полезно проверить заголовок: r.headers.get("Content-Type", "").startswith("application/json").

Таймауты: обязательный параметр

По умолчанию у requests нет таймаута. Если сервер принял соединение и замолчал, программа будет ждать бесконечно. Поэтому timeout указывают в каждом запросе без исключений.

# одно число: и на соединение, и на ожидание каждого блока ответа
requests.post(url, json=payload, timeout=10)

# кортеж: (connect, read)
requests.post(url, json=payload, timeout=(3.05, 30))

Таймаут чтения это не время всего запроса, а максимальная пауза между байтами ответа. Медленный, но живой поток данных может длиться дольше 30 секунд. Таймаут соединения стоит делать небольшим (3-5 секунд): если сервер недоступен, лучше узнать это быстро. Для долгих операций на сервере (генерация отчета) увеличивайте таймаут чтения, а не соединения.

Обработка ошибок сети

Все исключения библиотеки наследуются от requests.exceptions.RequestException. Основные из них:

import requests
from requests.exceptions import ConnectTimeout, ReadTimeout, ConnectionError, HTTPError, RequestException

def create_note(title):
    try:
        r = requests.post(URL, json={"title": title}, timeout=(3, 15))
        r.raise_for_status()
        return r.json()
    except ConnectTimeout:
        print("сервер не отвечает на подключение")
    except ReadTimeout:
        print("сервер подключился, но не прислал ответ вовремя")
    except HTTPError as e:
        print("ошибка HTTP", e.response.status_code, e.response.text[:200])
    except ConnectionError:
        print("сетевая ошибка")
    except RequestException as e:
        print("прочая ошибка requests:", e)
    return None

Порядок блоков важен: более узкие исключения ставят выше. Особенно опасен ReadTimeout для POST: запрос мог дойти до сервера и выполниться, а ответ просто не успел вернуться. Слепой повтор в этом случае создаст вторую запись. Общие принципы построения блоков try описаны в материале про обработку исключений.

Сессии: общие заголовки и переиспользование соединений

requests.Session хранит заголовки, cookies и пул соединений между запросами. Если вы делаете больше одного запроса к одному хосту, сессия ускоряет работу (не нужно заново устанавливать TCP и TLS) и убирает дублирование кода.

import os
import requests

with requests.Session() as s:
    s.headers.update({
        "Authorization": f"Bearer {os.environ['API_TOKEN']}",
        "Accept": "application/json",
    })
    for title in ["хлеб", "молоко", "сыр"]:
        r = s.post("https://api.example.com/v1/notes", json={"title": title}, timeout=10)
        r.raise_for_status()

Сессия также автоматически сохраняет cookies, которые сервер установил при логине через форму, и подставляет их в следующие запросы. Таймаут на уровне сессии задать нельзя, его по-прежнему указывают в каждом вызове.

Вход через форму и последующий POST

with requests.Session() as s:
    s.post("https://example.com/login",
           data={"username": "anna", "password": os.environ["APP_PASSWORD"]},
           timeout=10).raise_for_status()
    r = s.post("https://example.com/profile",
               data={"city": "Казань"}, timeout=10)
    print(r.status_code)

Повторные попытки (retry)

Встроенного параметра retries у requests.post нет, но к сессии можно подключить адаптер с политикой повторов из urllib3. Важная деталь: по умолчанию Retry не повторяет POST, потому что этот метод не идемпотентен. Его нужно явно добавить в allowed_methods.

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry = Retry(
    total=4,
    backoff_factor=0.5,                       # паузы растут экспоненциально
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=frozenset(["GET", "POST"]),
    respect_retry_after_header=True,
    raise_on_status=False,                    # вернуть последний ответ, а не RetryError
)

session = requests.Session()
adapter = HTTPAdapter(max_retries=retry)
session.mount("https://", adapter)
session.mount("http://", adapter)

r = session.post(url, json=payload, timeout=(3, 15))

Повторять POST безопасно только если сервер умеет распознавать повтор. Многие платежные и учетные API принимают заголовок Idempotency-Key: клиент генерирует уникальный ключ (например, uuid.uuid4()) и отправляет его со всеми попытками одного и того же действия. Сервер, увидев знакомый ключ, вернет результат первого запроса вместо создания дубля.

import uuid

key = str(uuid.uuid4())
r = session.post(url, json=payload,
                 headers={"Idempotency-Key": key}, timeout=(3, 15))

Если API такого не поддерживает, ограничьте повторы ситуациями, когда запрос точно не дошел до сервера: ошибка соединения и коды 429, 503. Параметр Retry(connect=3, read=0, ...) разрешает повторы при сбое подключения и запрещает при таймауте чтения.

Готовый шаблон для работы с API

import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

class ApiClient:
    def __init__(self, base_url, token, timeout=(3, 20)):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.s = requests.Session()
        self.s.headers.update({"Authorization": f"Bearer {token}",
                               "Accept": "application/json"})
        retry = Retry(total=3, connect=3, read=0, backoff_factor=0.5,
                      status_forcelist=[429, 502, 503, 504],
                      allowed_methods=frozenset(["GET", "POST"]),
                      raise_on_status=False)
        self.s.mount("https://", HTTPAdapter(max_retries=retry))

    def post(self, path, payload):
        r = self.s.post(f"{self.base_url}/{path.lstrip('/')}",
                        json=payload, timeout=self.timeout)
        if r.status_code == 204:
            return None
        r.raise_for_status()
        return r.json()

api = ApiClient("https://api.example.com/v1", os.environ["API_TOKEN"])
note = api.post("/notes", {"title": "позвонить"})

Если нужна асинхронность или HTTP/2, посмотрите на HTTP-клиент httpx: его API почти совпадает с requests, так что перенос кода занимает минуты.

Частые ошибки при отправке POST

Ниже собраны ошибки, которые чаще всего встречаются при разборе чужого кода, работающего с API. Каждая из них приводит либо к зависаниям, либо к трудноуловимым сбоям в продакшене.

  1. Нет таймаута. Скрипт работает неделями, а потом однажды зависает навсегда, потому что удаленный сервер принял соединение и перестал отвечать. Таймаут нужен в каждом вызове.
  2. Нет проверки статуса. Код вызывает r.json() сразу после запроса, и при ответе 502 с HTML-страницей падает с непонятной ошибкой разбора JSON. Сначала проверьте r.ok или вызовите raise_for_status().
  3. Ручной заголовок Content-Type для multipart. Заголовок без параметра boundary делает тело нечитаемым для сервера. При использовании files заголовок должна выставлять библиотека.
  4. Новое соединение на каждый запрос. Тысяча вызовов requests.post подряд означает тысячу TLS-рукопожатий. Сессия сокращает время в разы.
  5. Логирование секретов. Вывод r.request.headers целиком в лог раскрывает токен авторизации. Маскируйте заголовок Authorization перед записью.
  6. Повтор POST без защиты от дублей. Автоматический retry при таймауте чтения может создать два заказа или два платежа. Используйте ключ идемпотентности или повторяйте только ошибки соединения.

Как отлаживать POST-запросы

Когда сервер отвечает ошибкой, первым делом нужно увидеть, что именно ушло по сети. Объект r.request хранит подготовленный запрос: метод, итоговый URL с параметрами, заголовки и тело. Сравните его с примером из документации API: чаще всего расходится тип содержимого, имя поля или формат даты.

Второй шаг: прочитать тело ответа с ошибкой. Хорошие API возвращают в нем описание проблемы, например список полей, не прошедших проверку. Выводите первые несколько сотен символов r.text, а не только код статуса.

Третий шаг: включить подробный журнал библиотеки urllib3 через модуль logging на уровне DEBUG. В журнале видны установка соединений, повторные попытки и редиректы. Это помогает понять, почему запрос выполняется дольше ожидаемого или почему сработал retry.

Наконец, если запрос из браузера работает, а из Python нет, сравните заголовки. Некоторые серверы требуют определенный User-Agent, заголовок Referer или CSRF-токен из cookies, который браузер передает автоматически, а скрипт нет. В этом случае сначала выполните GET-запрос страницы с формой в рамках сессии, извлеките токен и передайте его вместе с данными формы.

Частые вопросы

Почему кириллица уходит на сервер escape-последовательностями?

Параметр json= сериализует данные с ensure_ascii=True, поэтому русские буквы превращаются в escape-последовательности. Это корректный JSON, и любой нормальный сервер его разберет. Если API требует именно UTF-8 символы, сериализуйте вручную через json.dumps(..., ensure_ascii=False).encode("utf-8") и передайте в data.

Как добавить параметры в URL у POST-запроса?

Через параметр params, как в GET: requests.post(url, params={"lang": "ru"}, json=payload). Тело и строка запроса передаются независимо.

Что делать с ошибкой SSLError?

Обычно у сервера самоподписанный сертификат или в системе устарели корневые сертификаты. Правильное решение: указать путь к сертификату через verify="/path/ca.pem" или обновить пакет certifi. Параметр verify=False отключает проверку полностью, его допустимо использовать только для локальной отладки.

Почему после редиректа POST превратился в GET?

Так требует стандарт для кодов 301, 302 и 303, и браузеры ведут себя так же. Коды 307 и 308 сохраняют метод и тело. Если API отвечает 301 на POST, скорее всего, вы забыли слеш в конце URL или используете http вместо https.

Как протестировать код с requests без реального сервера?

Подмените сетевой слой: пакеты responses и requests-mock перехватывают вызовы и возвращают заданные ответы. В pytest это удобно оформить фикстурой.