Last Updated: June 01, 2026
- Introduction to Python 3 Configuration Files
- Setup of Python 3 ConfigParser
- File Format of configuration file
- Reading the configuration file from python code
- Read config from the config file using ConfigParser
- Changing the datatype of the configuration value from ConfigParser
- What to do if a value is not available from a configfile
- Conclusion
- Full Code: ConfigParser Example Code
- Reference
- Want to see more useful tips?
- Frequently Asked Questions
- Related Articles
Intermediate
Putting parameters in configuration files can take some extra effort at the start, but then can save you a lot of time and heartache in the future. We are all tempted to simply hardcode parameters directly into our code as we save precious time when we write code, but then doing this properly can take extra effort. Some of us at least create constants or store parameters in a variable, while others store them in a class variable to keep this even cleaner. Arguably the best option is store these in a configuration file. In this article you’ll learn the steps compulsory to use configuration files in python 3. It will be strictly according to the official documentation of python 3.
ConfigParser is the class used to implement configuration files in python 3. The main function of using these files is to write python programs which can easily be modified by end users easily. The main aspect of this article is to know about the complete implementation of configuration files. We will cover the three main aspects in this article which are Setup, File format and Basic API.
Python developer and educator with 15+ years building production systems across data engineering, web APIs, and AI tooling. Founder of Python How To Program — 270+ in-depth tutorials covering the modern Python stack.
Introduction to Python 3 Configuration Files
Configuration files can play a vital role in any program and its management. One of the popular approaches to separate code from configuration is to store these files in YAML, JSON or INI and not in .py format. One reason that .py files are not used is that Python 3 can be slower when it comes to reloading. You would need to restart the whole program if you stored your config in a python .py file. Also, the end user can modify the code at will if it is in .py format. Configuration files make it easier to modify or change the code. The data stored in configuration is to have separation so that the programmer can focus on code development and ensure that is clean as possible and the user only needs to touch the configuration file.
Setup of Python 3 ConfigParser
The class used to create configuration files is ConfigParser. This is a part of the standard python 3 library so no need to do any pip installation. We have to import it: “import configparser” to use it or there is another way of using it, it will work in both python2 and python 3, which is:
import configparser
File Format of configuration file
One convention that is used for the file format is to use the extension .ini (short for initial or initiation) but you can use the configuration based on your own or on clients preferences. There are different parts of configuration files.
- A configuration file consists of one or more sections.
- The section names are written in these delimiters [section name].
- The concept is similar to mapping. It consists of key-value pairs meaning there is a name of the configuration item (“key”) and the other the actual value of the configuration (“value”)
- Two operators are used to initialize or separate key-value pair assignment operator (=) or colon operator (:).
- You can even put in a comment using the # or ; prefix.
Example:
[default]
host = 192.168.1.1
port = 31
username = admin
password = admin
[database]
#database related configuration files
port = 22
forwardx11 = no
name = db_test
In the above configuration file example, we have two sections first is [default] and second is [database]. Each section has its own key-value pairs/entries like username = admin and name = db_test. So all of the key-value pairs belong to a given section, so it is easier to organise your configuration files. Finally the sentence with a prefix of # is for commenting
Reading the configuration file from python code
Now, we will talk about the method to read from the config file. As mentioned earlier, ConfigParser is the module/class used to create configuration files. First, ConfigParser object has to be initialized: config = configparser.ConfigParser(); The following are functions:
Initialization of ConfigParser
You can can initiate the configuration file with the following syntax. Here the variable “config” will contain all the values
config = configparser.ConfigParser()
Write to a Configuration file with ConfigParser
Although normally you normally edit to a configuration file in a text editor by hand, there are times where you want to programmatically write to a config file. For example, this could be to create a default config file which a user can then use as a basis to change or edit. You may also want to over-ride a config entry (after confirming with the user) that is erroneous.
Once the object is initialised, we can now write in it. There are ways through which we can initialize the section to write in the config file. We are going use the example mentioned above in file format. Let’s initialize the default section using dictionary.
Example:
config['default'] = {
"host" : "192.168.1.1",
"port" : "22",
"username" : "username",
"password" : "password"
}
Here, “default” is the name of the section (the part in the actual configuration file that had the square “[” and “]” brackets) and curly braces denote the start and end of a dictionary. Inside the dictionary are key-value pairs i.e. “host” is the key and “192.168.1.1” is the value separated by colon “:”
Now, let’s initialize the database section using empty dictionary and add the key-value pairs line by line.
Example:
config['database'] = {}
config['database']['port'] = "22"
config['database']['forwardx11'] = "no"
config['database']['name'] = "db_test"
Here, “database” is the name of the section and curly braces denote the same start and end of a dictionary. In this case, the dictionary is empty. Key-value pairs i.e. “port” is the key and “22” is the value separated by colon “=.” This method provides a lot more flexibility.
Here’s the full code so far:
import configparser
config = configparser.ConfigParser()
config['default'] = {
"host" : "192.168.1.1",
"port" : "22",
"username" : "username",
"password" : "password"
}
config['database'] = {}
config['database']['port'] = "22"
config['database']['forwardx11'] = "no"
config['database']['name'] = "db_test"
with open('test.ini', 'w') as configfile:
config.write(configfile);
After initializing the sections in config, you can now write it to a config file:
with open('test.ini', 'w') as configfile:
config.write(configfile);
Now, you will be able to see the file named test.ini created.
Read config from the config file using ConfigParser
The next step is to read the file which you just have created.
- The config file can be read by using read() method: config.read(‘test.ini’). This will read the test.ini file which you just created.
- If you want to print just the sections available in configuration file, method sections() can be used: config.sections().
- Next is getting the value of any key stored in the section. config[‘database’][‘name’]
This will give you the value which is “db_test” of the key called “name” stored in data_base section.
The following code will print out all the values stored against the keys in the default section using a for loop.
for key in config['default']:
print(config['default'][key])
Code:

Output:

Changing the datatype of the configuration value from ConfigParser
The datatype of the object of ConfigParser is string by default. This is fine for most situations, but then suppose you want to get a true/false value instead, or a number value to do maths operations. For this the string default may not work. We can typecast/covert the datatype of the object of configparser or the datatype of keys of section into any other type such as integer, float etc. In order to change the datatype of object, you have to covert it manually or by using getter methods. The best and the preferred way is to use getter methods.
There are three getter methods:
- getint();
- getfloat();
- getboolean();
Example: config['default'].getint('port')
getint() will covert the datatype of port key of section “default” into “integer”. If you use the typeof(); method on port then it will show integer type now.
There is another way of doing it:
Example: config.getboolean('data_base', 'forwardx11')
In this way, config file is invoking the getboolean() method and its takin two parameters as argument. The first is the name of the section and the other is the key whole value’s type will be changed.
What to do if a value is not available from a configfile
A fallback result can also be obtained. Fallback is the result obtained when the key or section we want to get isn’t available.
Example: config.get('default', 'database', fallback='not_database')
In this case, not_database will be returned if the “database” key isn’t available or the section default is not found.
Conclusion
We come to know about the setup i.e. importing the ConfigParser first to create configuration files. Next section was about the file format. There you can check about the basic syntax of creating a configuration file. It consists of sections and key-value pairs.
We played with the data types of keys in default and data_base sections. We can change datatypes using getter methods. Last but not the least, we studied about the basic api like write, read and about fallback.
Using configuration files is not difficult and can save a lot of time. So in your next coding work, take the extra few minutes to create a configuration file instead of hardcoding.
Full Code: ConfigParser Example Code
import configparser
config = configparser.ConfigParser()
#Set up default item for hosts using dictionary
config['default'] = {"host" : "192.168.1.1",
"port" : "22",
"username" : "username",
"password" : "password" }
#setup config item bytes
config['database'] = {}
config['database']['port'] = "22"
config['database']['forwardx11'] = "no"
config['database']['name'] = "db_test"
#Write default file
with open('test.ini', 'w') as configfile:
config.write(configfile)
#Open the file again to try to read it
config.read('test.ini')
#Print the sections
print(config.sections())
print( config['database']['name'] )
#Print each key pair
for key in config['default']:
print(config['default'][key])
#print the type of integer value
print (type (config['default'].getint('port')))
print( config.getboolean('database', 'forwardx11') )
#Print default value
print( config.get('default', 'databaseabc', fallback='not_database') )
Output:

Reference
https://docs.python.org/3/library/configparser.html
Want to see more useful tips?
How To Use Python Decouple for Environment Variable Config
Intermediate
You write a FastAPI app that connects to a database and a third-party payment gateway. You hardcode the credentials during development and tell yourself you will fix it before deploying. A month later the repo is on GitHub and so is your production database password. This is not a hypothetical — it happens constantly, and it happens because os.environ.get() is clunky enough that developers avoid it until it is too late.
Python’s python-decouple library makes the right approach easier than the wrong one. It reads configuration from .env files or .ini files, applies type casting automatically, handles missing values with sensible defaults, and keeps your code clean. One pip install python-decouple is all it takes, and it works with any Python project — Flask, FastAPI, Django, or a plain script.
This article walks through everything you need to use python-decouple confidently: reading values, type casting, setting defaults, working with booleans, using .env vs .ini files, integrating with Django settings, and building a real-world config loader. By the end you will have a pattern you can drop into any project to keep secrets out of your source code for good.
Python Decouple: Quick Example
Here is a minimal working example that shows the core pattern — reading a string, an integer, and a boolean from a .env file — so you can see exactly what decouple does before we explore each feature in depth.
First, create a file named .env in your project root:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
PORT=8000
DEBUG=True
SECRET_KEY=my-local-dev-secret-key-change-in-prod
Now read those values in Python:
# quick_decouple.py
from decouple import config
# String (default type)
database_url = config('DATABASE_URL')
# Integer -- decouple casts automatically
port = config('PORT', cast=int)
# Boolean -- handles 'True', 'true', '1', 'yes', etc.
debug = config('DEBUG', cast=bool)
# String with a fallback default
secret_key = config('SECRET_KEY', default='fallback-dev-key')
print(f"DB: {database_url}")
print(f"Port: {port} (type: {type(port).__name__})")
print(f"Debug: {debug} (type: {type(debug).__name__})")
print(f"Key: {secret_key[:10]}...")
Output:
DB: postgresql://localhost:5432/myapp
Port: 8000 (type: int)
Debug: True (type: bool)
Key: my-local-d...
Notice that port comes back as a real Python int and debug as a real bool — not strings. With os.environ you would need to write int(os.environ['PORT']) and handle the conversion yourself every time. Decouple does that work once, at the point of reading, so the rest of your code receives properly typed values.
Read on to see how decouple handles missing values, search paths, .ini files, and real-world project layouts.
What Is Python Decouple and Why Use It?
Python decouple is a library that implements the Twelve-Factor App principle of strict separation between configuration and code. Configuration here means anything that is likely to vary between deployment environments: database URLs, API keys, feature flags, port numbers, and debug settings. The idea is that these values live in the environment (a .env file locally, environment variables in production), not in the source code that gets committed to a repository.
Think of it like a restaurant kitchen. The recipes (your code) are written down and shared. The ingredients (your config values) change depending on what the supplier has that day — and the head chef does not write the supplier’s phone number into every recipe card. They keep it in a separate contact file. Decouple is that contact file system for your Python app.
Decouple vs os.environ
Here is how python-decouple compares to using os.environ directly:
| Feature | os.environ | python-decouple |
|---|---|---|
| Read string value | os.environ['KEY'] — raises KeyError if missing | config('KEY') — raises UndefinedValueError with clear message |
| Default value | os.environ.get('KEY', 'default') | config('KEY', default='default') |
| Integer casting | int(os.environ.get('PORT', '8000')) | config('PORT', default=8000, cast=int) |
| Boolean casting | Manual: 'True' == os.environ.get('DEBUG') | config('DEBUG', cast=bool) handles True/true/1/yes |
| Read from .env file | Requires python-dotenv or manual parsing | Built in — searches parent directories automatically |
| Support .ini files | No | Yes — useful for projects with existing .ini configs |
| Test overrides | Must monkeypatch os.environ | Can pass values directly in code during tests |
The bottom line: os.environ is built-in and requires no extra dependency, but every type conversion is manual boilerplate. Decouple pays for itself the moment you have more than two or three config values that need casting.
Installing python-decouple
Install it with pip in your virtual environment:
# install_decouple.py (run this in your terminal, not as a script)
pip install python-decouple
Output:
Successfully installed python-decouple-3.8
There is one important naming note: the library is called python-decouple on PyPI (what you install), but the import name is decouple (what you use in code). Do not confuse it with decouple on PyPI — that is a different package for Django-specific use. Always install python-decouple.
The .env File: Format and Best Practices
A .env file is a plain text file with one KEY=value pair per line. Decouple searches for it starting in the directory of the script being run, then walks up to parent directories. This means you can place it at the root of your project and it will be found regardless of which subdirectory you run from.
# .env (place this in your project root)
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/myapp_dev
# Server
PORT=8000
HOST=0.0.0.0
# Feature flags
DEBUG=True
ENABLE_CACHING=False
# Third-party APIs
STRIPE_SECRET_KEY=sk_test_abc123
SENDGRID_API_KEY=SG.xyz789
# Email
EMAIL_BACKEND=console
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
There are a few formatting rules to know. Values do not need quotes — DEBUG=True works fine. If your value contains spaces or special characters, wrap it in single or double quotes: FULL_NAME='Ada Lovelace'. Lines starting with # are comments and are ignored. Empty lines are also ignored.
The most important rule: add .env to your .gitignore immediately. Create a .env.example file with the same keys but dummy values, and commit that instead. New developers clone the repo, copy .env.example to .env, fill in their local values, and they are ready to go.
Type Casting with cast=
Every value in a .env file is stored as a string. Decouple’s cast parameter converts the string to the type you need before returning it, so the rest of your code never sees a string where it expects an integer or boolean.
Integers and Floats
Pass cast=int or cast=float to convert numeric config values. This is far cleaner than wrapping every read in a manual conversion.
# cast_examples.py
from decouple import config
# These values exist in .env:
# PORT=8000
# WORKERS=4
# TIMEOUT=30.5
port = config('PORT', default=8000, cast=int)
workers = config('WORKERS', default=2, cast=int)
timeout = config('TIMEOUT', default=30.0, cast=float)
print(f"Port: {port} -- {type(port).__name__}")
print(f"Workers: {workers} -- {type(workers).__name__}")
print(f"Timeout: {timeout} -- {type(timeout).__name__}")
Output:
Port: 8000 -- int
Workers: 4 -- int
Timeout: 30.5 -- float
If the .env value cannot be cast to the requested type — for example, PORT=eight_thousand — decouple raises a ValueError with a clear message pointing to the offending key. You get the error at startup when reading config, not somewhere deep in your app when the value is used.
Booleans
Boolean config values are tricky with os.environ because every string is truthy. "False" evaluates to True in Python because it is a non-empty string. Decouple’s boolean cast handles this correctly by recognizing a set of canonical true and false values.
# cast_bool.py
from decouple import config
# .env contains:
# DEBUG=True
# ENABLE_CACHING=False
# USE_SSL=yes
# MAINTENANCE_MODE=0
debug = config('DEBUG', cast=bool)
caching = config('ENABLE_CACHING', cast=bool)
ssl = config('USE_SSL', cast=bool)
maintenance = config('MAINTENANCE_MODE', cast=bool)
print(f"DEBUG: {debug}")
print(f"ENABLE_CACHING: {caching}")
print(f"USE_SSL: {ssl}")
print(f"MAINTENANCE_MODE: {maintenance}")
Output:
DEBUG: True
ENABLE_CACHING: False
USE_SSL: True
MAINTENANCE_MODE: False
The recognized truthy values are True, true, 1, yes, on. The recognized falsy values are False, false, 0, no, off. Anything else raises a ValueError. This strict set prevents the bug where DEBUG=False still evaluates to True because you forgot to cast.
Comma-Separated Lists
Decouple does not have a built-in list type, but you can pass any callable as the cast argument — including a lambda that splits a string into a list.
# cast_list.py
from decouple import config, Csv
# .env contains:
# ALLOWED_HOSTS=localhost,127.0.0.1,myapp.com
# CORS_ORIGINS=http://localhost:3000,https://app.example.com
# Option 1: built-in Csv helper (strips whitespace, handles quoting)
allowed_hosts = config('ALLOWED_HOSTS', cast=Csv())
# Option 2: lambda for simple cases
cors_origins = config('CORS_ORIGINS', default='', cast=lambda v: [s.strip() for s in v.split(',')])
print(f"Allowed hosts: {allowed_hosts}")
print(f"CORS origins: {cors_origins}")
Output:
Allowed hosts: ['localhost', '127.0.0.1', 'myapp.com']
CORS origins: ['http://localhost:3000', 'https://app.example.com']
The Csv() helper from decouple is the cleaner option for comma-separated values. It handles edge cases like extra whitespace and quoted values with commas inside them. The lambda approach works fine for simple cases where you control the format.
Defaults and Missing Values
When a key is missing from both the .env file and the actual environment, decouple’s behavior depends on whether you provided a default.
# defaults_demo.py
from decouple import config, UndefinedValueError
# KEY_WITH_DEFAULT is not in .env -- returns the default
log_level = config('LOG_LEVEL', default='INFO')
print(f"Log level: {log_level}")
# KEY_WITH_NONE_DEFAULT is not in .env -- returns None
cache_url = config('CACHE_URL', default=None)
print(f"Cache URL: {cache_url}")
# KEY_REQUIRED is not in .env and has no default -- raises UndefinedValueError
try:
api_key = config('REQUIRED_API_KEY')
except UndefinedValueError as e:
print(f"Missing required config: {e}")
Output:
Log level: INFO
Cache URL: None
Missing required config: REQUIRED_API_KEY not found. Declare it as envvar or define a default value.
This behavior is intentional and useful. Required values — things your app absolutely cannot run without — should have no default. That way decouple raises a clear error at startup rather than letting the app start in a broken state and fail later with a cryptic message. Optional values should have a sensible default so the app can run in a minimal configuration without a full .env file in place.
.ini File Support
In addition to .env files, decouple can read from .ini files using the AutoConfig or explicit RepositoryIni approach. This is useful when your project already has a settings.ini or setup.cfg and you do not want to introduce a second config file.
# settings.ini
[settings]
DATABASE_URL=postgresql://localhost:5432/myapp
PORT=8000
DEBUG=True
# read_ini.py
from decouple import Config, RepositoryIni
# Explicitly read from a .ini file instead of .env
config = Config(RepositoryIni('settings.ini'))
database_url = config('DATABASE_URL')
port = config('PORT', cast=int)
debug = config('DEBUG', cast=bool)
print(f"DB: {database_url}")
print(f"Port: {port}")
print(f"Debug: {debug}")
Output:
DB: postgresql://localhost:5432/myapp
Port: 8000
Debug: True
The default config object (imported directly from decouple) uses AutoConfig, which searches for .env first, then .ini, then falls back to actual environment variables. You only need to use RepositoryIni explicitly when you want to force a specific file rather than letting decouple search.
Django Integration
Django’s settings.py is the most common place developers accidentally commit secrets. Decouple is designed to slot in cleanly as a drop-in replacement for hardcoded settings.
# settings.py (Django)
from decouple import config, Csv
# Core Django settings
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', cast=bool, default=False)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv(), default='localhost')
# Database -- dj-database-url makes this even cleaner
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME', default='myapp'),
'USER': config('DB_USER', default='postgres'),
'PASSWORD': config('DB_PASSWORD', default=''),
'HOST': config('DB_HOST', default='localhost'),
'PORT': config('DB_PORT', default=5432, cast=int),
}
}
# Email
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend')
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
EMAIL_USE_TLS = config('EMAIL_USE_TLS', cast=bool, default=False)
# Stripe
STRIPE_PUBLIC_KEY = config('STRIPE_PUBLIC_KEY', default='')
STRIPE_SECRET_KEY = config('STRIPE_SECRET_KEY', default='')
The pattern is consistent throughout: use config('KEY') for required values that must exist in production, and config('KEY', default=...) for optional values with safe development defaults. The entire settings.py file becomes safe to commit because it contains no actual secrets — just the names of the keys and their defaults.
Real-Life Example: Environment-Aware FastAPI App
Here is a realistic FastAPI application config module that uses decouple to manage all its settings. This pattern — a dedicated config.py module that gathers all config into a dataclass — scales cleanly as the project grows.
# config.py
from dataclasses import dataclass
from decouple import config, Csv, UndefinedValueError
@dataclass
class AppConfig:
# Server
host: str
port: int
debug: bool
workers: int
# Database
database_url: str
# Security
secret_key: str
allowed_origins: list
# External APIs
stripe_secret_key: str
sendgrid_api_key: str
slack_webhook_url: str
# Feature flags
enable_caching: bool
enable_email: bool
def load_config() -> AppConfig:
"""Load and validate all application configuration at startup."""
return AppConfig(
# Server
host=config('HOST', default='0.0.0.0'),
port=config('PORT', default=8000, cast=int),
debug=config('DEBUG', default=False, cast=bool),
workers=config('WORKERS', default=1, cast=int),
# Database -- required in production, no default
database_url=config('DATABASE_URL'),
# Security -- required always
secret_key=config('SECRET_KEY'),
allowed_origins=config('ALLOWED_ORIGINS', cast=Csv(), default='http://localhost:3000'),
# External APIs -- optional with empty defaults (check before use)
stripe_secret_key=config('STRIPE_SECRET_KEY', default=''),
sendgrid_api_key=config('SENDGRID_API_KEY', default=''),
slack_webhook_url=config('SLACK_WEBHOOK_URL', default=''),
# Feature flags
enable_caching=config('ENABLE_CACHING', default=False, cast=bool),
enable_email=config('ENABLE_EMAIL', default=False, cast=bool),
)
# main.py
from fastapi import FastAPI
from config import load_config, AppConfig
cfg: AppConfig = load_config() # Fails fast at startup if required vars missing
app = FastAPI(debug=cfg.debug)
@app.get("/health")
def health():
return {
"status": "ok",
"debug": cfg.debug,
"caching": cfg.enable_caching,
"port": cfg.port,
}
if __name__ == "__main__":
import uvicorn
print(f"Starting on {cfg.host}:{cfg.port} (debug={cfg.debug})")
uvicorn.run(app, host=cfg.host, port=cfg.port, workers=cfg.workers)
Output (with example .env values):
Starting on 0.0.0.0:8000 (debug=False)
The key design choice here is that load_config() is called once at module level, so any missing required variable raises UndefinedValueError the moment the app starts — not on the first request five minutes into a production deploy. The dataclass gives you IDE autocomplete throughout the rest of the codebase and makes it obvious what configuration the app expects without opening the .env file.
Testing with Decouple
Config management and testability often conflict — tests need predictable values, but decouple reads from files. There are two clean approaches: use a test-specific .env file, or monkeypatch os.environ.
# test_config.py
import os
import pytest
from unittest.mock import patch
# Approach 1: patch os.environ before importing config
def test_debug_defaults_to_false():
with patch.dict(os.environ, {'DATABASE_URL': 'sqlite:///test.db', 'SECRET_KEY': 'test-key'}, clear=True):
# Reimport or reload config within the patch context
from decouple import config
# config() reads os.environ after checking .env
debug = config('DEBUG', default=False, cast=bool)
assert debug is False
def test_port_casting():
with patch.dict(os.environ, {'PORT': '9000'}):
from decouple import config
port = config('PORT', cast=int)
assert port == 9000
assert isinstance(port, int)
def test_missing_required_raises():
from decouple import config, UndefinedValueError
with patch.dict(os.environ, {}, clear=True):
# Remove DATABASE_URL from the environment
env_without_db = {k: v for k, v in os.environ.items() if k != 'DATABASE_URL'}
with patch.dict(os.environ, env_without_db, clear=True):
with pytest.raises(UndefinedValueError):
config('DATABASE_URL')
Output (pytest):
...
3 passed in 0.12s
The recommended approach for larger projects is to create a .env.test file and use a fixture that temporarily swaps the decouple search path to point at it. This gives you a full, realistic config setup for tests without polluting your development .env. The patch.dict(os.environ, ...)` approach shown above works well for unit tests of individual config values.
Frequently Asked Questions
Does decouple replace os.environ entirely?
Not entirely -- decouple reads from .env files first, then falls back to actual environment variables set in the shell or by the deployment platform. In production you typically do not deploy a .env file; instead, environment variables are set by the platform (Heroku config vars, Docker environment, Kubernetes secrets). Decouple reads those just fine through the os.environ fallback. The .env file is a development convenience, not a production requirement.
Where should I put my .env file?
Place it in the root of your project -- the same directory as manage.py (Django), main.py (FastAPI/Flask), or your top-level package. Decouple uses AutoConfig which walks up from the running script's directory until it finds a .env or .ini file, so as long as it is somewhere in the directory tree above your code, it will be found. Do not commit it to version control -- add it to .gitignore and commit a .env.example instead.
How do I handle multiple environments (dev, staging, prod)?
The cleanest approach is one .env per environment, kept out of the repo. Your CI/CD pipeline injects the appropriate values as environment variables for staging and production. Locally you maintain a .env with development values. You can also use a tool like direnv to switch .env files automatically when you change directories. Never create .env.production files and commit them -- that defeats the entire purpose.
What is the difference between python-decouple and python-dotenv?
Both libraries load .env files, but they take different approaches. python-dotenv loads values into os.environ as a side effect, making them available to os.environ.get() and any other code that reads the environment. python-decouple does not modify os.environ -- instead it provides a config() function that reads directly from the file or the real environment. Decouple is preferable when you want typed values, sensible defaults, and a clean API. Dotenv is useful when you need the values to land in os.environ for libraries that read from there directly.
Should I use .env files in production?
Generally no. Deploying a .env file to a server creates a file on disk containing secrets, which is a security risk if the server is ever compromised. In production, use your platform's secret management: Heroku config vars, AWS Secrets Manager, Docker secrets, Kubernetes secrets, or environment variables set in your deployment config. Decouple reads all of these through its os.environ fallback, so no code changes are needed between development (using .env) and production (using platform secrets).
How do I manage a large number of config values?
Group related config into separate config objects or dataclasses, as shown in the FastAPI example above. A single config.py module that defines everything in one place makes it easy to see what the app needs at a glance. Avoid calling config() scattered throughout your codebase -- centralizing config reads means you only have one place to look when a value is wrong, and one place to update when a key changes.
Conclusion
Python decouple solves a problem that trips up almost every developer at some point: configuration that leaks into source code. The library gives you config('KEY') with type casting, defaults, and clear errors for missing required values -- all reading from a .env file that stays out of your repository. We covered reading strings, integers, booleans, and lists; comparing decouple to raw os.environ; using .ini files; integrating with Django settings; and building a real FastAPI config module with a dataclass pattern.
The next step is to take the config module pattern from the real-life example and adapt it to your own project. Start by identifying every hardcoded string in your settings that could change between environments -- database URLs, API keys, debug flags, port numbers -- and move them behind config() calls. Your future self, and anyone else who has to deploy your app, will thank you.
Full documentation for python-decouple is at github.com/HBNetwork/python-decouple. The Twelve-Factor App methodology that inspired it is documented at 12factor.net/config. For a complementary approach using type-validated settings objects, see our guide on Pydantic Settings for Configuration Management. If you prefer a more flexible multi-environment solution, dynaconf is worth exploring. To keep your secrets extra safe, pair decouple with the built-in Python secrets module for generating tokens and keys.
Related Articles
Frequently Asked Questions
What is ConfigParser used for in Python?
ConfigParser is a built-in Python module for reading and writing configuration files in INI format. It handles settings organized into sections with key-value pairs, making it easy to store and retrieve application configuration without hardcoding values.
What format does ConfigParser use?
ConfigParser uses the INI file format with sections in square brackets ([section]), followed by key-value pairs using = or : as delimiters. Comments start with # or ;. There is always a [DEFAULT] section for fallback values.
How do I read a config file with ConfigParser?
Create a ConfigParser() instance, call config.read('filename.ini'), then access values with config['section']['key'] or config.get('section', 'key'). Use getint(), getfloat(), or getboolean() for type conversion.
Can ConfigParser handle nested sections?
No, ConfigParser does not support nested sections natively. For nested configuration structures, consider using TOML (tomllib in Python 3.11+), YAML (PyYAML), or JSON configuration files instead.
What is the difference between ConfigParser and JSON for configuration?
ConfigParser uses human-friendly INI format with sections and is ideal for simple settings. JSON supports nested structures and lists but lacks comments. ConfigParser has built-in type conversion methods and a DEFAULT section for fallback values, while JSON requires manual type handling.
Related Articles
- How To Use TOML for Configuration Files
- Managing Environment Variables with dotenv
- How To Use Pathlib for File Paths
Continue Learning Python
Tutorials you might also find useful:
- How To Use Python TOML For Configuration Files Instead of INI
- How To Use Python dynaconf for Configuration Management
- How To Work with ZIP Files in Python
- How To Use Python tempfile for Temporary Files and Directories
- How To Read and Write CSV Files in Python
- How To Read and Write JSON Files in Python
Trackbacks/Pingbacks