Get started / Python
docuconf for Python
Your BaseSettings class stays as it is: descriptions from Field(description=...), bounds from ge/le, secrets from SecretStr. docuconf adds URL schemes, CSV lists and file inputs, and checks everything at boot.
- Builds on
- pydantic-settings 2
- Package
- docuconf-pydantic (PyPI)
- Requires
- Python 3.10
- Status
- v0.1 alpha, not yet released · conformance 134 of 134 (json-schema with the jsonschema extra)
- Source
- docuconf-python · the orders example
Not published yet
main branch. After the first release it will be pip install docuconf-pydantic.The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.
1.Install
The distribution is not on PyPI yet. Install it from the main branch (Python 3.10 or later); the import package is docuconf.
pip install "docuconf-pydantic @ git+https://github.com/Docuconf/docuconf-python@main"2.Declare your configuration
A pydantic-settings class, on docuconf's DocuconfSettings, which is BaseSettings with docuconf's checks in its constructor. Field(description=...), ge, le and min_length become the contract, and an attribute docstring becomes the details; docuconf adds Url(schemes=...) and Csv(), and docuconf_service names the contract.
class Settings(DocuconfSettings):
# metadata.name in the exported contract.
docuconf_service: ClassVar[str] = "orders"
port: int = Field(8080, ge=1, le=65535, description="HTTP listen port")
log_level: Literal["debug", "info", "warn", "error"] = Field("info", description="Minimum log level")
# SecretStr makes it a secret in the contract, and keeps it out of reprs and error messages.
database_url: Annotated[SecretStr, Url(schemes=("postgres",))] = Field(
max_length=2048, description="Postgres connection string for orders"
)
# NoDecode + Csv: read "a,b" rather than pydantic-settings' default JSON list.
allowed_origins: Annotated[list[str], NoDecode, Csv()] = Field(
["http://localhost:3000"], min_length=1, description="CORS origins allowed to call the API"
)
# pydantic reads durations as ISO 8601 (PT45S); the contract says so, and the platform converts "45s".
request_timeout: timedelta = Field(
timedelta(seconds=30),
ge=timedelta(seconds=1),
le=timedelta(minutes=5),
description="Timeout for a request to finish",
)
"""How long a request may take before the server gives up on it.
Raise it when clients upload large order batches. Keep it below the load balancer's idle timeout, or the
client sees a reset rather than a ``504``.
The platform writes Go durations such as ``45s``; docuconf converts them to ISO 8601 for pydantic.
"""
worker_count: int = Field(4, ge=1, le=64, description="Workers processing orders")3.Load it at boot
Settings.load_or_exit() instantiates the class as pydantic-settings would and checks every rule. On a problem it prints them all and exits 1, without a traceback; Settings() and docuconf.load(Settings) raise ConfigValidationError instead.
def main() -> None:
# Reads the environment and checks every rule. On failure it prints every problem at once, writes the
# termination log and exits with status 1, without a traceback.
settings = Settings.load_or_exit()4.Run it, and see an error
Run the example with a valid environment:
DATABASE_URL=postgres://orders:pw@localhost:5432/orders .venv/bin/python app.pyWith PORT=0 and no DATABASE_URL, the service refuses to start, lists every problem with its error code and exits 1. The same text goes to /dev/termination-log.
$ PORT=0 .venv/bin/python app.py
docuconf: 2 configuration problems:
- DATABASE_URL [missing_required]: required, but not set
- PORT [out_of_range]: Input should be greater than or equal to 1 (got '0')5.Test your configuration
A pytest test. monkeypatch sets exactly the variables each test needs and restores the environment afterwards; termination_log=False keeps tests from writing one.
import pytest
import docuconf
from app import Settings
NAMES = ("PORT", "LOG_LEVEL", "DATABASE_URL", "ALLOWED_ORIGINS", "REQUEST_TIMEOUT", "WORKER_COUNT")
@pytest.fixture
def load(monkeypatch):
"""Load Settings from exactly the given variables; monkeypatch restores the environment."""
def load(**env):
for name in NAMES:
monkeypatch.delenv(name, raising=False)
for name, value in env.items():
monkeypatch.setenv(name, value)
return docuconf.load(Settings, watch=False, termination_log=False)
return load
def test_defaults(load):
settings = load(DATABASE_URL="postgres://orders@db/orders")
assert settings.port == 8080
assert settings.worker_count == 4
def test_rejects_bad_values(load):
with pytest.raises(docuconf.ConfigValidationError) as e:
load(PORT="70000")
codes = {(v.input, v.code) for v in e.value.violations}
assert ("PORT", "out_of_range") in codes
assert ("DATABASE_URL", "missing_required") in codes.venv/bin/pytest -q test_settings.py6.Export the contract
Export imports the class and writes the contract without reading the environment. --check fails when the committed file is out of date, for CI.
.venv/bin/docuconf export app:Settings -o contract.cue// Code generated by docuconf. DO NOT EDIT.
package orders
import "docuconf.dev/contract"
contract.#Contract & {
apiVersion: "docuconf.dev/v1alpha1"
kind: "ConfigContract"
metadata: {
name: "orders"
generator: {
language: "python"
sdk: "docuconf-pydantic"
version: "0.1.0"
}
}
vars: {
ALLOWED_ORIGINS: {
type: "list"
description: "CORS origins allowed to call the API"
items: "string"
encoding: "csv"
separator: ","
minItems: 1
default: ["http://localhost:3000"]
}
DATABASE_URL: {
type: "url"
description: "Postgres connection string for orders"
required: true
secret: true
schemes: ["postgres"]
maxLength: 2048
}
LOG_LEVEL: {
type: "enum"
description: "Minimum log level"
values: ["debug", "info", "warn", "error"]
default: "info"
}
PORT: {
type: "int"
description: "HTTP listen port"
min: 1
max: 65535
default: 8080
}
REQUEST_TIMEOUT: {
type: "duration"
description: "Timeout for a request to finish"
details: "How long a request may take before the server gives up on it.\n\nRaise it when clients upload large order batches. Keep it below the load balancer's idle timeout, or the\nclient sees a reset rather than a `504`.\n\nThe platform writes Go durations such as `45s`; docuconf converts them to ISO 8601 for pydantic."
encoding: "iso8601"
min: "1s"
max: "5m"
default: "30s"
}
WORKER_COUNT: {
type: "int"
description: "Workers processing orders"
min: 1
max: 64
default: 4
}
}
}The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.
7.FastAPI, Django and Flask
Call docuconf.load(Settings) once at startup and share the result: in FastAPI, at module level or in the lifespan, then hand it out with Depends; in Django, in settings.py, copying the values into Django's settings; in Flask, in the app factory, before app.config.from_mapping. A failed load raises before the app serves anything.
Next
- The docuconf-python README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: No JKS keystores. No profiles.
Every snippet on this page is compiled or run against docuconf-python's main branch in CI. Facts checked 2026-10-08.