strictyaml
StrictYAML is a Python parser and schema validator for a deliberately restricted subset of YAML. Without a schema it loads scalars as strings, avoiding YAML's surprising implicit conversions; with a Python-defined schema it converts and validates integers, booleans, dates, sequences, mappings, enums, and more. It rejects features the project considers ambiguous or unsafe, including duplicate keys, explicit tags, anchors, and normal flow-style collections. Its distinctive feature is round-trip editing: parsed values retain comments and source positions, can be changed, and can be emitted as YAML again.
Choose StrictYAML when rejecting clever YAML is a feature and configuration authors benefit from precise schema errors. Do not use it as a drop-in reader for YAML produced elsewhere, or when portable schemas and speed matter.
Use it if
- You control a human-edited configuration format and want invalid keys or types rejected with line-aware messages
- You want YAML scalars to stay strings unless a schema explicitly converts them
- You need to edit and re-emit configuration while preserving comments
- You prefer a small Python schema language over maintaining a separate JSON Schema file
- You must accept ordinary YAML from other tools: anchors, aliases, explicit tags, duplicate keys, and flow-style maps or lists are deliberately rejected by normal load
- Parsing throughput matters: the project's own priorities explicitly say speed is not a priority
- Your schema must be language-neutral or shared with frontend code: validators are Python objects rather than JSON Schema or another portable document
- You need file-object convenience: the design documentation says StrictYAML only parses strings, so opening files and handling encodings remains your job
- You need high release activity: PyPI is still at 1.7.3 and the repository's latest push was 2025-05-23
Setup reality
pip install strictyaml installs a pure Python-facing package with python-dateutil as its declared runtime dependency, and release 1.7.3 supports Python 3.7 or newer. The real setup cost is defining the accepted document shape in Python. Map is closed by default, so an unexpected key raises an error; use Optional for optional keys, MapPattern for arbitrary names, or MapCombined when a few known keys coexist with patterned ones. Without a schema, every scalar stays a string. That prevents implicit values such as yes or no from silently becoming booleans, but it surprises code expecting numbers from load(...).data. Normal load intentionally rejects flow-style collections, anchors and aliases, explicit tags, and duplicate keys. dirty_load can admit flow style when allow_flow_style=True, but using it weakens the main reason to choose this package and still does not make StrictYAML a general YAML compatibility layer. The parser accepts YAML text, not a path or file object, so open with an explicit encoding and pass the resulting string. Keep the returned YAML object if you need comments, line numbers, or round-trip output; accessing .data gives ordinary Python structures and loses that editing context. Optional defaults appear in parsed data but are omitted when serializing if they equal the default, which can surprise snapshot tests. Assignments are revalidated, so updates can raise YAMLSerializationError. Catch YAMLError for user-facing parse and validation failures and print its marked snippet, but avoid exposing full configuration text if it can contain secrets.
Patterns
Load conservative string dataload-without-schema
from strictyaml import load
config = load("port: 8080\nenabled: yes\n")
assert config.data == {"port": "8080", "enabled": "yes"}Without a schema, scalars remain strings. This is intentional protection against YAML implicit typing, not a failed conversion.
Validate and convert a fixed mappingvalidate-mapping
from strictyaml import Bool, Int, Map, Str, load
schema = Map({
"host": Str(),
"port": Int(),
"debug": Bool(),
})
config = load(yaml_text, schema).dataMap rejects missing required keys and unexpected keys. Add Optional or choose MapCombined when the shape is not fully closed.
Add an optional key with a defaultset-optional-default
from strictyaml import Bool, Int, Map, Optional, load
schema = Map({
"port": Int(),
Optional("debug", default=False): Bool(),
})
config = load("port: 8080\n", schema).data
assert config["debug"] is FalseA value equal to its Optional default is dropped from serialized YAML even though it is present in parsed .data.
Validate a sequence of mappingsvalidate-sequence
from strictyaml import Int, Map, Seq, Str, load
schema = Seq(Map({"name": Str(), "port": Int()}))
services = load("- name: api\n port: 8080\n", schema).dataSeq applies one validator to every item. Use FixedSeq when positions intentionally have different types.
Require unique sequence valuesreject-duplicates
from strictyaml import Str, UniqueSeq, load
schema = UniqueSeq(Str())
tags = load("- stable\n- internal\n", schema).dataUniqueSeq raises YAMLValidationError for duplicate input and YAMLSerializationError if code tries to serialize duplicate values.
Validate arbitrary mapping keysallow-patterned-keys
from strictyaml import Int, MapPattern, Regex, load
schema = MapPattern(Regex(r"^[a-z][a-z0-9-]+$"), Int())
ports = load("api: 8080\nadmin: 8081\n", schema).dataMapPattern is the right fit for user-defined key names; a plain Map is for a known closed set.
Allow one of two scalar typesaccept-either-type
from strictyaml import Bool, Int, Map, load
schema = Map({"workers": Int() | Bool()})
config = load("workers: 4\n", schema).dataThe | operator composes validators. Keep alternatives distinguishable or error messages and resulting types can become harder to reason about.
Edit while preserving commentsround-trip-comments
from strictyaml import Int, Map, Str, load
schema = Map({"name": Str(), "port": Int()})
doc = load("# public listener\nname: api\nport: 8080\n", schema)
doc["port"] = 8081
print(doc.as_yaml())Keep the YAML document object for round trips. Converting to .data and rebuilding a document does not preserve the original comments.
Locate a value in the sourcereport-source-line
doc = load(yaml_text, schema)
print("port came from line", doc["port"].start_line)Line metadata belongs to YAML nodes, not the ordinary objects returned by .data.
Admit flow style as a migration escape hatchload-flow-style
from strictyaml import Int, Map, dirty_load
schema = Map({"x": Int(), "y": Int()})
doc = dirty_load("{x: 1, y: 2}", schema, allow_flow_style=True)Normal load rejects flow-style YAML. dirty_load is explicitly the less-strict path and should not become the default unnoticed.
Create YAML from Python databuild-document
from strictyaml import Int, Map, Str, as_document
schema = Map({"name": Str(), "port": Int()})
doc = as_document({"name": "api", "port": 8080}, schema)
print(doc.as_yaml())as_document validates during serialization, so incompatible Python values raise YAMLSerializationError rather than producing invalid output.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| PyYAML | PyPI | You need broad YAML compatibility and familiar safe_load behavior without a built-in schema system |
| ruamel.yaml | PyPI | You need YAML 1.2 features plus detailed comment-preserving round trips |
| pydantic | PyPI | You want rich typed validation after loading YAML, JSON, environment variables, or other input sources |