Skip to content
docuconf
docuconf on GitHub

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

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the 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.

app.py
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.

app.py
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.py

With 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.

Terminal
$ 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.

test_settings.py
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.py

6.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
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

Every snippet on this page is compiled or run against docuconf-python's main branch in CI. Facts checked 2026-10-08.