Fast API - Environment Variables

Fast API - Environment Variables

Creating and Reading ENVs

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:

startup-env-default.png

# 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.

startup-env-cache.png

# 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.

startup-env-file.png

# 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.