python-decouple: Typed Settings and Environment Variables

Quick Answer

Install python-decouple and import config from decouple. Read a value with config('KEY', default=..., cast=...). Environment variables take precedence over an ini or .env file and defaults, while values should be cast explicitly because environment data starts as strings.

python-decouple configuration precedence from environment files and defaults to typed settings
python-decouple reads environment variables first, then .env or ini values, then defaults, and can cast strings to types.

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.

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. If the dotenv import still fails during setup, Fix ModuleNotFoundError: No Module Named dotenv separates the python-dotenv distribution name from the active interpreter and local file naming conflicts.

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.

Python Pool infographic showing python decouple, a settings file, environment, and lookup
Settings source: Python decouple, a settings file, environment, and lookup.

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.

Python Pool infographic comparing config strings, bool, int, csv, and typed application settings
Typed values: Config strings, bool, int, csv, and typed application settings.

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.

Python Pool infographic mapping a missing environment value to a default and a required setting
Defaults: A missing environment value to a default and a required setting.

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.

Python Pool infographic testing .env handling, production secrets, precedence, and validation
Secret checks: .env handling, production secrets, precedence, and validation.

Read Typed Settings With Defaults

Configuration values arrive as text, so a string such as "False" is truthy in ordinary Python. Use the cast argument when the application expects a boolean, integer, list, or another type.

from decouple import config

DEBUG = config("DEBUG", default=False, cast=bool)
PORT = config("PORT", default=8000, cast=int)
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost")

Give required secrets no unsafe default so a missing value fails early. Use a non-secret example file for onboarding and keep real credentials outside version control.

Understand Configuration Precedence

python-decouple checks the process environment before the repository configuration file, then falls back to the default passed to config(). This lets deployment configuration override a shared file without editing code.

from decouple import Config, RepositoryEnv

settings = Config(RepositoryEnv(".env"))
api_url = settings("API_URL")
retries = settings("RETRIES", default=3, cast=int)

Keep the configuration boundary near application startup, validate required values there, and avoid printing secrets while debugging. The package’s own documentation is the authority for version-specific behavior.

Frequently Asked Questions

What is python-decouple used for?

It separates settings from Python code by reading values from environment variables, ini or .env files, and defaults.

How do I cast a python-decouple setting to a boolean?

Call config(‘DEBUG’, default=False, cast=bool). Casting is important because environment variables are read as strings.

Which value wins in python-decouple?

The process environment takes precedence over the repository configuration file, which takes precedence over the default passed to config().

How should I store secrets with python-decouple?

Keep real secrets in deployment environment variables or an untracked configuration file, commit only a safe example, and validate required values at startup.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted