In many cases your application could need some external settings or configurations, for example secret keys, database credentials, credentials for email services, etc.
Most of these settings are variable (can change), like database URLs. And many could be sensitive, like secrets.
For this reason it's common to provide them in environment variables that are read by the application.
If you already know what "environment variables" are and how to use them, feel free to skip to the next section below.
An environment variable (also known as "env var") is a variable that lives outside of the Python code, in the operating system, and could be read by your Python code (or by other programs as well).
You can create and use environment variables in the shell, without needing Python:
fast →💬 You could create an env var MY_NAME withexport MY_NAME="Wade Wilson" 💬 Then you could use it with other programs, likeecho "Hello $MY_NAME" Hello Wade Wilson
You could also create environment variables outside of Python, in the terminal (or with any other method), and then read them in Python.
For example you could have a file main.py with:
importosname=os.getenv("MY_NAME","World")print(f"Hello {name} from Python")
Tip
The second argument to os.getenv() is the default value to return.
If not provided, it's None by default, here we provide "World" as the default value to use.
Then you could call that Python program:
fast →💬 Here we don't set the env var yetpython main.py 💬 As we didn't set the env var, we get the default value Hello World from Python
💬 But if we create an environment variable firstexport MY_NAME="Wade Wilson" 💬 And then call the program againpython main.py 💬 Now it can read the environment variable Hello Wade Wilson from Python
As environment variables can be set outside of the code, but can be read by the code, and don't have to be stored (committed to git) with the rest of the files, it's common to use them for configurations or settings.
You can also create an environment variable only for a specific program invocation, that is only available to that program, and only for its duration.
To do that, create it right before the program itself, on the same line:
fast →💬 Create an env var MY_NAME in line for this program callMY_NAME="Wade Wilson" python main.py 💬 Now it can read the environment variable Hello Wade Wilson from Python
💬 The env var no longer exists afterwardspython main.py Hello World from Python
These environment variables can only handle text strings, as they are external to Python and have to be compatible with other programs and the rest of the system (and even with different operating systems, as Linux, Windows, macOS).
That means that any value read in Python from an environment variable will be a str, and any conversion to a different type or validation has to be done in code.
In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality.
If you want something quick to copy and paste, don't use this example, use the last one below.
Then, when you create an instance of that Settings class (in this case, in the settings object), Pydantic will read the environment variables in a case-insensitive way, so, an upper-case variable APP_NAME will still be read for the attribute app_name.
Next it will convert and validate the data. So, when you use that settings object, you will have data of the types you declared (e.g. items_per_user will be an int).
Next, you would run the server passing the configurations as environment variables, for example you could set an ADMIN_EMAIL and APP_NAME with:
fast →ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
In some occasions it might be useful to provide the settings from a dependency, instead of having a global object with settings that is used everywhere.
This could be especially useful during testing, as it's very easy to override a dependency with your own custom settings.
If you have many settings that possibly change a lot, maybe in different environments, it might be useful to put them on a file and then read them from it as if they were environment variables.
This practice is common enough that it has a name, these environment variables are commonly placed in a file .env, and the file is called a "dotenv".
Tip
A file starting with a dot (.) is a hidden file in Unix-like systems, like Linux and macOS.
But a dotenv file doesn't really have to have that exact filename.
The Config class is used just for Pydantic configuration. You can read more at Pydantic Model Config.
Info
In Pydantic version 1 the configuration was done in an internal class Config, in Pydantic version 2 it's done in an attribute model_config. This attribute takes a dict, and to get autocompletion and inline errors you can import and use SettingsConfigDict to define that dict.
Here we define the config env_file inside of your Pydantic Settings class, and set the value to the filename with the dotenv file we want to use.
Reading a file from disk is normally a costly (slow) operation, so you probably want to do it only once and then re-use the same settings object, instead of reading it for each request.
But every time we do:
Settings()
a new Settings object would be created, and at creation it would read the .env file again.
If the dependency function was just like:
defget_settings():returnSettings()
we would create that object for each request, and we would be reading the .env file for each request. ⚠️
But as we are using the @lru_cache decorator on top, the Settings object will be created only once, the first time it's called. ✔️
Then for any subsequent calls of get_settings() in the dependencies for the next requests, instead of executing the internal code of get_settings() and creating a new Settings object, it will return the same object that was returned on the first call, again and again.
@lru_cache modifies the function it decorates to return the same value that was returned the first time, instead of computing it again, executing the code of the function every time.
So, the function below it will be executed once for each combination of arguments. And then the values returned by each of those combinations of arguments will be used again and again whenever the function is called with exactly the same combination of arguments.