Read, write, and validate YAML in Python

Load YAML with PyYAML, validate configuration structure, write nested data, use PyKwalify schemas, and understand type conversion and round-trip limits.

Best practices
Concurrent tasks moving through a shared processing hub

PyYAML is a third-party library for parsing and emitting YAML in Python. Use safe_load() for ordinary configuration data and safe_dump() when writing supported data types. Then validate the result: valid YAML can still be a scalar, an empty document, or a mapping with the wrong fields.

Safe loading restricts object construction. It is not a complete resource sandbox for arbitrary untrusted input, and it does not validate your application's configuration contract.

Install and read a configuration

python -m pip install pyyaml

Save this as config.yaml:

database:
  host: localhost
  port: 5432
  pool_size: 10
features:
  - search

Read it and check the structure before accessing nested fields:

import yaml

with open("config.yaml", encoding="utf-8") as stream:
    config = yaml.safe_load(stream)

if not isinstance(config, dict):
    raise ValueError("Configuration must be a mapping")
database = config.get("database")
if not isinstance(database, dict) or not isinstance(database.get("host"), str):
    raise ValueError("database must contain a string host")
features = config.get("features")
if not isinstance(features, list) or not all(isinstance(item, str) for item in features):
    raise ValueError("features must be a list of strings")
print(database["host"])

An empty document can return None, and a document containing only a scalar returns that scalar's Python value. safe_load() accepts standard YAML tags rather than arbitrary Python object constructors. In current PyYAML, yaml.load() requires an explicit loader; the risk depends on that loader, so a blanket claim that every call to load is unsafe is inaccurate. Prefer the clearly named safe interface for configuration. See PyYAML documentation.

Understand type conversion

YAML data Typical Python result
Mapping dict
Sequence list
String str
Integer or floating-point number int or float
Boolean bool
null or ~ None
Date or timestamp datetime.date or datetime.datetime

Implicit typing deserves attention. PyYAML can interpret unquoted yes, no, on, or off as Booleans, and an unquoted date can become a date object. Quote values intended to remain strings, such as a version label or an identifier that resembles a number. Do not assume another YAML library or a YAML 1.2 consumer applies identical implicit typing rules.

PyYAML's ordinary loader also does not provide a general duplicate-key rejection policy: later entries can replace earlier ones. If rejecting duplicate keys is part of your configuration contract, enforce that with an appropriate loader or validation tool before values are silently lost.

Modify and write the data

Continue from the validated config object above:

config["database"]["pool_size"] = 20
config["features"].append("new-ui")

with open("config.updated.yaml", "w", encoding="utf-8") as stream:
    yaml.safe_dump(
        config,
        stream,
        allow_unicode=True,
        default_flow_style=False,
        sort_keys=False,
    )

This writes a separate file for inspection. If a deployment later replaces the live configuration, handle that replacement and validation according to its update policy.

allow_unicode=True emits supported Unicode characters directly instead of unnecessary escape sequences. sort_keys=False uses the Python mapping's iteration order, and default_flow_style=False requests block-style collections. These options do not preserve the source document's comments, original quoting, or exact formatting.

Generate a new document

import yaml

# An application-specific example, not a Kubernetes resource schema.
deployment = {
    "version": "1.0",
    "services": [
        {"name": "web", "replicas": 3, "ports": [80, 443]},
        {"name": "worker", "replicas": 2},
    ],
}

with open("deploy.yaml", "w", encoding="utf-8") as stream:
    yaml.safe_dump(deployment, stream, allow_unicode=True, sort_keys=False)

Nested dictionaries and lists become corresponding YAML structures. This technique can generate Kubernetes or CI configuration, but only if the input data follows the target tool's actual schema. Successful serialization does not make the illustrative document above a Kubernetes manifest.

Validate a schema with PyKwalify

Install pykwalify. Parsing checks YAML syntax and constructs values; schema validation checks the shape and constraints your application expects. This independent example allows only a database mapping:

import yaml
from pykwalify.core import Core

config = {"database": {"host": "localhost", "port": 5432}}
schema = """
type: map
mapping:
  database:
    required: true
    type: map
    mapping:
      host: {type: str, required: true}
      port: {type: int, range: {min: 1, max: 65535}}
"""

validator = Core(source_data=config, schema_data=yaml.safe_load(schema))
validator.validate()

The database mapping and host are required. port is optional in this schema, but if present it must satisfy its type and range rules. Add required: true when the application requires it. A schema failure raises an exception by default; handle it at startup with a useful configuration error.

The earlier configuration also has pool_size and features. Declare those keys and their constraints before applying a schema to that file; do not assume this deliberately smaller example accepts them. See PyKwalify validation rules.

Validate configuration before initializing clients or starting background work, so a missing field fails at the point where it can be explained.

Handle comments, multiple documents, and substitutions

PyYAML's normal load/dump cycle discards comments and may change presentation. For editing existing documents where those details matter, evaluate ruamel.yaml's round-trip mode and test its preservation limits on your files. See ruamel.yaml details.

A file can contain several documents separated by ---. Consume safe_load_all() while the stream remains open:

import yaml

with open("manifests.yaml", encoding="utf-8") as stream:
    for document in yaml.safe_load_all(stream):
        if document is not None:
            print(type(document).__name__)

The iterator reads lazily, so returning it from a block that has already closed the file is a mistake. Validate each document against the intended resource type.

Plain PyYAML does not expand ${VARIABLE} from the environment. If the application supports substitutions, implement an explicit allowed set and handle missing values and type conversion deliberately. Avoid evaluating configuration as Python code or performing unrestricted string substitution before parsing, which can change YAML structure unexpectedly.

Common questions

When should I choose PyYAML or ruamel.yaml?

PyYAML suits reading and emitting ordinary data. ruamel.yaml's round-trip mode is useful when editing a document while retaining comments and more of its presentation. Neither choice removes the need for schema and resource checks.

Should configuration use YAML, JSON, or TOML?

Follow the consuming tool's contract first. YAML and TOML support comments for human-maintained configuration; JSON is common for data exchange. Kubernetes accepts JSON as well as YAML manifests. Familiarity and ecosystem compatibility are more useful than assuming one format is universally best.

Is safe_load enough for untrusted input?

No. It restricts object construction but does not enforce your size, nesting, or execution-time budget. Aliases can create shared or cyclic object structures that break naive recursive processing. Bound inputs and subsequent processing, and use a suitably constrained process when the trust boundary requires it. Schema validation itself is not a substitute for those limits.

Keep configuration errors useful

Log which configuration file and field failed validation without printing credentials or the entire configuration. Guance's log collection guide can help retain startup diagnostics so a failed deployment is distinguishable from an application request error.

Read/write, typing, multidocument, and schema examples were exercised locally. No live Kubernetes deployment, adversarial resource-limit test, or Guance ingestion was performed. For project setup, see uv package and environment management (Chinese).

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting