Introduction
Recap... the first article gave a simple Hello World
setup to get FastAPI up and running. This article will take the next step and introduce using environment variables in your FastAPI project.
Environment Variables
Environment variables are used for specifying external configurations or settings such as: credentials, secrets, important stuff we need to remember, etc. Not just are they important but they can change from time to time, so they need to be in a location for quick access to change and be available for any resource or application that is utilizing them.
First, let's review how we set environment variables on macOS or similar *nix environments. Read my short article on Environment Variables as a refresher.
Now let's review how to read those environment variables in a FastAPI project.
Read Environment Variables in FastAPI
Reading environment variables can be done with the os
and the dotenv
libraries to read both system or file-based variables. It is more or less a default common practice in Python development. The example below shows how this is done.
import os
from dotenv import dotenv_values
# Python Environment Variable setup required on System or .env file
config_env = {
**dotenv_values(".env"), # load local file development variables
**os.environ, # override loaded values with system environment variables
}
# Access the variable like below
# print(config_env["VAR_NAME"])
This is a fine option but not the best option when working with FastAPI.
Let's review how to do this the FastAPI way according to the documentation.
Option 1 - Pyndatic Settings()
Code for this example can be found here- Default Option. The README provides startup steps.
Utilizing the Pydantic Settings Management utility is the recommended option when working with environment variables in a FastAPI project. This option will walk through creating a global class instance of your environment variables to be shared in your FastAPI project.
This is done by importing Pydantic's BaseSettings
and creating a class Settings()
just as with any Pydantic model.
We'll follow good standards and create a config file to host our environment variable settings rather than having this clutter up the main app.py
file. When the Settings()
class is instantiated, Pydantic will read in the environment variables (case sensitive) for each of the attributes you created in your class.
Default values will be supplied to the class instance if the environment variables do not exist.
Caution when using default values if you are trying to avoid checking in sensitive information in your code repository.
# config.py
from pydantic import BaseSettings
class Settings(BaseSettings):
DEFAULT_VAR="some default string value" # default value if env variable does not exist
API_KEY: str
APP_MAX: int=100 # default value if env variable does not exist
# global instance
settings = Settings()
With the config in a separate file, you can import it into your main app.py
file.
In our example the route /vars
will return the class attributes that were imported.
# app.py
from fastapi import FastAPI
# import settings for variable access
from config import settings
app = FastAPI()
@app.get("/vars")
async def info():
return {
"default variable": settings.DEFAULT_VAR,
"api key": settings.API_KEY,
"app max integer": settings.APP_MAX,
}
I created my python virtual environment and started the server by including the environment variables at invocation. The API_KEY
and APP_MAX
are set at invocation but the DEFAULT_VAR
will be the default value set up in the Pydantic class because I am not specifying a value for it. Hitting the API will provide the result below:
# Response body for /vars
{
"default variable": "some default string value",
"api key": "SECRETKEY",
"app max integer": 799
}
Option 2 - Environment Variables using LRU Cache
You can follow along with the following GitHub Project - Cache Option. The README provides startup steps.
Instead of creating a global settings instance, another way of reading environment variables is to utilize LRU (Least Recently Used) Cache. This creates a dependency of Settings()
instead of a default instance of settings=Settings()
. For this option, more imports are required: LRU Cache, Depends, and the class Settings()
. Note, that the Settings()
class is imported not the instance settings
, the global instance is no longer needed for this option.
# import
from functools import lru_cache
from fastapi import Depends, FastAPI
from config import Settings
app = FastAPI()
# New decorator for cache
@lru_cache()
def get_settings():
return Settings()
# route is now using the Depends feature to import Settings
@app.get("/vars")
async def info(settings: Settings = Depends(get_settings)):
return {
"default variable": settings.DEFAULT_VAR,
"api key": settings.API_KEY,
"app max integer": settings.APP_MAX,
}
The config.py
file change for this option is just removing the created class instance (last line from the option 1). Everything else is the same.
# Updated config.py file
from pydantic import BaseSettings
class Settings(BaseSettings):
DEFAULT_VAR="some default string value" # default value if env variable does not exist
API_KEY: str
APP_MAX: int=100 # default value if env variable does not exist
I'm starting the virtual environment and server by including the environment variables at invocation just like before.
# Response body for /vars
{
"default variable": "some default string value",
"api key": "SECRETKEY",
"app max integer": 799
}
Option 3 - Environment Variables in .env
File
You can follow along with the following GitHub Project - Env File Option. The README provides startup steps.
The last thing we'll cover is to set up environment variables in a local .env
file instead of passing them during invocation. I prefer using env files that I can leave in a project.
# Example .env file
API_KEY="my file key"
APP_MAX="199"
REMEMBER: Add the .env file to your
.gitignore
file so it won't be checked into your repository.
Your new .env
file is added to the config.py
file as part of the class attributes.
# config.py
from pydantic import BaseSettings
class Settings(BaseSettings):
DEFAULT_VAR="some default string value" # default value if env variable does not exist
API_KEY: str
APP_MAX: int=100 # default value if env variable does not exist
# specify .env file location as Config attribute
class Config:
env_file = ".env.sample"
Pydantic will now look explicitly in your file to read in environment variables. I'm starting the virtual environment and server now with the following command.
# Response body for /vars
{
"default variable": "some default string value",
"api key": "my file key",
"app max integer": 199
}
End
The official FastAPI docs are great and explain in further detail the use of LRU Cache and other options around Environment Variables and Settings. Read them here.
In the next article, we'll review API Key authorization.