Back to Blog
Python

Python PyYAML: Read and Write YAML Safely

Learn how to use PyYAML's safe_load() and safe_dump() to read and write YAML files without exposing your application to code injection.

PyYAMLYAMLPythonsafe_loaddata serialization
Illustration of a Python script using PyYAML to safely read and write YAML files, with a shield icon representing security.

PyYAML is a common YAML library for Python. Its load() API can execute arbitrary Python code if it is used with an unsafe loader and the YAML document contains specially crafted tags. To handle YAML from users, APIs, or shared configuration files, use safe_load() for reading and safe_dump() for writing. This article explains why that distinction matters, how to use the safe methods, how to deal with custom objects, and when load() might still be acceptable.

Why load() Can Be Unsafe

PyYAML's load() function is a full-featured deserializer. When used with an unsafe loader, it can construct arbitrary Python objects, including instances of custom classes, by interpreting YAML tags such as !!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 runs a system command or reads sensitive files.

Consider this YAML snippet:

!!python/object/apply:os.system ['rm -rf /']

Loading it with an unsafe loader can invoke os.system. Even if your application does not intentionally use such tags, 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 warns against using load() with an unsafe loader on untrusted input, yet many tutorials and legacy code still use it. The safe alternative, safe_load(), restricts construction to standard YAML data types such as dictionaries, lists, strings, numbers, booleans, and None, and refuses to instantiate arbitrary Python objects.

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 safe, standard YAML 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 something outside the supported safe types. This stops the dangerous operation before it runs.

Writing YAML with safe_dump()

When you serialize a Python dictionary or list back to YAML, use yaml.safe_dump(). It works with the same safe, standard YAML types as safe_load(). If you pass an object for which the safe dumper has no representer, such as a custom class instance, 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 flow style:

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-safe objects and keeps the output clean and portable.

Handling Custom Python Objects

If you need to serialize custom class instances, the plain safe methods are not enough. One safe approach is to convert objects to dictionaries before serialization, then reconstruct them 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 custom YAML tags and keeps the serialization format explicit. It also makes the YAML human-readable and language-agnostic.

You can also register explicit representers and constructors with yaml.add_representer() and yaml.add_constructor(), but those registrations must target SafeDumper/SafeLoader or subclasses. This is more complex and requires careful validation. In most applications, converting objects 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. Catch that 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, check for None and default to an empty dictionary:

data = yaml.safe_load(f) if data is None: data = {}

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

The safe loaders and dumpers provide the security guarantee that matters for untrusted input, and the ordinary pure-Python implementations are usually sufficient. PyYAML also provides C-accelerated CSafeLoader and CSafeDumper variants in builds where the optional C extension is compiled. If you use those classes explicitly, remember that they are optional and fall back to SafeLoader/SafeDumper when unavailable.

For most YAML files, choose a safe loader for its security, not for performance micro-optimization.

For applications that need round-trip preservation of comments and formatting, PyYAML is not the best choice. Libraries like ruamel.yaml offer round-trip loading and dumping that preserve comments, but they have 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. The loader that permits arbitrary Python object construction is intentionally named UnsafeLoader; treat that as a deliberate, audited choice.

If you use load(), always pass a Loader argument. Recent PyYAML releases require it, and it forces you to think about which loader you are choosing. 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.

Read and Write YAML Safely with PyYAML | RYUSLOG DEV