Appearance
uncouple
Typed, structured configuration from environment variables. A thin layer over python-decouple and pydantic: declare a class with type annotations, call load(), and get a validated, fully-typed config object — with nesting, prefixes, custom field types, and a guard rail for insecure development defaults.
lib/ is on the Python path, so import it as a top-level package:
python
from uncouple import (
Config,
InsecureDefault,
InsecureConfigError,
InsecureDefaultWarning,
StringList,
Addr,
YarlUrl,
ToYarlUrl,
ReadYaml,
ConfigTable,
)Concepts
| Concept | What it is |
|---|---|
Config | A pydantic BaseModel subclass whose annotations map to env vars. |
Config.load(prefix=…) | Resolve every field from the environment, validate, and instantiate. |
InsecureDefault(value) | A dev-only fallback that warns when used — and can be made fatal. |
| Field types | StringList, Addr, YarlUrl, ReadYaml, ConfigTable, … |
Defining configuration
Subclass Config and annotate fields. load() reads each field from the environment (via decouple, so .env files work), applies the annotated type's parsing/validation, and returns an instance:
python
from uncouple import Config, InsecureDefault, StringList
class DjangoConfig(Config):
DEBUG: bool = False
SECRET_KEY: str = InsecureDefault("insecure-development-key")
ALLOWED_HOSTS: StringList = ["*"]
DATABASE_URL: str = "sqlite:///db.sqlite3"
config = DjangoConfig.load(prefix="DJANGO")
config.DEBUG # bool, cast from e.g. DJANGO_DEBUG=true
config.ALLOWED_HOSTS # ["api.example.com", "www.example.com"] from a CSV valueHow a field resolves
For each field, load() picks the first value that exists, in order:
- An environment variable /
.enventry named<PREFIX>_<FIELD>(e.g.DJANGO_SECRET_KEY). - An explicit keyword passed to
load(**defaults). - The default declared on the class.
- If none of the above, the field is omitted and pydantic applies its own default (or raises if the field is required).
Because resolution flows through pydantic, any standard pydantic machinery — field_validator, computed defaults, strict types — works as usual:
python
from pydantic import field_validator
class DjangoConfig(Config):
URL_PREFIX: str = "-"
@field_validator("URL_PREFIX")
@staticmethod
def strip_slashes(val: str) -> str:
return val.strip("/")Prefixes and nesting
A prefix namespaces every variable, letting several config objects share one environment without collisions:
python
django_config = DjangoConfig.load(prefix="DJANGO") # DJANGO_*
email_config = EmailConfig.load(prefix="EMAIL") # EMAIL_*
celery_config = CeleryConfig.load(prefix="CELERY") # CELERY_*Nesting composes prefixes automatically. A nested Config field reads from <PARENT_PREFIX>_<FIELD>_<NESTED_FIELD>:
python
class Database(Config):
HOST: str = "localhost"
PORT: int = 5432
class AppConfig(Config):
database: Database = Database()
cfg = AppConfig.load(prefix="APP")
# database.HOST ← APP_database_HOST
# database.PORT ← APP_database_PORTInsecure defaults
InsecureDefault marks a fallback that is fine for local development but must never silently reach production — secret keys, signing salts, demo credentials:
python
class DjangoConfig(Config):
SECRET_KEY: str = InsecureDefault("insecure-development-key")Behaviour depends on whether the environment supplies a real value:
- Env var set → the real value is used, no warning.
- Env var unset → the fallback is used and an
InsecureDefaultWarningis emitted naming the key. - Env var unset and
UNCOUPLE_REQUIRE_SECUREis truthy → loading raisesInsecureConfigErrorlisting every offending key, forcing the value to be provided explicitly.
Set UNCOUPLE_REQUIRE_SECURE=1 in production (or CI) to turn "I forgot to set a secret" from a quiet warning into a hard failure at startup.
Field types
These annotated types add parsing on top of plain pydantic fields:
| Type | Accepts | Produces |
|---|---|---|
StringList | "a,b,c" or ["a", "b"] | list[str] (["a", "b", "c"]) |
Addr | "host:port" | AddrStruct(host, port) |
YarlUrl | a URL string | a yarl.URL |
ToYarlUrl | validator to add to your own type | converts a parsed URL to yarl.URL |
ReadYaml[T] | a path to a YAML file | LoadedData[T] (parsed + source path) |
ConfigTable(Cls, prefix=…) | a dict of named sub-configs | dict[str, Cls] |
python
from uncouple import Config, Addr, StringList, YarlUrl
class ServiceConfig(Config):
LISTEN: Addr = "0.0.0.0:8000" # → AddrStruct(host="0.0.0.0", port=8000)
HOSTS: StringList = ["localhost"] # CSV-friendly list
UPSTREAM: YarlUrl = "https://api.example.com" # → yarl.URLConfigTable builds a mapping of repeated config blocks, prefixing each sub-config with its key — useful for things like per-tenant or per-queue settings:
python
from uncouple import Config, ConfigTable
class Queue(Config):
URL: str
CONCURRENCY: int = 1
class AppConfig(Config):
queues: ConfigTable(Queue, prefix="QUEUE") = {}Inspecting the environment
Config.find() iterates every available config key, combining os.environ with decouple's loaded repository (.env file). Handy for debugging which values are visible at load time.
Usage in this template
conf/settings.py is the canonical example: it declares DjangoConfig, EmailConfig, CeleryConfig and ProjectConfig, loads each with its own prefix, and uses InsecureDefault for SECRET_KEY. See tests/test_uncouple.py for the InsecureDefault behaviour exercised in isolation.