Python PyYAML: Read and Write YAML Safely
python pyyaml read write yaml safely: Learn how to read and write YAML files with PyYAML without exposing your application to code injection. Use safe_load and safe_dump.
python pyyaml read write yaml safely requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When working with YAML in Python, PyYAML is the most common library. But its default load() function can execute arbitrary Python code if the YAML contains specially crafted tags. This article explains how to read and write YAML safely using safe_load() and safe_dump(), and when you might need more advanced handling.
Why load() Is Unsafe
PyYAML's load() function is a full-featured deserializer. It can construct arbitrary Python objects, including instances of custom classes, by interpreting YAML tags like !!python/object/apply. If you load YAML from an untrusted source—a user upload, an API response, or a configuration file that can be edited by non‑administrators—an attacker can craft a YAML document that executes a system command or reads sensitive files.
Consider this YAML snippet:
!!python/object/apply:os.system ["rm -rf /"]
Loading it with load() would invoke os.system and attempt to delete files. Even if your application doesn't intentionally use such tags, a malicious input can exploit the loader's ability to resolve arbitrary Python references. This is a code injection vulnerability, not a theoretical concern.
The PyYAML documentation has always warned against using load() on untrusted input, yet many tutorials and legacy code still use it. The safe alternative, safe_load(), restricts construction to basic YAML types—dictionaries, lists, strings, numbers, booleans, and None—and refuses to instantiate any Python object.
Reading YAML with safe_load()
To read a YAML file safely, use yaml.safe_load(). It accepts a file object or a string and returns a Python data structure built only from basic types.
import yaml with open("config.yaml", "r") as f: data = yaml.safe_load(f) print(data)
If config.yaml contains:
version: 1 name: demo features: - auth - logging
safe_load() returns a dictionary:
{'version': 1, 'name': 'demo', 'features': ['auth', 'logging']}
You can also parse a string directly:
parsed = yaml.safe_load("key: value\nnumber: 42\n")
safe_load() raises a yaml.constructor.ConstructorError if it encounters a tag that attempts to create a non‑basic type. This error is clear and prevents the dangerous operation from executing.
Writing YAML with safe_dump()
When you need to serialize a Python dictionary or list back to YAML, use yaml.safe_dump(). It mirrors safe_load() by only emitting basic YAML types. If you pass an object that isn't a basic type, it raises a RepresenterError instead of silently producing a potentially unsafe tag.
import yaml config = { "version": 1, "name": "demo", "features": ["auth", "logging"] } with open("output.yaml", "w") as f: yaml.safe_dump(config, f)
By default, safe_dump() sorts dictionary keys alphabetically. To preserve insertion order, pass sort_keys=False:
yaml.safe_dump(config, f, sort_keys=False)
You can also control indentation and line width:
yaml.safe_dump(config, f, indent=2, default_flow_style=False)
For simple data structures, safe_dump() is the right choice. It prevents accidental serialization of non‑basic objects and keeps the output clean and portable.
Handling Custom Python Objects
If you need to serialize custom class instances, safe_load() and safe_dump() won't work directly. You have two safe options: convert your objects to dictionaries before serialization, or register explicit representers and constructors with yaml.add_representer() and yaml.add_constructor().
The first approach is simpler and often sufficient. Define a method that returns a dictionary representation, and reconstruct the object manually after loading.
class User: def __init__(self, name, age): self.name = name self.age = age def to_dict(self): return {"name": self.name, "age": self.age} @classmethod def from_dict(cls, data): return cls(data["name"], data["age"]) user = User("Alice", 30) with open("user.yaml", "w") as f: yaml.safe_dump(user.to_dict(), f) with open("user.yaml", "r") as f: data = yaml.safe_load(f) user = User.from_dict(data)
This approach avoids any custom YAML tags and keeps the serialization format explicit. It also makes the YAML human‑readable and language‑agnostic.
If you must use custom tags, register a representer and a constructor that only accept expected data shapes. This is more complex and requires careful validation to avoid introducing the same vulnerabilities you're trying to prevent. In most applications, converting to dictionaries is the safer and more maintainable path.
Error Handling and Edge Cases
safe_load() raises yaml.YAMLError (or a subclass) when the input is malformed. You should catch this exception and handle it gracefully, especially when reading from user‑provided files.
import yaml try: with open("config.yaml", "r") as f: data = yaml.safe_load(f) except yaml.YAMLError as e: print(f"Invalid YAML: {e}") # fallback or abort
Empty files are a common edge case. safe_load() returns None for an empty string or an empty file. If your application expects a dictionary, you may want to default to an empty dict:
data = yaml.safe_load(f) or {}
Another edge case is multiple YAML documents in a single file, separated by ---. safe_load() only reads the first document. To read all documents, use yaml.safe_load_all(), which returns a generator.
with open("multi.yaml", "r") as f: for doc in yaml.safe_load_all(f): print(doc)
Similarly, yaml.safe_dump_all() writes a sequence of documents.
Performance and Compatibility Considerations
safe_load() and safe_dump() are implemented in pure Python, just like the regular loaders. The performance difference between load() and safe_load() is negligible for typical configuration files and data exchange. The safety benefit far outweighs any micro‑optimization you might gain by using the unsafe variant.
If you need to parse YAML in a performance‑critical path, consider using CSafeLoader and CSafeDumper, which are C‑accelerated versions available in PyYAML. They behave identically to the safe loaders but run faster.
import yaml # Use C accelerated safe loader/dumper data = yaml.load(stream, Loader=yaml.CSafeLoader) yaml.dump(data, stream, Dumper=yaml.CSafeDumper)
However, CSafeLoader and CSafeDumper are optional; they may not be available in all builds. Always fall back to the pure‑Python versions if the C extensions are missing.
For applications that need round‑trip preservation of comments and formatting, PyYAML is not the best choice. Libraries like ruamel.yaml offer a round_trip_load and round_trip_dump that preserve comments, but they come with their own safety considerations. If you only need to read and write plain data structures, PyYAML's safe methods are sufficient and secure.
When to Use the Unsafe load()
There are rare cases where you intentionally need to deserialize Python objects from YAML, such as when loading a trusted configuration file that you control completely. In that scenario, you can use yaml.load() with an explicit Loader class, like yaml.FullLoader or yaml.UnsafeLoader. But this should be a deliberate decision, not the default.
If you must use load(), always pass a Loader argument to avoid the deprecated default behavior. In PyYAML 5.1+, calling yaml.load(stream) without a Loader raises a TypeError to force you to choose a loader. For untrusted input, yaml.safe_load() is the only correct choice.
The safest pattern is to treat all YAML as untrusted until you have verified its origin. Use safe_load() for reading and safe_dump() for writing. This simple practice prevents a whole class of code injection vulnerabilities and keeps your application secure without sacrificing functionality.