Part 3 of 7 · 3 min read
Typed settings in code
Reading every setting once, in one place, checked and typed, so that a missing key stops the program on the first second.
Do not scatter calls to os.getenv around the code. Read everything once, in one place, check it, and pass the settings object around. The example project uses a library called pydantic-settings, which builds on the Pydantic you met in module 1:
$ pip install pydantic-settings
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_", extra="ignore")
llm_api_key: SecretStr # required: start-up fails if missing
llm_name: str = "small-1"
request_timeout_s: float = 30.0
log_level: str = "INFO"
Read it: the class lists the settings and their types. env_prefix="APP_" means the variable for llm_name is called APP_LLM_NAME, so your settings do not clash with other programs' variables. env_file=".env" means "also read a file called .env in the folder you run the program from". llm_api_key has no default value, so it is required. SecretStr is a type for secrets that we will come back to.
What you get, shown with real output:
Fail fast. If APP_LLM_API_KEY is not set, the program stops at start-up, not an hour into production:
$ python -c "from settings import load_settings; load_settings()"
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
llm_api_key
Field required [type=missing, input_value={}, input_type=dict]
The top of the output is a long Python traceback. The last lines are the message that matters: the setting llm_api_key is required and missing.
Types and secrets. With a .env file containing the key, a timeout of 5 and a model name, the settings load like this:
>>> s = load_settings()
>>> s
Settings(llm_api_key=SecretStr('**********'), llm_name='from-file', request_timeout_s=5.0, log_level='INFO')
>>> type(s.request_timeout_s)
<class 'float'>
APP_REQUEST_TIMEOUT_S=5 in the file was text, and became the number 5.0. And the key prints as **********. That is SecretStr: printing the settings, or logging them by accident, shows stars instead of the key. You read the real value only at the moment you need it, with .get_secret_value().
Fail fast
Detecting a problem as early as possible, at the moment it first could be known, instead of letting the program run on until the problem causes confusing damage far away.
Which value wins?
If the same setting is given in several places, one has to win. Here is the order, which I confirmed by running the example with APP_LLM_NAME set in three places:
| Source | llm_name it gave |
Rank |
|---|---|---|
An environment variable set in the shell (from-shell) |
from-shell |
1st: wins |
The .env file (from-file) |
from-file |
2nd |
The default in the class (small-1) |
small-1 |
3rd: only if nothing else is set |
The order is: real environment variable, then .env file, then the default. That is what you want. A host can override a developer's file without anyone editing it.
The names in the settings class and in .env.example must match. The committed example file shows only names:
APP_LLM_API_KEY=
APP_LLM_NAME=small-1
APP_REQUEST_TIMEOUT_S=30
APP_LOG_LEVEL=INFO
A newcomer copies it to .env and fills in the key. Your tests can set variables with monkeypatch.setenv (module 5).
Check yourself
- What does
SecretStrdo when you print the settings? - A setting is given in the shell, in
.envand as a default. Which one wins? - Why is it better to stop at start-up than to fail later?