88.5 dynaconf: Layered Configuration for Multiple Environments
Right, let’s talk about configuration. You’ve outgrown sticking database URLs in your code. You have development, staging, production, maybe even that “test” environment that’s just your laptop with a different hat on. Managing this with a bunch of if statements and .env files you have to remember to swap out is a recipe for connecting your production API to your local SQLite database. We’ve all done it. Let’s not do it again.
Enter dynaconf. It’s not just another config library; it’s a layered configuration management system. Think of it like an onion (it has layers, and it might make you cry if you do it wrong). Each layer can override the previous one, which is how you get a solid baseline config that gets specialized for each environment without repeating yourself. The genius is in the order of operations.
The Core Concepts: It’s All About Layers
The magic, and the occasional headache, comes from understanding its loading order. It reads from least important to most important, so later sources can override earlier ones. The default order is:
- Defaults: Internal defaults (you barely use these).
- Settings Files: Your
.toml,.yaml,.ini, or.jsonfiles. This is your bedrock. - Environment Variables: The most forceful overrider. A
DEBUG=trueset here will stomp on any value in any file. This is by design and is your best friend for secrets and deployment-specific settings. - External Secrets: Vault, Redis, etc. (Advanced, we’ll skip this for now).
The key takeaway: environment variables win. Always. This is crucial for Twelve-Factor App compliance and for stopping you from accidentally shipping debug mode because you forgot to update a file.
Setting Up: Your settings.toml is Your New Best Friend
First, install it: pip install dynaconf. Now, let’s structure our project. You’ll typically have a base configuration file and then environment-specific ones. Dynaconf loves TOML, and so should you—it’s a sane format with actual types.
# your_project/settings.toml
[default]
database = "sqlite:///./default.db"
port = 5000
debug = false
message = "This is from default"
[development]
database = "sqlite:///./dev.db"
debug = true
[production]
database = "postgresql://user:pass@prod-db:5432/myapp"
port = 8030
debug = false
See what we did there? [default] is our baseline. [development] and [production] only define what’s different. No repeated keys. Clean.
Now, to use this in your app, you don’t use os.getenv like a peasant. You use the dynaconf object.
# your_project/app.py
from dynaconf import Dynaconf
settings = Dynaconf(
settings_files=['settings.toml'], # Path to your file
environments=True, # Enable the multi-environment magic
env_switcher="MYAPP_ENV", # The env var to switch environments
)
print(f"Database URL: {settings.database}")
print(f"Debug mode: {settings.debug}")
How does it know what environment to use? It looks for the MYAPP_ENV environment variable. No variable? It defaults to development because the developers assume you’re probably messing around locally, which is both thoughtful and slightly insulting. To run for production, you’d do:
export MYAPP_ENV=production
python your_project/app.py
The .secrets.toml and Why You Should Use It
You just committed your database password to GitHub. Congratulations. To avoid this, dynaconf automatically tries to load a .secrets.toml file. This file is gitignored by default if you use their CLI tool. This is where your passwords, API keys, and other shameful secrets live.
# .secrets.toml
[default]
admin_password = "default_pass" # Still not great, but better than nothing
[production]
database_password = "sup3r_s3cr3t_p@ssw0rd!"
Now, in your main settings.toml, you can reference the secret:
[production]
database = "postgresql://user:${database_password}@prod-db:5432/myapp"
Dynaconf will seamlessly merge the value from .secrets.toml into your main config. It’s a simple but incredibly effective pattern.
Common Pitfalls and How to Avoid Them
The Boolean Env Var Trap: Environment variables are strings.
export DEBUG=falseactually sets the value to the string"false", which is truthy in Python. Dynaconf is smart enough to cast common strings like"false","0","no"toFalse. But if you’re using a custom variable, remember:settings.get('MY_VAR', cast=bool)is your friend.The Nested Key Mystery: Accessing nested keys in TOML can be confusing. For a section like
[server.auth], you don’t use dot notation in the file, but you do use it in code.[server] [server.auth] secret_key = "key"In code:
settings.server.auth.secret_keyor the safersettings.get('server.auth.secret_key').The Silent Failure on Missing Keys: By default,
settings.MISSING_KEYreturnsNone. This is terrible because it fails silently later. Always use the.getmethod with a default or enableraise_error_on_missingin your Dynaconf initialization. Do it. I’ll wait.settings = Dynaconf( settings_files=['settings.toml'], environments=True, env_switcher="MYAPP_ENV", raise_error_on_missing=True # This is the way )
So, there you have it. Use layered configs, keep your secrets separate, and for the love of all that is holy, enable raise_error_on_missing. It will save you from a night of debugging why your “None” string was trying to connect to a database.