Config documentation fails when a generated table invents production defaults or treats every environment variable as a public example. A typed settings class already encodes names, types, and prefixes that a compiler can extract without guessing runtime values. Models may draft short field descriptions from those extracted names, while humans must still classify secrets, missing-key failure, and blast radius. The workflow below keeps that split explicit so a README cannot silently promote a database password into a copy-paste snippet.
This article proposes an unexecuted, stdlib-only extractor for Python dataclasses used as settings objects. It does not claim production incident rates, vendor benchmarks, or traffic numbers, because those figures were not supplied for review. The method is useful only when configuration already lives in annotated fields rather than stringly typed dictionaries. Teams that construct environment names at runtime should treat the catalog as a lower bound, not as a complete inventory.
What a compiler can see, and what it cannot
A settings dataclass usually exposes field names, type annotations, default sentinels, and sometimes an environment prefix. Those facts are testable: if a field appears in source, the catalog row must exist, and a unit test can fail the build when it does not. Descriptions inferred from names are not testable in the same way, because several honest sentences can match one identifier. Secret classification is also not testable from the type system, because str does not mean “public example value.”
Missing-key behavior is a product decision rather than a parsing result. Some services must refuse to boot when a signing key is absent, while others may start with a cache disabled and a degraded path. Outage copy, customer-visible status text, and pager ownership cannot be compiled from annotations. Those cells stay blank until a human signs them, even if a model drafts nearby prose.
Lane table for each settings field
Treat every extracted field as a row with two lane groups. The compile lane is filled by the extractor. The signature lane stays empty until review. Proposed columns follow; they are a template, not a measured industry standard.
| Column | Lane | Source of truth | Allowed into a model prompt |
|---|---|---|---|
key |
compile | field name plus env prefix | yes, if names are not themselves classified |
python_type |
compile | annotation string | yes |
has_default |
compile | presence of a default or factory | yes |
default_repr |
compile, redacted | public defaults only | never for secrets |
description |
model draft | human-editable after draft | yes, names and types only |
secret_class |
human sign | threat model | no |
missing_key |
human sign | runbook and SLOs | no |
blast_radius |
human sign | ownership map | no |
The table is the artifact’s contract. A model that fills secret_class or missing_key is out of policy, even when the sentence sounds confident. Public example values belong only on rows that a reviewer already marked non-secret.
Artifact: extract rows, then refuse unsigned secrets
The extractor walks a dataclass, emits JSON rows, and leaves signature cells null. A second function renders Markdown with placeholders that continuous integration can grep. Label this as sample code for a proposed workflow, not as a harvested production module.
# settings_catalog.py — proposed extractor, stdlib only
from __future__ import annotations
import dataclasses
import json
from typing import Any, get_type_hints
@dataclasses.dataclass(frozen=True)
class CatalogRow:
key: str
python_type: str
has_default: bool
default_repr: str | None
description: str | None
secret_class: str | None
missing_key: str | None
blast_radius: str | None
def _annotation_name(hint: Any) -> str:
return getattr(hint, "__name__", str(hint))
def extract_settings_catalog(
cls: type,
env_prefix: str = "",
redact_defaults: bool = True,
) -> list[dict[str, Any]]:
if not dataclasses.is_dataclass(cls):
raise TypeError(f"{cls!r} is not a dataclass")
hints = get_type_hints(cls)
rows: list[CatalogRow] = []
for field in dataclasses.fields(cls):
key = f"{env_prefix}{field.name}".upper() if env_prefix else field.name
has_default = field.default is not dataclasses.MISSING or (
field.default_factory is not dataclasses.MISSING # type: ignore[attr-defined]
)
default_repr = None
if has_default and not redact_defaults and field.default is not dataclasses.MISSING:
default_repr = repr(field.default)
rows.append(
CatalogRow(
key=key,
python_type=_annotation_name(hints.get(field.name, field.type)),
has_default=has_default,
default_repr=default_repr,
description=None,
secret_class=None,
missing_key=None,
blast_radius=None,
)
)
return [dataclasses.asdict(row) for row in rows]
def assert_signature_complete(rows: list[dict[str, Any]]) -> None:
unsigned = [
row["key"]
for row in rows
if row.get("secret_class") in (None, "")
or row.get("missing_key") in (None, "")
or row.get("blast_radius") in (None, "")
]
if unsigned:
raise AssertionError("unsigned config keys: " + ", ".join(unsigned))
A small settings object makes the compile lane visible. Keep secret-looking defaults out of the extract call so the JSON file cannot leak a password into pull-request diffs.
# example_settings.py — sample input only
from dataclasses import dataclass
@dataclass
class ServiceSettings:
database_url: str
signing_key: str
cache_ttl_seconds: int = 60
public_base_url: str = "https://api.example.test"
python - <<'PY'
from example_settings import ServiceSettings
from settings_catalog import extract_settings_catalog
import json
print(json.dumps(extract_settings_catalog(ServiceSettings, env_prefix="APP_"), indent=2))
PY
Expected compile-lane shape, with signature cells still null:
[
{
"key": "APP_DATABASE_URL",
"python_type": "str",
"has_default": false,
"default_repr": null,
"description": null,
"secret_class": null,
"missing_key": null,
"blast_radius": null
}
]
Render Markdown only after a reviewed sidecar file supplies the three human columns. The renderer should refuse to print example values for any row whose secret_class is not public.
# render_catalog.py — proposed renderer
from __future__ import annotations
SIGNED = ("secret_class", "missing_key", "blast_radius")
def render_markdown(rows: list[dict], signed: dict[str, dict]) -> str:
lines = [
"| Key | Type | Default in source | Description | Secret class | Missing key | Blast radius |",
"| --- | --- | --- | --- | --- | --- | --- |",
]
for row in rows:
extra = signed.get(row["key"], {})
merged = {**row, **extra}
for col in SIGNED:
if not merged.get(col):
raise ValueError(f"{row['key']} missing signed column {col}")
default_cell = "—"
if merged["secret_class"] == "public" and merged.get("default_repr"):
default_cell = merged["default_repr"]
elif merged["has_default"]:
default_cell = "present (redacted)"
desc = merged.get("description") or "_unsigned description_"
lines.append(
f"| `{merged['key']}` | `{merged['python_type']}` | {default_cell} | "
f"{desc} | {merged['secret_class']} | {merged['missing_key']} | {merged['blast_radius']} |"
)
return "\n".join(lines) + "\n"
Numbered workflow
- Freeze the settings type in review, including the environment prefix and which modules are allowed to read
os.environdirectly. Direct reads outside the dataclass are catalog holes and should fail a grep-based test. - Run
extract_settings_catalogin continuous integration and commit the JSON as a generated artifact next to a human sidecar, not as a chat transcript. The generated file should be deterministic so diffs stay small when a field is renamed. - Optionally draft the
descriptioncolumn from names and types only. Do not include live values,.envfiles, or cluster secrets in that prompt. A hosted editor is enough if it cannot see the sidecar’s secret classes. - Require a reviewer to fill
secret_class,missing_key, andblast_radiusin the sidecar. Suggested vocabularies arepublic | confidential | secretandrefuse_boot | degrade | ignore, but teams should replace those labels with their own incident taxonomy. - Render Markdown only through
render_catalog, then publish that file. Hand-edited HTML in the docs site is a third source of truth and will drift. - Add a unit test that new dataclass fields cannot merge until the sidecar has matching keys. Unsigned rows must break the build rather than shipping with placeholder prose.
# test_settings_catalog.py — proposed contract test
from example_settings import ServiceSettings
from settings_catalog import extract_settings_catalog, assert_signature_complete
def test_every_field_has_a_row():
rows = extract_settings_catalog(ServiceSettings, env_prefix="APP_")
keys = {row["key"] for row in rows}
assert keys == {
"APP_DATABASE_URL",
"APP_SIGNING_KEY",
"APP_CACHE_TTL_SECONDS",
"APP_PUBLIC_BASE_URL",
}
def test_unsigned_rows_fail_the_build():
rows = extract_settings_catalog(ServiceSettings, env_prefix="APP_")
try:
assert_signature_complete(rows)
except AssertionError as exc:
assert "APP_SIGNING_KEY" in str(exc)
else:
raise AssertionError("unsigned catalog must not pass")
python -m unittest test_settings_catalog.py
Where a free model and a free server fit
Description drafting is the only model-shaped step in this workflow, and it still needs a human edit pass. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode’s free model access and free server option can run the extractor and host a draft of the description column while the signed sidecar stays off the prompt. That split is the product-relevant part; this article does not assert model names, quotas, hardware, duration, or quality rankings, because those details were not provided as verified facts.
Keep the prompt to compile-lane JSON plus a one-paragraph style guide for descriptions. Reject any completion that writes secret_class, invents a production default, or emits an example connection string. If field names themselves are sensitive, skip the model step and write descriptions on the workstation that already holds the repository.
Failure analysis the tests will not catch
Dynamically built keys such as APP_TENANT_{id}_TOKEN never appear on the dataclass and therefore never appear in the catalog. Feature flags loaded from a remote service have the same gap. Operators who export secrets as shell functions rather than environment variables will also drift from the extracted names. Those cases need a second inventory, not a more aggressive language model.
Default factories that read files at import time can still leak into traces if logging prints the settings object. The extractor redacts default_repr by default, but application logs are a separate channel. Reviewers should forbid __repr__ on settings types that hold credentials, independent of documentation generation.
Limitations and who should not use this
The method assumes one dataclass, or a small set of dataclasses, is the only legal reader of process environment. Polyglot services with duplicated keys in YAML, Helm, and Terraform need a cross-language inventory that this script does not provide. It also assumes reviewers will actually sign three columns; an empty sidecar that is force-merged makes the Markdown renderer a false control.
Do not use this approach when configuration is entirely dynamic, when field names are classified, or when legal copy such as retention periods must be drafted by counsel rather than by engineering review. Do not use a model to invent missing-key behavior for payment, identity, or medical systems. The compiler can list the knobs; it cannot accept residual risk on behalf of the operator.
The durable output is a catalog whose compile columns change only when code changes, and whose signature columns change only when a human accepts operational meaning. Descriptions may be drafted, edited, or discarded. Secrets, boot-fail policy, and blast radius remain unsigned until a reviewer writes them, which is the entire point of keeping documentation generation on a short leash.
Top comments (0)