python-decouple is a small configuration helper for Python projects. It lets your code ask for named settings while the actual values come from the process environment, a local .env file, or another configured source. That keeps deployment details out of application logic and makes local development, staging, and production easier to separate.
The package is most useful for values that differ by machine: debug flags, service URLs, database connection strings, cache hosts, allowed host lists, and feature toggles. It is not a secret manager by itself. Treat it as a reader for settings, keep sensitive values out of source control, and avoid printing loaded secrets in logs or examples.
The primary references are the python-decouple PyPI page, the python-decouple project repository, Python’s os.environ documentation, and the pathlib documentation. Related PythonPool guides cover checking the Python version, finding a virtual environment, requirements files, and Requests JSON API work.
Install it in the same interpreter that runs your app with python -m pip install python-decouple, then add python-decouple to your project dependency file. The examples below include guarded fallbacks so they run as local snippets even when the package is not installed.
Read A Setting With A Default
Start with the common pattern: import config, read a named value, and provide a safe default for optional settings. The code below sets demo values first so it can run without touching your real shell setup.
import os
try:
from decouple import config
except ImportError:
def config(name, default=None, cast=None):
value = os.environ.get(name, default)
if cast is bool:
return str(value).strip().lower() in {"1", "true", "yes", "on"}
if cast and value is not None:
return cast(value)
return value
os.environ["DEMO_SECRET_KEY"] = "local-demo-key"
os.environ["DEMO_DEBUG"] = "false"
debug = config("DEMO_DEBUG", default=False, cast=bool)
secret_key = config("DEMO_SECRET_KEY")
print(debug)
print(f"secret loaded: {bool(secret_key)}")
The example confirms that a value exists without printing the value itself. That habit matters in real applications because settings often include tokens, passwords, and signing keys.
Use defaults only for settings where a fallback is genuinely safe. A debug flag can default to False. A database URL usually should not default to a fake production connection because that hides deployment mistakes.
Load Values From A .env File
For local development, a .env file keeps settings near the project while staying outside committed source. The package can read a specific file through Config and RepositoryEnv, which is useful for tests and command-line tools.
import os
from pathlib import Path
from tempfile import TemporaryDirectory
def read_env_file(path):
values = {}
for line in path.read_text(encoding="utf8").splitlines():
if "=" in line and not line.lstrip().startswith("#"):
name, value = line.split("=", 1)
values[name.strip()] = value.strip()
return values
api_name = "PYTHONPOOL_DECOUPLE_API_URL"
retry_name = "PYTHONPOOL_DECOUPLE_RETRIES"
os.environ.pop(api_name, None)
os.environ.pop(retry_name, None)
with TemporaryDirectory() as tmp:
env_path = Path(tmp) / ".env"
env_path.write_text(
f"{api_name}=https://api.example.test\n{retry_name}=3\n",
encoding="utf8",
)
try:
from decouple import Config, RepositoryEnv
read_config = Config(RepositoryEnv(str(env_path)))
api_url = read_config(api_name)
retries = read_config(retry_name, cast=int)
except ImportError:
file_values = read_env_file(env_path)
api_url = file_values[api_name]
retries = int(file_values[retry_name])
print(api_url)
print(retries + 1)
In a real project, keep .env out of version control and commit an example file with placeholder names instead. That gives teammates the required setting names without exposing local values.
A dedicated loader also makes tests cleaner. Tests can point at a temporary file, set process values, and assert how the app behaves without editing a developer’s real shell profile.
Cast Strings Into Python Types
Settings arrive as text. python-decouple supports casts so callers can receive booleans, integers, floats, lists, or custom converted values at the point where the setting is read.
import os
os.environ["DEMO_ALLOWED_HOSTS"] = "localhost,api.pythonpool.com"
os.environ["DEMO_PORT"] = "8000"
os.environ["DEMO_DEBUG"] = "yes"
try:
from decouple import Csv, config
hosts = config("DEMO_ALLOWED_HOSTS", cast=Csv())
port = config("DEMO_PORT", cast=int)
debug = config("DEMO_DEBUG", cast=bool)
except ImportError:
hosts = [
item.strip()
for item in os.environ["DEMO_ALLOWED_HOSTS"].split(",")
if item.strip()
]
port = int(os.environ["DEMO_PORT"])
debug = os.environ["DEMO_DEBUG"].lower() in {"1", "true", "yes", "on"}
print(hosts)
print(port + 1)
print(debug)
Casting near the read keeps the rest of the application honest. Instead of passing the string "8000" through several layers, the app receives an integer immediately. Instead of treating any non-empty debug text as truthy, the app receives a boolean.
For list-like settings, keep the separator simple and document it. Comma-separated host names are easy to read in a shell or .env file, but deeply nested configuration is usually better as a structured file format.
Fail Fast For Required Settings
Required settings should fail at startup, not halfway through a request. python-decouple raises an error when a requested name is missing and no default is supplied. You can wrap that behavior in one settings loader.
import os
from dataclasses import dataclass
required_name = "PYTHONPOOL_DECOUPLE_REQUIRED_URL"
os.environ.pop(required_name, None)
try:
from decouple import UndefinedValueError, config
except ImportError:
class UndefinedValueError(Exception):
pass
missing = object()
def config(name, default=missing, cast=None):
value = os.environ.get(name, missing)
if value is missing:
if default is missing:
raise UndefinedValueError(f"{name} is not set")
value = default
if cast is bool:
return str(value).lower() in {"1", "true", "yes", "on"}
return cast(value) if cast else value
@dataclass(frozen=True)
class AppSettings:
service_url: str
debug: bool
def load_settings():
return AppSettings(
service_url=config(required_name),
debug=config("PYTHONPOOL_DECOUPLE_DEBUG", default=False, cast=bool),
)
try:
load_settings()
except UndefinedValueError as exc:
print(type(exc).__name__)
print(str(exc).split()[0])
This gives operators a clear startup failure while keeping application code simple. The rest of the project can depend on a settings object instead of checking the same names repeatedly.
Do not add defaults just to silence errors. A missing required setting is information. It tells you that the deployment, service account, container, or local shell is not ready for the code that is about to run.
Let The Process Environment Override Files
A common deployment model uses a checked-in example file for names, a local .env file for development values, and process-level settings in production. The process environment should win when both sources define the same name.
import os
from pathlib import Path
from tempfile import TemporaryDirectory
def read_env_file(path):
result = {}
for line in path.read_text(encoding="utf8").splitlines():
if "=" in line:
key, value = line.split("=", 1)
result[key.strip()] = value.strip()
return result
setting_name = "PYTHONPOOL_DECOUPLE_SERVICE_NAME"
os.environ[setting_name] = "from-process"
with TemporaryDirectory() as tmp:
env_path = Path(tmp) / ".env"
env_path.write_text(f"{setting_name}=from-file\n", encoding="utf8")
try:
from decouple import Config, RepositoryEnv
settings = Config(RepositoryEnv(str(env_path)))
selected = settings(setting_name)
except ImportError:
file_values = read_env_file(env_path)
selected = os.environ.get(setting_name, file_values.get(setting_name))
print(selected)
This precedence is helpful for containers, hosted platforms, and CI jobs. They can inject production values without rewriting files inside the app image. Local developers can still use a private .env file for day-to-day work.
When debugging, print whether a setting came from the process environment or a file, but do not print sensitive values. Source labels are usually enough to explain mismatches.
Build A Typed Settings Object
Small scripts can call config() directly. Larger applications are easier to maintain when settings are loaded once into a typed object and then passed to the modules that need them.
import os
from dataclasses import dataclass
from pathlib import Path
os.environ["DEMO_BASE_DIR"] = "/srv/pythonpool"
os.environ["DEMO_ALLOWED_HOSTS"] = "www.pythonpool.com,api.pythonpool.com"
os.environ["DEMO_CACHE_TTL"] = "300"
try:
from decouple import Csv, config
except ImportError:
class Csv:
def __call__(self, value):
return [item.strip() for item in value.split(",") if item.strip()]
def config(name, default=None, cast=None):
value = os.environ.get(name, default)
return cast(value) if cast else value
@dataclass(frozen=True)
class WebSettings:
base_dir: Path
allowed_hosts: tuple[str, ...]
cache_ttl: int
settings = WebSettings(
base_dir=Path(config("DEMO_BASE_DIR")),
allowed_hosts=tuple(config("DEMO_ALLOWED_HOSTS", cast=Csv())),
cache_ttl=config("DEMO_CACHE_TTL", cast=int),
)
print(settings.allowed_hosts)
print(settings.base_dir / "logs")
print(settings.cache_ttl > 0)
This pattern gives one clear place for names, casts, defaults, and required settings. It also makes tests easier because a test can construct WebSettings directly instead of patching process state for every function.
The practical workflow is straightforward: install python-decouple in the active environment, keep local values out of source control, cast settings as soon as they are read, fail fast when required names are missing, and centralize application settings in a small loader. That keeps configuration explicit without scattering deployment details across the codebase.