Skip to main content

Generate, read and document pydantic-settings config files in YAML

Project description

pydantic-settings-yaml-plus

A helper library that builds upon pydantic-settings to generate, read, and document YAML config files — with comment headers that explain every setting's type, default value, allowed values, environment variable override, and description.


Features

  • Generate documented YAML — each field gets a comment block with its type, required flag, default value, allowed enum values, corresponding environment variable, description, and examples.
  • Nested modelsBaseModel sub-models are rendered as indented YAML sections with full comment headers.
  • Read back with env-var overrideload() loads the YAML file and still applies environment variable overrides using pydantic-settings' standard machinery.
  • Markdown docs — generate a Markdown reference document straight from your settings class.
  • Zero boilerplate — define your settings in a standard pydantic_settings.BaseSettings subclass; psyplus does the rest.

Installation

pip install psyplus

Requirements: Python ≥ 3.12, pydantic ≥ 2.0, pydantic-settings ≥ 2.3


Quick start

1 — Define your settings model

# config.py
from typing import Annotated, Dict, List, Literal, Optional
from pathlib import Path, PurePath

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class DatabaseServerSettings(BaseSettings):
    model_config = SettingsConfigDict(env_nested_delimiter="__")

    host: Optional[str] = Field(
        default="localhost",
        description="The hostname the database will be available at",
    )
    port: Optional[int] = Field(
        default=5678,
        description="The port to connect to the database",
    )
    database_names: List[str] = Field(
        description="The names of the databases to use",
        examples=[["mydb", "theotherdb"]],
    )


class MyAppConfig(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", env_nested_delimiter="__")

    log_level: Optional[Literal["INFO", "DEBUG"]] = "INFO"
    app_name: Optional[str] = Field(
        default="THE APP",
        description="The display name of the app",
        examples=["THAT APP", "THIS APP"],
    )
    storage_dir: Optional[str] = Field(
        description="A directory to store files for the app.",
        default_factory=lambda: str(Path(PurePath(Path().home(), ".config/myapp/"))),
    )
    admin_pw: Annotated[str, Field(description="The init password for the admin account")]
    database_server: DatabaseServerSettings = Field(
        description="The settings for the database server",
        examples=[
            DatabaseServerSettings(
                host="db.company.org", port=1234, database_names=["db1", "db2"]
            )
        ],
    )
    init_values: Dict[str, str]

Note on model_config: use SettingsConfigDict (pydantic-settings v2 style) rather than the old inner class Config. Both work, but SettingsConfigDict is preferred.

2 — Generate the YAML config file

from psyplus import YamlSettingsPlus
from config import MyAppConfig

handler = YamlSettingsPlus(MyAppConfig, "config.yaml")
handler.generate_config_file()                       # silently skips if file exists (default)
handler.generate_config_file(on_exists="overwrite")  # replace existing file
handler.generate_config_file(on_exists="error")      # raise FileExistsError if file exists

Generated YAML output

The command above creates config.yaml with a comment block above every key:

# ## log_level ###
# Type:         Enum
# Required:     False
# Default:      "INFO"
# Allowed vals: ['INFO', 'DEBUG']
# Env-var:      'APP_LOG_LEVEL'
log_level: INFO

# ## app_name ###
# Type:        str
# Required:    False
# Default:     "THE APP"
# Env-var:     'APP_APP_NAME'
# Description: The display name of the app
# Example No. 1:
#  >app_name: THAT APP
# Example No. 2:
#  >app_name: THIS APP
app_name: THE APP

# ## storage_dir ###
# Type:        str
# Required:    False
# Env-var:     'APP_STORAGE_DIR'
# Description: A directory to store files for the app.
storage_dir: /home/user/.config/myapp

# ## admin_pw ###
# Type:        str
# Required:    True
# Env-var:     'APP_ADMIN_PW'
# Description: The init password for the admin account
admin_pw: ''

# ## database_server ###
# Type:        Object (DatabaseServerSettings)
# Required:    True
# Env-var:     'APP_DATABASE_SERVER'
# Description: The settings for the database server
# Example:
#  >database_server:
#  >  host: db.company.org
#  >  port: 1234
#  >  database_names:
#  >  - db1
#  >  - db2
database_server:

  # ## host ###
  # YAML-path:   database_server.host
  # Type:        str
  # Required:    False
  # Default:     "localhost"
  # Env-var:     'APP_DATABASE_SERVER__HOST'
  # Description: The hostname the database will be available at
  host: localhost

  # ## port ###
  # YAML-path:   database_server.port
  # Type:        int
  # Required:    False
  # Default:     5678
  # Env-var:     'APP_DATABASE_SERVER__PORT'
  # Description: The port to connect to the database
  port: 5678

  # ## database_names ###
  # YAML-path:   database_server.database_names
  # Type:        List of str
  # Required:    True
  # Env-var:     'APP_DATABASE_SERVER__DATABASE_NAMES'
  # Description: The names of the databases to use
  # Example:
  #  >database_names:
  #  >- mydb
  #  >- theotherdb
  database_names: []

# ## init_values ###
# Type:     Dictionary of (str, str)
# Required: True
# Env-var:  'APP_INIT_VALUES'
init_values: {}

Required fields without defaults are written with an empty placeholder ('', [], {}).
The Required: True comment tells the user they must fill these in before the app will validate.


Reading the config

from psyplus import YamlSettingsPlus
from config import MyAppConfig

handler = YamlSettingsPlus(MyAppConfig, "config.yaml")
config: MyAppConfig = handler.load()

print(config.database_server.host)   # → "localhost" (or whatever is in the YAML)
print(config.log_level)              # → "INFO"

load() honours environment variable overrides through pydantic-settings' standard source priority: env vars beat YAML values. For example:

export APP_LOG_LEVEL=DEBUG

…will make config.log_level == "DEBUG" even if the YAML file says INFO.

Tip: if you prefer to keep psyplus out of your runtime path, configure yaml_file directly in your model and call it without psyplus:

from pydantic_settings import BaseSettings, SettingsConfigDict

class MyAppConfig(BaseSettings):
    model_config = SettingsConfigDict(
        yaml_file="config.yaml",
        env_prefix="APP_",
        env_nested_delimiter="__",
    )

config = MyAppConfig()  # reads YAML + env vars automatically

Generating Markdown documentation

Write a Markdown reference document to disk:

from psyplus import YamlSettingsPlus
from config import MyAppConfig

YamlSettingsPlus(MyAppConfig).generate_markdown_doc("SETTINGS.md")

Or get the Markdown as a string (e.g. to post to a wiki API):

md = YamlSettingsPlus(MyAppConfig).render_markdown()

Both produce a document with a property table for every field: type, required flag, default, allowed values, environment variable, description, and examples.


Example gallery

The table below links the bundled test models to their pre-generated output so you can see exactly what psyplus produces before writing a single line of your own config.

Model Source What it demonstrates YAML output Markdown output
SimpleModel tests/models/simple.py Flat settings: scalars, Optional, Literal, List, Dict, Annotated metadata, default_factory SimpleModel.yaml SimpleModel.md
NestedModel tests/models/nested.py Nested BaseModel sub-field, optional sub-model, env-var path generation with env_nested_delimiter NestedModel.yaml NestedModel.md
MultiNestedModel tests/models/nested.py List[BaseModel] and Dict[str, BaseModel] fields — YAML renders each item, Markdown recurses into the item schema MultiNestedModel.yaml MultiNestedModel.md
MyAppConfig tests/models/readme_example.py The full README quick-start model with nested database settings and field examples MyAppConfig.yaml MyAppConfig.md

These files are regenerated automatically by the smoke script (pdm run smoke) and committed to the repository, so they are always in sync with the current code.


API reference

YamlSettingsPlus(model, file_path=None)

Parameter Type Description
model Type[BaseSettings] The pydantic-settings class to work with
file_path str | Path | None Path of the YAML config file

Methods

Method Description
generate_config_file(on_exists="skip") Write a documented YAML template to file_path. on_exists controls what happens when the file already exists: "skip" (default) does nothing, "overwrite" replaces it, "error" raises FileExistsError.
generate_markdown_doc(output_path) Write a Markdown reference document to output_path.
render_yaml() → str Render the documented YAML as a string without touching the filesystem.
render_markdown() → str Render the Markdown reference document as a string without touching the filesystem.
load() → BaseSettings Load file_path, apply env var overrides, return a validated settings instance.

YamlFileGenerator(settings_class, indent_size=2)

Low-level generator for the YAML string. Useful if you want to customise output without using YamlSettingsPlus.

from psyplus import YamlFileGenerator
from config import MyAppConfig

gen = YamlFileGenerator(MyAppConfig)
gen.parse()
print(gen.get_yaml())

MarkdownDocGenerator(settings_class)

Low-level generator for the Markdown string.

from psyplus import MarkdownDocGenerator
from config import MyAppConfig

doc = MarkdownDocGenerator(MyAppConfig).generate()

Defining your settings model

psyplus works with any standard pydantic_settings.BaseSettings subclass. The richer the metadata you provide on your fields, the more informative the generated YAML/Markdown will be.

Useful Field() kwargs

Kwarg Shown in output as
description="…" # Description: comment row
title="…" Appended to the ## field_name - Title ### header
examples=[…] # Example: block with rendered YAML
default=… # Default: comment row
default_factory=lambda: … # Default: not shown (factory evaluated at generation time)
max_length=… / gt=… / etc. # Constraints: comment row

Configuring env vars

Set env_prefix and env_nested_delimiter in model_config so that psyplus can generate correct Env-var: hints:

from pydantic_settings import BaseSettings, SettingsConfigDict

class MyConfig(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="MYAPP_",
        env_nested_delimiter="__",
    )

    database_host: str = "localhost"
    # → Env-var: 'MYAPP_DATABASE_HOST'

For nested models the env var path is built as:

{env_prefix}{parent_key}{env_nested_delimiter}{child_key}
# e.g. MYAPP_DATABASE__HOST  (prefix="MYAPP_", delimiter="__")

Environment variable overrides

load() creates a thin dynamic subclass that adds the YAML file as a pydantic-settings source while keeping the normal env var source with higher priority. The effective source priority (highest → lowest) is:

  1. Environment variables (respects env_prefix / env_nested_delimiter)
  2. YAML file values
  3. Model defaults

No changes to your settings class are required.


Development

This project uses PDM for dependency management.

git clone https://github.com/DZD-eV-Diabetes-Research/pydantic-settings-yaml-plus
cd pydantic-settings-yaml-plus
pdm install -G test
pdm run pytest

If you prefer plain pip (no PDM):

pip install -e ".[test]"
pytest

Smoke test

To visually inspect the generated YAML and Markdown for all built-in test models, run:

pdm run smoke

This writes four files per model (YAML config template + Markdown reference) to smoke_output/ and prints a preview of each to the terminal. The output is committed to the repository and linked from the Example gallery section — re-run the script and commit the results whenever the generator logic changes.

smoke_output/
├── SimpleModel.yaml
├── SimpleModel.md
├── NestedModel.yaml
├── NestedModel.md
├── MultiNestedModel.yaml
├── MultiNestedModel.md
├── MyAppConfig.yaml
└── MyAppConfig.md

Running tests with coverage

pdm run pytest tests/ --cov=psyplus --cov-report=term-missing

PDM cheat-sheet

Task Command
Install all deps incl. test extras pdm install -G test
Add a runtime dependency pdm add <package>
Add a test-only dependency pdm add -G test <package>
Update lock file pdm update
Run any command in the PDM venv pdm run <command>
Show installed packages pdm list

Tests are in tests/ and cover utilities, field metadata, YAML generation, Markdown generation, and the full YamlSettingsPlus API. They run in CI against Python 3.12, 3.13, and 3.14.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

psyplus-1.0.0.tar.gz (26.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

psyplus-1.0.0-py3-none-any.whl (17.5 kB view details)

Uploaded Python 3

File details

Details for the file psyplus-1.0.0.tar.gz.

File metadata

  • Download URL: psyplus-1.0.0.tar.gz
  • Upload date:
  • Size: 26.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for psyplus-1.0.0.tar.gz
Algorithm Hash digest
SHA256 343640e32c16d58667fc0ce08ca417e17ea10b79d68cc1dbc1e31bb8653b4823
MD5 4d390b4f80379b43b670bdca824916e4
BLAKE2b-256 23c9701533e049dc9ab726276dd469062969720c7423ea6b5d24032fa384fb9c

See more details on using hashes here.

Provenance

The following attestation bundles were made for psyplus-1.0.0.tar.gz:

Publisher: publish-pypi.org.yaml on DZD-eV-Diabetes-Research/pydantic-settings-yaml-plus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file psyplus-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: psyplus-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 17.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for psyplus-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f5f0894383f0086c2f8e584b86e2138ec2c8b098c1348edd071cd89a456408bb
MD5 b2bc737e96de886d228d9dfffb5442c3
BLAKE2b-256 0cdecea81bfa04fd73ac9d7f957bb2a8cd1d0d114e9262b99726624a958b0e08

See more details on using hashes here.

Provenance

The following attestation bundles were made for psyplus-1.0.0-py3-none-any.whl:

Publisher: publish-pypi.org.yaml on DZD-eV-Diabetes-Research/pydantic-settings-yaml-plus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page