Работа с API по токену в Python: рабочие приёмы
Токен, вбитый прямо в код, утекает в git. Токен без таймаута вешает поток на минуты. Разбираем, как хранить, обновлять и передавать токены в requests и httpx без граблей.

Токен для доступа к API — это не строка, которую вставляют в переменную и забывают. Он утекает в репозиторий, протухает посреди долгой выгрузки, ломает поток при первом же 401 и молча висит на медленном соединении. Ниже — приёмы для типовых задач: где держать токен, как передавать его в запрос, как переживать истечение и rate limit. Код на Python 3.11+, библиотеки requests 2.31+ и httpx 0.27+. Ротацию ключей на стороне провайдера и OAuth-флоу с браузером не трогаем — здесь только клиентская работа с уже полученным токеном.
Где хранить токен
Самая частая ошибка — TOKEN = "eyJhbGci..." в теле скрипта. Дальше файл коммитят, и токен живёт в истории git навсегда, даже если строку потом удалить. Очевидное решение — переменные окружения, и для большинства скриптов этого достаточно.
import os
token = os.environ["API_TOKEN"] # KeyError, если переменной нет — это хорошо
Использование os.environ["API_TOKEN"] вместо os.environ.get(...) — сознательный выбор: пусть скрипт падает на старте с понятной ошибкой, а не отправляет запрос с None в заголовке и получает невнятный 401 через сорок строк.
Для локальной разработки удобно держать токен в файле .env и подхватывать через пакет python-dotenv. Файл .env обязательно добавляют в .gitignore — иначе смысла в нём нет.
from dotenv import load_dotenv
import os
load_dotenv() # читает .env из текущей директории
token = os.environ["API_TOKEN"]
Правило простое: токен нигде не должен встречаться в коде, который попадает в систему контроля версий. Ни как строка, ни как значение по умолчанию, ни в тестовой фикстуре.
Как передавать токен в запрос
Три подхода — от быстрого к правильному. Разница в том, сколько раз вы повторяете себя и как легко потом сменить схему авторизации.
1. Заголовок вручную в каждом запросе
import requests
headers = {"Authorization": f"Bearer {token}"}
response = requests.get("https://api.example.com/v1/orders", headers=headers, timeout=10)
Годится для скрипта из пяти строк. Как только запросов становится больше двух, вы начинаете копировать headers и рано или поздно где-то его забудете.
2. Session с общими заголовками
requests.Session держит заголовки и переиспользует TCP-соединение (connection pooling), что заметно ускоряет серию запросов к одному хосту.
import requests
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {token}"})
orders = session.get("https://api.example.com/v1/orders", timeout=10)
users = session.get("https://api.example.com/v1/users", timeout=10)
3. Собственный класс авторизации
Если токен может меняться в процессе работы (истёк и обновился), логику авторизации выносят в отдельный объект. В requests для этого есть базовый класс requests.auth.AuthBase.
import requests
from requests.auth import AuthBase
class BearerAuth(AuthBase):
def __init__(self, token: str):
self.token = token
def __call__(self, request):
request.headers["Authorization"] = f"Bearer {self.token}"
return request
session = requests.Session()
session.auth = BearerAuth(token)
response = session.get("https://api.example.com/v1/orders", timeout=10)
Теперь схема авторизации в одном месте, а не размазана по вызовам.
| Подход | Когда брать | Чем платите |
|---|---|---|
| Заголовок вручную | Одноразовый скрипт, 1–2 запроса | Дублирование, легко забыть заголовок |
| Session с headers | Серия запросов к одному API | Токен фиксирован на всё время сессии |
| AuthBase / кастомный класс | Токен обновляется, несколько API | Больше кода, нужно понимать хук |
Таймаут — не опция, а обязательное
Ни один из примеров выше не должен идти без timeout. По умолчанию requests ждёт ответа бесконечно: если сервер завис, ваш поток встанет навсегда. Указывайте таймаут явно в каждом вызове или задавайте на уровне обёртки.
# кортеж: (таймаут на соединение, таймаут на чтение)
response = session.get("https://api.example.com/v1/orders", timeout=(3.05, 27))
В httpx таймаут можно задать один раз на клиент, и он применится ко всем запросам:
import httpx
client = httpx.Client(
base_url="https://api.example.com",
headers={"Authorization": f"Bearer {token}"},
timeout=10.0,
)
response = client.get("/v1/orders")
Токен протух посреди работы
Токены с ограниченным сроком жизни возвращают 401, когда истекают. Наивный код падает; правильный — обновляет токен и повторяет запрос один раз. Ключевое слово — один раз: если после обновления снова 401, дело не в сроке, и повтор превратится в бесконечный цикл.
import requests
def fetch_token() -> str:
"""Получить свежий токен. Реализация зависит от вашего провайдера."""
# здесь запрос к endpoint выдачи токена по вашим учётным данным
...
def get_with_retry(session: requests.Session, url: str, token_box: dict):
response = session.get(url, timeout=10)
if response.status_code == 401:
token_box["token"] = fetch_token()
session.auth = BearerAuth(token_box["token"])
response = session.get(url, timeout=10) # ровно один повтор
response.raise_for_status()
return response
Токен держат в изменяемом контейнере (token_box), чтобы обновлённое значение переиспользовали все последующие запросы, а не только текущий.
Rate limit: сервер просит подождать
Ответ 429 (Too Many Requests) означает, что вы превысили лимит. Многие API отдают заголовок Retry-After с числом секунд ожидания. Уважайте его вместо фиксированного sleep.
import time
import requests
def request_respecting_limit(session, url, max_attempts=5):
for attempt in range(max_attempts):
response = session.get(url, timeout=10)
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
continue
response.raise_for_status()
return response
raise RuntimeError("Превышен лимит попыток из-за rate limit")
Если Retry-After нет — откатывайтесь с экспоненциальной задержкой (2 ** attempt: 1, 2, 4, 8 секунд). Точное имя и формат заголовка лимита у вашего API уточняйте в его документации: часть провайдеров отдаёт время в секундах, часть — HTTP-датой.
Что ломается на практике
- Лишние пробелы в токене. Если читаете токен из файла,
token.strip()спасает от невидимого\nв конце, из-за которого сервер отдаёт 401 без объяснений. - Токен в логах. При отладке легко залогировать весь объект заголовков — и токен уедет в лог-файл. Логируйте статус и URL, но не
headers. - Токен в URL. Некоторые API принимают токен как query-параметр. Так делать не стоит: URL попадает в логи прокси, историю браузера и заголовок Referer. Держите токен только в заголовке
Authorization. - Отсутствие
raise_for_status(). Без него ответ с кодом 500 пройдёт как успешный, и вы будете парсить тело ошибки как данные.
Что настроить дальше
Когда базовая работа с токеном налажена, следующие шаги: вынести токен из .env в менеджер секретов (для продакшена переменные окружения хранят в системе оркестрации или в отдельном хранилище, а не в файле рядом с кодом); настроить автоматический повтор транспортных сбоев через urllib3.util.retry.Retry, смонтированный в HTTPAdapter; для асинхронных задач перейти на httpx.AsyncClient. Что мерить в проде: долю ответов 401 и 429 — их рост сигналит о протухшем токене или слишком агрессивных запросах раньше, чем вы получите жалобы.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: requests: Authentication, HTTPX documentation
Частые вопросы
У меня requests другой версии — код сработает?
Что делать, если после обновления токена снова прилетает 401?
requests или httpx — что выбрать?
Можно ли хранить токен в keyring вместо .env?
Почему запросы иногда просто зависают?
Как проверить, что токен не утёк в репозиторий?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.