Конфиги и окружения
три беды одного маленького файла
Что это
Окружение — это набор настроек, с которыми запускается один и тот же код. Обычно их три: твоя машина, тестовый сервер, боевой сервер. Отличаются они не кодом, а значениями: адрес базы, ключи API, включена ли отладка.
Настройки выносят из кода по двум причинам: чтобы не хранить секреты в репозитории (статья 5.7) и чтобы одна и та же программа работала в разных местах. Способ при этом везде один — переменные окружения, и в них есть ровно одна тонкость, из-за которой ломаются проекты: всё, что приходит из окружения, — это строка.
Как выглядит в коде
Файл настроек, который выглядит нормально
import os
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///test.db")
PORT = os.environ.get("PORT", 8000)
DEBUG = os.environ.get("DEBUG", False)Три строки, три беды. Причём на твоей машине, где переменных нет, всё безупречно:
база : sqlite:///test.db
порт : 8000 int
отладка: False bool
-> режим отладки выключен
порт + 1 = 8001Теперь то же самое с переменными — так, как запускается на сервере:
PORT=8080 DEBUG=false DATABASE_URL=postgresql://prod/db python3 app.pyбаза : postgresql://prod/db
порт : '8080' str
отладка: 'false' str
-> режим отладки ВКЛЮЧЁН
порт + 1 = TypeError: can only concatenate str (not "int") to strРазберём по одной.
1. DEBUG=false включил отладку. Строка 'false' — непустая, а значит,
истинная. Проверка if DEBUG: срабатывает. На боевом сервере это означает
подробные страницы с ошибками, трассировки наружу и всё, что обычно видят
только разработчики. Ошибка в одну строку, последствия — утечка.
2. Порт стал строкой. TypeError — ещё удачный случай: он хотя бы
громкий. Хуже, когда строка молча проходит дальше.
3. Тип зависит от того, задана ли переменная. На твоей машине PORT —
число, на сервере — строка. Один и тот же код, разные типы. Проверить это
локально нельзя в принципе.
И отдельно — самая тихая из трёх: DATABASE_URL со значением по умолчанию
sqlite:///test.db. Забыли задать переменную на сервере — приложение не упало,
не пожаловалось, а начало писать в локальный файл. Узнаёшь об этом, когда
данные ищут и не находят.
Как надо
from pydantic import Field
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
api_key: str
port: int = 8000
debug: bool = False
timeout: float = Field(default=5.0, gt=0)
settings = Settings()Разница вся в типах. port: int — значит, при запуске строка '8080'
превратится в число или программа не запустится. debug: bool — значит,
false станет настоящим False. У database_url и api_key умолчания нет —
значит, без них запуска не будет.
Запуск без переменных:
pydantic_core._pydantic_core.ValidationError: 2 validation errors for Settings
database_url
Field required [type=missing, input_value={}, input_type=dict]
api_key
Field required [type=missing, input_value={}, input_type=dict]Обе недостающие переменные названы сразу, при старте. Не по одной, не в середине работы, не через час после выкладки.
С переменными:
база : postgresql://prod/db
порт : 8080 int
отладка: False bool
порт+1 : 8081DEBUG=false стал False, как и ожидает любой здравый смысл. А если значение
окажется мусором:
port
Input should be a valid integer, unable to parse string as an integer
[type=int_parsing, input_value='вось', input_type=str]Названа настройка, названо значение, названа причина. Это и есть разница между «программа упала» и «программа объяснила».
Что лежит в репозитории, а что нет
.env ← настоящие значения. В .gitignore. Никогда не коммитится.
.env.example ← те же имена, пустые или примерные значения. Коммитится..env.example — маленький файл с большой пользой: это список того, что
нужно задать, чтобы проект запустился. Без него новый человек (или ты сам
через полгода на другой машине) узнаёт о недостающей переменной методом проб.
DATABASE_URL=postgresql://user:password@localhost/dbname
API_KEY=
PORT=8000
DEBUG=falseИ напоминание из статьи 12.4: в шаблонах проектов строки .env в
.gitignore может не оказаться. Проверяется это один раз, командой
git ls-files, и лучше в первый день.
Три окружения — один код
Правило простое: код не знает, где он работает. Нет if environment == "production", разбросанных по проекту; есть настройки, которые в разных местах
разные.
Единственное исключение, которое оправдано, — одна переменная вроде
ENV=production и несколько решений на её основе в одном месте: уровень
логов, показывать ли подробные ошибки, включать ли документацию API.
Как сказать ИИ
Сделай модуль настроек на pydantic-settings.
Нужны: database_url (строка, обязательная),
api_key (строка, обязательная), port (int, по умолчанию 8000),
debug (bool, по умолчанию false).
Умолчаний для обязательных не ставь.
Рядом — .env.example со всеми именами.Найди в проекте все обращения к os.environ и os.getenv.
Для каждого скажи: тип значения, есть ли умолчание
и что произойдёт, если переменная не задана.
Таблицей. Код пока не меняй.Этот запрос стоит выполнить на любом проекте, которому больше месяца: обращения к окружению расползаются незаметно.
Проверь этот конфиг на три ошибки: строка вместо числа,
строка 'false' вместо булева значения, умолчание там,
где его быть не должно. Что нашёл — перечисли.Проект должен падать при старте, если не заданы
обязательные настройки. Сейчас он падает позже.
Сделай проверку при запуске, с понятным сообщением,
в котором перечислено всё недостающее сразу.На что смотреть
os.environ.get с умолчанием для адреса базы или ключа. Самая тихая из
ошибок этой статьи.
Настройка используется как число или как флаг без приведения типа. Из окружения приходит строка, всегда.
DEBUG=true как строка. Любая непустая строка истинна, включая 'false'
и '0'.
Настройки читаются в десяти местах. Один модуль настроек, остальной код обращается к нему.
.env в репозитории. Проверяется git ls-files.
Нет .env.example. Проект нельзя запустить, не угадав список переменных.
Ключи заданы прямо в коде «временно». Временное живёт дольше всего, а git помнит всё (статья 5.7).
if production: по всему проекту. Код знает, где он работает, — значит, на
тестовом сервере проверяется не то, что поедет в бой.
Шпаргалка
Всё, что приходит из окружения, — строка. Всегда.
DEBUG=falseбез приведения типа включает отладку.Умолчание для обязательной настройки прячет ошибку, а не помогает.
Типизированные настройки называют все недостающие переменные сразу.
Настройки проверяются при старте, а не в момент использования.
.env— в.gitignore,.env.example— в репозиторий.Один модуль настроек; остальной код читает его, а не окружение.
Код не должен знать, на каком сервере он работает.