Python Dotenv with FastAPI, Flask, and Django
python dotenv with fastapi flask and django: Learn how to use python-dotenv to load environment variables in FastAPI, Flask, and Django, including framework-specific s...
When you build a Python web application with FastAPI, Flask, or Django, you often need to keep configuration values such as database URLs, API keys, and secret keys outside your source code. The python-dotenv package reads key-value pairs from a .env file and makes them available as environment variables. This article explains how to integrate python dotenv with fastapi flask and django correctly, covering the differences in each framework's startup sequence and configuration loading.
Why python-dotenv Exists
Environment variables are the standard way to configure a Python application across different environments. However, setting them manually in a shell session is tedious and error-prone. A .env file stores these variables in a simple format:
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=your-secret-key
DEBUG=true
The python-dotenv library parses this file and populates os.environ so that your application code can read the values with os.getenv() or os.environ. It does not replace the need for environment variables; it only automates the loading step.
Installing python-dotenv
Install the package with pip:
pip install python-dotenv
In most projects, you will also add it to your dependency list, for example in requirements.txt or pyproject.toml. The package is framework-agnostic, so the same installation works for FastAPI, Flask, and Django.
Loading a .env File Manually
The simplest usage is to call load_dotenv() at the beginning of your application entry point. This function looks for a file named .env in the current directory and loads its contents into os.environ. By default, it does not override existing environment variables; if a variable is already set in the shell, the .env value is ignored.
from dotenv import load_dotenv load_dotenv() # loads .env from current directory import os database_url = os.getenv("DATABASE_URL")
You can specify a path if the file is not in the working directory:
load_dotenv("/path/to/.env")
This manual approach works in any Python script, but web frameworks have their own initialization flow. You need to know where to place the call so that the variables are available before your settings are evaluated.
Using python-dotenv with Flask
Flask has built-in support for .env files when python-dotenv is installed. When you run flask run or use the Flask CLI, Flask automatically loads .env and .flaskenv files from the project root. The .flaskenv file is specifically for Flask CLI configuration, such as FLASK_APP and FLASK_DEBUG, while .env is for application variables.
If you are not using the Flask CLI—for example, when running your app with Gunicorn or uWSGI—you must load the file explicitly. The recommended place is at the top of your wsgi.py or the module that creates the app instance.
# wsgi.py from dotenv import load_dotenv load_dotenv() from myapp import create_app app = create_app()
If you are using an application factory pattern, you can also load the file inside the factory before reading configuration:
# app/__init__.py from dotenv import load_dotenv from flask import Flask def create_app(): load_dotenv() app = Flask(__name__) app.config["SECRET_KEY"] = os.getenv("SECRET_KEY") # ... return app
Keep in mind that Flask's automatic loading only happens when you use the flask command. In production, you will likely run the app with a WSGI server, so an explicit load_dotenv() is necessary.
Using python-dotenv with Django
Django does not load .env files automatically. You must call load_dotenv() before Django reads your settings. The most common place is at the top of manage.py and wsgi.py (and asgi.py if you use ASGI). This ensures the variables are available when settings.py is imported.
# manage.py #!/usr/bin/env python import os import sys from dotenv import load_dotenv if __name__ == "__main__": load_dotenv() os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv)
# wsgi.py import os from dotenv import load_dotenv load_dotenv() from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") application = get_wsgi_application()
You also need to do the same in asgi.py if you are using Django Channels or an ASGI server.
Once loaded, you can reference the variables in settings.py using os.getenv() or os.environ.get():
# settings.py import os SECRET_KEY = os.getenv("DJANGO_SECRET_KEY") DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": os.getenv("DB_NAME"), "USER": os.getenv("DB_USER"), "PASSWORD": os.getenv("DB_PASSWORD"), "HOST": os.getenv("DB_HOST"), "PORT": os.getenv("DB_PORT"), } }
Because load_dotenv() is called in manage.py, the variables are available for management commands as well. This is important for migrations, shell commands, and any custom management scripts.
Using python-dotenv with FastAPI
FastAPI does not have built-in .env loading. You have two common options: call load_dotenv() at the top of your main module, or use Pydantic's BaseSettings with a dotenv parameter. The latter is more integrated if you are already using Pydantic models for configuration.
Manual Loading
In your main.py:
from dotenv import load_dotenv load_dotenv() from fastapi import FastAPI import os app = FastAPI() @app.get("/") def read_root(): return {"database_url": os.getenv("DATABASE_URL")}
This works, but it couples the loading to the module import order. If you have multiple modules that read environment variables at import time, you need to ensure load_dotenv() runs before those imports. A more robust approach is to load the file in a dedicated config module that is imported first.
Using Pydantic Settings
Pydantic's BaseSettings can read from environment variables and a .env file. This is a common pattern in FastAPI projects:
# config.py from pydantic import BaseSettings class Settings(BaseSettings): database_url: str secret_key: str class Config: env_file = ".env" settings = Settings()
When you instantiate Settings(), Pydantic reads the .env file and also checks the actual environment. The precedence is: real environment variables override .env values. This is similar to python-dotenv's default behavior but is built into Pydantic.
You can then use settings in your application:
from config import settings app = FastAPI() @app.get("/") def root(): return {"db": settings.database_url}
If you are using Pydantic v2, the configuration class is slightly different:
from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): database_url: str secret_key: str model_config = SettingsConfigDict(env_file=".env")
This approach centralizes configuration and gives you type validation. It is often preferred in FastAPI projects because FastAPI already uses Pydantic for request and response models.
Loading Order and Precedence
Understanding when .env values are loaded relative to existing environment variables is critical. By default, load_dotenv() does not overwrite variables that are already set in the environment. This means that if you have a variable DATABASE_URL set in your shell, the .env file value will be ignored.
If you want .env to take precedence, you can pass override=True:
load_dotenv(override=True)
However, this is rarely recommended because it can mask real environment variables that you set intentionally for production. The default behavior is safer: it lets you set variables in the shell for one-off runs while keeping the .env file as a baseline for local development.
In Django, you can also use os.environ.setdefault() to set defaults without overriding existing values. This is useful when you want to provide a fallback in code but still allow environment variables to win.
For Pydantic's BaseSettings, the precedence is: real environment variables first, then values from the .env file. This matches the default python-dotenv behavior.
Security Considerations
A .env file often contains sensitive information such as API keys, database passwords, and secret keys. You must ensure that this file is never committed to version control. Add .env to your .gitignore file. Instead, commit a .env.example file with placeholder values so other developers know which variables are required.
# .env.example
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=change-me
Also be careful about logging. Do not print environment variables in debug output or error messages. If you use a configuration management system, consider using secrets management tools for production, but .env files are fine for local development and many small deployments.
Another point is that python-dotenv does not encrypt the file. It is plain text. If your server is compromised, the .env file is readable. In production, you might prefer to set environment variables directly in the process manager or use a secrets vault. The .env file is primarily a developer convenience.
Framework-Specific Pitfalls
Each framework has a few traps that can lead to missing environment variables.
Flask: Automatic Loading Only with CLI
If you run your Flask app with python app.py instead of flask run, the automatic .env loading does not happen. You must call load_dotenv() manually. Also, the .flaskenv file is only read by the CLI, so variables needed by your app should go in .env.
Django: Multiple Entry Points
Django has several entry points: manage.py, wsgi.py, asgi.py, and sometimes custom scripts. If you only load .env in manage.py, your WSGI server will not have the variables when it imports the settings. You must add load_dotenv() to every entry point that starts the application.
FastAPI: Import Order
If you call load_dotenv() in main.py but other modules are imported before that line, they may read environment variables before they are loaded. For example, if a database connection is created at module import time, it will fail. To avoid this, load the .env file as early as possible, ideally in a separate module that is imported first, or use Pydantic settings that are instantiated after the load.
When to Use Manual Loading vs. Framework Integration
For Flask and Django, manual load_dotenv() is the standard approach because those frameworks do not have a native configuration object that reads .env files. For FastAPI, you have the choice between manual loading and Pydantic settings. Use Pydantic settings if you want type validation and a single configuration object. Use manual loading if you prefer to keep dependencies minimal or if you are not already using Pydantic extensively.
In all cases, the key is to load the .env file before your application reads any configuration values. The exact placement depends on the framework's startup sequence, which we have covered above.
Handling Missing Variables Gracefully
When a required environment variable is not set, your application should fail fast with a clear error message rather than silently using None. You can use os.environ directly and raise an exception if a key is missing:
import os try: secret_key = os.environ["SECRET_KEY"] except KeyError: raise RuntimeError("SECRET_KEY environment variable is not set")
Or use os.getenv with a default and validate later. In Django, you can use os.environ.setdefault to provide a default, but be careful not to hide configuration errors.
For Pydantic settings, missing fields will raise a ValidationError at startup, which is helpful. This is one reason why Pydantic settings are popular in FastAPI projects.
Conclusion
Integrating python dotenv with fastapi flask and django is straightforward once you understand each framework's initialization order. Flask loads .env automatically only when using the CLI, Django requires explicit loading in every entry point, and FastAPI gives you the flexibility to use python-dotenv directly or through Pydantic settings. Always respect the default precedence where real environment variables override .env values, and keep your .env file out of version control. With these patterns, you can manage configuration consistently across development, staging, and production environments.