Конфигурация Python-приложения: env, файлы и секреты
Как разделить настройки для dev и prod, не хранить пароли в git и не получить AttributeError на старте. Разбираем три подхода к конфигурации и цену каждого.

Приложение падает на проде с KeyError: 'DATABASE_URL', потому что переменную забыли выставить в systemd-юните. На следующей неделе в публичный репозиторий утекает settings.py с боевым паролем к Postgres, и его находят сканеры за минуты. Оба инцидента — про одно и то же: конфигурация приложения не отделена от кода и не защищена. Разберём три способа хранить настройки в Python-проекте, чем каждый платит и когда какой брать. Код проверялся на Python 3.11 и 3.12.
Что вообще относится к конфигурации
Конфигурация — это всё, что меняется между окружениями: адрес базы, ключи внешних API, уровень логирования, флаги фич, режим отладки. Код в dev и prod один и тот же, а вот значения разные. Отсюда правило, которое проще всего сформулировать так:
Если значение отличается между вашим ноутбуком и продом — это конфигурация, и в исходниках её быть не должно. Если оно одинаковое везде — это часть кода, и переменной окружения ему не нужно.
Секреты — подмножество конфигурации: пароли, токены, приватные ключи. Их отличает не формат, а последствия утечки. Поэтому обращение с ними жёстче: они не попадают ни в git, ни в логи, ни в текст ошибки.
Подход 1. Переменные окружения напрямую
Базовый вариант: приложение читает os.environ. Ничего не устанавливаете сверх стандартной библиотеки.
import os
# os.environ["KEY"] бросит KeyError, если переменной нет —
# это лучше, чем тихо получить None и упасть глубже
DATABASE_URL = os.environ["DATABASE_URL"]
# для необязательных значений — get с дефолтом
DEBUG = os.environ.get("DEBUG", "0") == "1"
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
Главная ловушка здесь — типы. Всё, что приходит из окружения, — строка. os.environ.get("DEBUG") вернёт "0", а не False, и наивная проверка if os.environ.get("DEBUG"): будет истинной при любом непустом значении, включая "0" и "false". Числа тоже надо приводить руками через int(), ловя ValueError.
Что выигрываете: ноль зависимостей, прозрачность, совместимость с любым способом деплоя — Docker, systemd, CI. Чем платите: ручное приведение типов, разбросанные по коду обращения к os.environ и отсутствие валидации — опечатку в имени переменной вы найдёте только в рантайме.
Подход 2. .env-файл для локальной разработки
Держать десять export в терминале неудобно. Локально их складывают в файл .env и подгружают библиотекой python-dotenv.
# файл .env в корне проекта
DATABASE_URL=postgresql://localhost:5432/myapp
DEBUG=1
LOG_LEVEL=DEBUG
# pip install python-dotenv
from dotenv import load_dotenv
import os
# load_dotenv по умолчанию НЕ перезаписывает уже существующие
# переменные окружения — реальный env с прода имеет приоритет
load_dotenv()
DATABASE_URL = os.environ["DATABASE_URL"]
Ключевой момент, который путают: .env — это инструмент разработчика, а не механизм деплоя. На проде переменные задаёт оркестратор или systemd, а не файл в репозитории. Поэтому .env добавляют в .gitignore, а рядом кладут .env.example с теми же ключами, но без значений — чтобы новый разработчик знал, что нужно заполнить.
.env— в.gitignore, с реальными значениями, только локально;.env.example— в git, с пустыми или фейковыми значениями, как шаблон;- на проде
load_dotenv()просто ничего не найдёт и не сломается — переменные придут из окружения.
Проверьте прямо сейчас: git status не должен показывать .env как неотслеживаемый или изменённый. Если показывает — вы на шаг от утечки секретов в историю коммитов.
Подход 3. Типизированные настройки через pydantic-settings
Когда переменных много и хочется валидации на старте, а не падений в глубине запроса, берут pydantic-settings (отдельный пакет, вынесен из ядра в Pydantic 2.x).
# pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
database_url: str
debug: bool = False # "1", "true", "yes" -> True автоматически
log_level: str = "INFO"
max_connections: int = 10 # приведение к int тоже автоматом
# при импорте всё окружение прочитается и провалидируется сразу.
# нет database_url -> ValidationError на старте, а не на первом запросе
settings = Settings()
Здесь исчезают обе боли первого подхода: типы приводятся по аннотациям, а отсутствие обязательного поля роняет приложение при импорте с понятным сообщением, какого именно ключа не хватает. Имена переменных в окружении матчатся с полями без учёта регистра: поле database_url подхватит DATABASE_URL.
Цена: зависимость от Pydantic и его версии. Синтаксис BaseSettings заметно менялся между Pydantic 1.x и 2.x — в первой версии настройки задавались внутренним классом Config, во второй появился model_config и отдельный пакет pydantic-settings. Если копируете чужой пример и получаете ImportError на from pydantic import BaseSettings — это код под первую версию. Точный набор доступных полей SettingsConfigDict сверяйте в документации вашей версии.
Сравнение подходов
| Критерий | os.environ | python-dotenv | pydantic-settings |
|---|---|---|---|
| Зависимости | нет | 1 пакет | Pydantic + пакет |
| Приведение типов | вручную | вручную | по аннотациям |
| Валидация на старте | нет | нет | да |
| Удобство локально | низкое | высокое | высокое |
| Порог входа | минимальный | низкий | средний |
Когда что выбирать
- Скрипт или мелкая утилита, две-три переменные —
os.environнапрямую, не тащите зависимости. - Веб-приложение, локальная разработка командой —
python-dotenvдля dev-удобства как минимум; секреты всё равно из окружения на проде. - Много настроек, нужна валидация и типы —
pydantic-settings. Один объектsettings, импортируемый по всему проекту, и падение на старте вместо сюрприза в проде.
Секреты: где проходит граница
Env-переменные и .env решают задачу «настройки отдельно от кода», но не задачу «секрет защищён». Переменная окружения видна любому процессу пользователя, попадает в дампы и иногда в логи краш-репортеров. Для боевых секретов этого мало.
Что делать помимо вынесения в окружение:
- Никогда не логируйте объект настроек целиком. В Pydantic поле секрета оборачивают в
SecretStr— при выводе в лог оно печатается как**********, а реальное значение достаётся явным.get_secret_value(). - Не кладите секреты в аргументы командной строки — они видны в
psлюбому пользователю системы. - Для прода рассмотрите менеджер секретов: HashiCorp Vault, AWS Secrets Manager, Docker/Kubernetes secrets. Приложение получает значение в рантайме, а не хранит его в файле рядом с кодом.
- Если секрет всё же попал в git-историю — смены его в
.gitignoreнедостаточно. Значение считается скомпрометированным: ротируйте пароль или ключ, а не пытайтесь «затереть» коммит.
SecretStr на практике
from pydantic import SecretStr
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
api_token: SecretStr
settings = Settings()
print(settings) # api_token=SecretStr('**********')
print(settings.api_token.get_secret_value()) # реальное значение — только явно
Если не сработало
KeyError: 'DATABASE_URL'— переменная не задана в текущем окружении. Проверьтеecho $DATABASE_URLв той же сессии, откуда запускаете; в systemd — секциюEnvironment=илиEnvironmentFile=юнита..envне подхватывается —load_dotenv()ищет файл от текущей рабочей директории вверх, а не от расположения скрипта. Запускаете из другой папки — передайте путь явно:load_dotenv("/path/to/.env").ImportError: cannot import name 'BaseSettings' from 'pydantic'— код под Pydantic 1.x. В 2.x импорт идёт изpydantic_settings, пакет ставится отдельно.- Булев флаг ведёт себя странно — вы сравниваете строку.
"False"из окружения истинна вif. Приводите явно или используйтеpydantic-settingsс аннотациейbool.
Что настроить дальше
Соберите настройки в один модуль config.py и импортируйте settings оттуда — обращения к os.environ по всему коду плохо ищутся и не валидируются. Добавьте .env.example в репозиторий, а .env — в .gitignore, и проверьте историю коммитов на уже утёкшие секреты (инструменты вроде gitleaks сканируют её локально). Для прода определитесь с менеджером секретов до первого релиза, а не после первого инцидента.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: pydantic-settings — официальная документация, python-dotenv — репозиторий на GitHub
Частые вопросы
А если у меня Pydantic 1.x, а не 2.x?
Можно ли хранить .env на проде?
Что делать, если секрет уже попал в git?
Нужен ли pydantic-settings для маленького скрипта?
Почему булев флаг DEBUG всегда включён?
Как безопасно логировать настройки при отладке?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.