# fast-yaml
[](https://github.com/bug-ops/fast-yaml/actions)
[](https://codecov.io/gh/bug-ops/fast-yaml)
[](https://crates.io/crates/fast-yaml-cli)
[](https://docs.rs/fast-yaml-core)
[](https://pypi.org/project/fastyaml-rs/)
[](https://www.npmjs.com/package/fastyaml-rs)
[](LICENSE-MIT)
**High-performance YAML 1.2.2 parser for Python and Node.js, powered by Rust.**
Near drop-in replacement for PyYAML and js-yaml (exception: `safe_dump(width=..., allow_unicode=...)` raises `TypeError`, see the FAQ). Matches or beats PyYAML C on small/medium files, **2-4x faster** than pure Python, **1.2-1.4x faster** than js-yaml. Full YAML 1.2.2 Core Schema compliance, comprehensive linting, and multi-threaded parallel processing.
> [!IMPORTANT]
> **YAML 1.2.2 Compliance** — Unlike PyYAML (YAML 1.1), `fast-yaml` follows the modern YAML 1.2.2 specification. This means `yes/no/on/off` are strings, not booleans.
## Installation
```bash
# Python
pip install fastyaml-rs
# Node.js
npm install fastyaml-rs
# CLI (prebuilt binary — no Rust toolchain required)
curl -fsSL https://raw.githubusercontent.com/bug-ops/fast-yaml/main/scripts/install.sh | sh
# CLI (from source via cargo)
cargo install fast-yaml-cli
```
> [!WARNING]
> Requires Rust 1.91+, Python 3.10+ or Node.js 22+ (only for building from source — the prebuilt CLI binary has no runtime dependencies)
> [!NOTE]
> `scripts/install.sh` detects the host OS/arch/libc (including musl) and downloads the matching prebuilt `fy` binary with checksum verification from the [GitHub releases](https://github.com/bug-ops/fast-yaml/releases). Linux aarch64 musl (e.g. Alpine on ARM64) has no prebuilt binary yet — use `cargo install fast-yaml-cli` instead. Pin a version with `FASTYAML_VERSION=v0.7.0`, or change the install directory with `FASTYAML_INSTALL_DIR`.
<details>
<summary><b>Build from source</b></summary>
```bash
git clone https://github.com/bug-ops/fast-yaml.git
cd fast-yaml
# Python
uv sync && uv run maturin develop
# Node.js
cd nodejs && npm install && npm run build
```
</details>
## Architecture
fast-yaml is organized as a modular Rust workspace with clear separation of concerns:
```
fast-yaml/
├── crates/fast-yaml-cli/ # fy CLI (parse, format, lint, convert)
├── crates/fast-yaml-core/ # Parser + Emitter + Streaming Formatter
├── crates/fast-yaml-linter/ # Linter + Diagnostic Formatters
├── crates/fast-yaml-parallel/ # Multi-threaded processing
├── python/ # PyO3 bindings
└── nodejs/ # NAPI-RS bindings
```
### Core Components
| Component | Location | Input | Output | Use Case |
|-----------|----------|-------|--------|----------|
| **Parser** | `fast-yaml-core` | YAML text | `Value` (DOM) | Deserialize YAML to data structures |
| **Emitter** | `fast-yaml-core` | `Value` (DOM) | YAML text | Serialize data structures to YAML |
| **Streaming Formatter** | `fast-yaml-core` | Parser events | YAML text | Format YAML without building DOM |
| **Linter** | `fast-yaml-linter` | YAML text | `Vec<Diagnostic>` | Validate YAML against rules |
| **Parallel Processor** | `fast-yaml-parallel` | YAML files/streams | `BatchResult` | Parallel processing at document and file level |
> [!TIP]
> **Parser vs Streaming Formatter**: Parser builds a full DOM (use for data manipulation), Streaming Formatter processes events directly (use for formatting/conversion).
> [!TIP]
> **Linter vs Diagnostic Formatter**: Linter validates YAML and produces diagnostics, Diagnostic Formatter renders them for display (rustc-style text, JSON, GitHub annotations, parsable, SARIF).
### Parallelism Types
| Type | API | Use Case |
|------|-----|----------|
| **Document-level** | `parse_parallel()` | Parse multi-document YAML streams |
| **File-level** | `process_files()`, `FileProcessor` | Process multiple files in parallel |
| **CLI batch mode** | `fy format -j 8 dir/` | Format directories with parallel workers |
> [!NOTE]
> All parallelism is now unified in the `fast-yaml-parallel` crate. CLI batch mode and FFI bindings use this single implementation.
## Quick Start
### Python
```python
import fast_yaml
data = fast_yaml.safe_load("""
name: fast-yaml
features: [fast, safe, yaml-1.2.2]
""")
yaml_str = fast_yaml.safe_dump(data)
```
> [!TIP]
> Migrating from PyYAML? Just change your import: `import fast_yaml as yaml`
### Node.js
```typescript
import { safeLoad, safeDump } from 'fastyaml-rs';
const data = safeLoad(`name: fast-yaml`);
const yamlStr = safeDump(data);
```
### CLI
```bash
# Single file operations
fy parse config.yaml # Validate syntax
fy format -i config.yaml # Format in-place (exits with error if comments detected)
fy format -i --strip-comments config.yaml # Format and strip comments silently
fy format --dry-run config.yaml # Preview: exit 5 if the file would change
fy convert json config.yaml # YAML → JSON
fy lint config.yaml # Lint with diagnostics
fy lint --format github . # CI output: github, sarif or parsable
# Batch mode (directories, globs, multiple files)
fy format -i src/ # Format entire directory
fy format -i "**/*.yaml" # Format with glob pattern
fy format -i -j 8 project/ # Parallel processing (8 workers)
fy lint --exclude "tests/**" . # Lint all except tests
fy lint --max-input-bytes 10MiB big.yaml # Reject inputs over 10 MiB (KiB/MiB/GiB suffixes)
```
Every `fy` command rejects inputs over 100 MiB by default, like the library and bindings; `--max-input-bytes` (alias `--max-input-size`) raises or lowers it. `fy lint` also reads the limit from `max-input-bytes: <integer bytes>` in `.fast-yaml.yaml` (fast-yaml-only key, no size suffixes; leave it out of files shared with yamllint); the flag overrides the config key.
`fy lint` reads yamllint-style configuration: `extends: default|relaxed|<file>`, yamllint rule names (`trailing-spaces`, `key-duplicates`, `anchors`), per-rule `ignore`/`ignore-from-file`, `locale: C` and the `indentation` options `spaces`, `indent-sequences` and `check-multi-line-strings`. `indentation`, `quoted-strings`, `key-ordering` and `empty-values` are checked against yamllint 1.38 output; remaining differences are listed in `specs/003-lint/spec.md`.
> [!TIP]
> Batch mode activates automatically for directories, globs, or multiple files. Supports parallel processing, include/exclude patterns, and respects `.gitignore`. A run that finds no YAML files exits 1.
> [!WARNING]
> `fy format --dry-run` exits with code 5 when any file would change (1 = a file failed, takes precedence).
> [!WARNING]
> `fy format` does **not** preserve YAML comments. If the input contains comments, the command exits with an error (exit code 1). Pass `--strip-comments` to acknowledge this and allow formatting to proceed — comments will be removed from the output.
## Features
- **High Performance** — Matches PyYAML C on small/medium files, 2-4x faster than pure Python
- **YAML 1.2.2** — Full Core Schema compliance
- **Familiar API** — PyYAML/js-yaml-shaped (`safe_dump` no longer accepts `width` or `allow_unicode`)
- **Batch Processing** — Multi-file operations with parallel workers, glob patterns, .gitignore support
- **Linting** — Rich diagnostics with line/column tracking
- **Parallel** — Multi-threaded processing for large files
- **Safe** — Memory-safe Rust with minimal `unsafe` (FFI boundaries only, explicitly documented)
> [!TIP]
> Parallel processing provides 3-6x speedup on 4-8 core systems for multi-document files.
<details>
<summary><b>Feature details</b></summary>
### Linting
```python
from fast_yaml.lint import lint
diagnostics = lint("key: value\nkey: duplicate")
for diag in diagnostics:
print(f"{diag.severity}: {diag.message} at line {diag.span.start.line}")
```
### Inline Lint Directives
Suppress diagnostics from within the YAML file (all surfaces: CLI, Python, Node.js):
```yaml
# fy: disable-file
```
`disable-file` suppresses the whole file. It must be an own-line comment before any YAML content; blank lines, other comments and `%YAML`/`%TAG` lines may precede it, `---` may not. Other verbs:
```yaml
# fy: disable line-length trailing-whitespace
long: value
# fy: enable line-length
a: 1
a: 2 # fy: disable-line duplicate-key
# fy: disable-line duplicate-key
a: 3
```
`disable` / `enable` open and close a block and must be own-line comments (inline ones are rejected with a warning); an inline `disable-line` covers its own line, a full-line one covers the next line. Text after the rule names (`# fy: disable x # note`) is reported as a `lint-directive` warning; the names before it still apply, and a directive with no names plus trailing text is ignored. A block `disable` carries across `---` into following documents.
- Verbs: `disable`, `enable`, `disable-line`, `disable-file`; with no rule names they apply to all rules. The `rule:` prefix on names is optional (`rule:line-length`).
- `# yamllint ...` is accepted as an alias for `# fy: ...`; yamllint names map to fast-yaml codes (`key-duplicates` -> `duplicate-key`, `anchors` -> `invalid-anchor` + `undefined-alias`, `trailing-spaces` -> `trailing-whitespace`).
- Unknown or malformed rule names, unknown `fy:` verbs, trailing text and misplaced `disable-file` produce one `lint-directive` warning per directive comment (severity and enablement are configurable like any rule, but a directive cannot suppress it). A directive whose names are all unknown suppresses nothing.
- Syntax errors are never suppressed.
**Limitations**
- Matching is by the line where a diagnostic's span starts. An inline `disable-line` on the last line of a multi-line scalar or flow collection misses diagnostics reported on its first line.
- Diagnostics inside block scalar bodies cannot carry a comment; use `disable` / `enable` around them.
- `duplicate-key` is reported on the second occurrence of the key.
- `document-start` reports at line 1 (`1:1`), so a full-line `disable-line` on line 1 (which targets line 2) misses it; use `disable` on line 1 or `disable-file`.
- `lint-directive` is config-only and cannot be named in a directive.
### Parallel Processing (Document-Level)
```python
from fast_yaml._core import parallel
# Parse ONE file with MULTIPLE documents in parallel
multi_doc_yaml = "---\nfoo: 1\n---\nbar: 2\n---\nbaz: 3"
config = parallel.ParallelConfig(thread_count=4, max_input_bytes=100*1024*1024)
docs = parallel.parse_parallel(multi_doc_yaml, config) # 3 documents parsed in parallel
```
> [!NOTE]
> This is document-level parallelism (parsing documents inside one file). For file-level parallelism (processing multiple files), use CLI batch mode: `fy format -i -j 8 directory/`
</details>
## Performance
> [!NOTE]
> Three separate benchmark suites: **Python API** (vs PyYAML), **Node.js API** (vs js-yaml), and **CLI Batch Mode** (vs yamlfmt).
> [!NOTE]
> Process startup overhead (~15ms for Python, ~20-25ms for Node.js) affects small file benchmarks. In long-running servers (persistent processes), speedups would be 2-4x higher.
> [!TIP]
> Batch mode is where fast-yaml excels with parallel processing. Use `-j` to specify worker count.
<details>
<summary><b>Benchmark results</b></summary>
### Python API vs PyYAML
**Parse (loading):**
| File Size | fast-yaml | PyYAML (C) | PyYAML (pure) | vs C | vs pure |
|-----------|-----------|------------|---------------|------|---------|
| Small (502B) | **15.5 ms** | 20.2 ms | 20.8 ms | **1.30x** | **1.34x** |
| Medium (44KB) | **26.3 ms** | 26.4 ms | 61.2 ms | **1.00x** | **2.33x** |
| Large (449KB) | 130.3 ms | **79.3 ms** | 429.6 ms | 0.61x | **3.30x** |
**Dump (serialization):**
| File Size | fast-yaml | PyYAML (C) | PyYAML (pure) | vs C | vs pure |
|-----------|-----------|------------|---------------|------|---------|
| Small (502B) | **15.7 ms** | 20.8 ms | 21.2 ms | **1.33x** | **1.35x** |
| Medium (44KB) | **31.6 ms** | 31.7 ms | 82.7 ms | **1.00x** | **2.62x** |
| Large (449KB) | 177.6 ms | **131.1 ms** | 653.8 ms | 0.74x | **3.68x** |
**Key findings:**
- **Small/Medium files**: fast-yaml matches or beats PyYAML C (1.0-1.3x speedup)
- **Pure Python**: fast-yaml consistently 1.3-3.7x faster across all sizes
- **Large files**: PyYAML C optimized for single large files; use fast-yaml's parallel mode for multi-document streams
Full benchmarks: [benches/comparison](benches/comparison/)
### Node.js API vs js-yaml (Apple M3 Pro, 12 cores)
**Parse (loading):**
| File Size | fast-yaml | js-yaml | Speedup |
|-----------|-----------|---------|---------|
| Small (502B) | **24.4 ms** | 28.1 ms | **1.15x** |
| Medium (44KB) | **26.2 ms** | 31.9 ms | **1.22x** |
| Large (449KB) | **40.4 ms** | 48.3 ms | **1.20x** |
**Dump (serialization):**
| File Size | fast-yaml | js-yaml | Speedup |
|-----------|-----------|---------|---------|
| Small (502B) | **24.1 ms** | 29.3 ms | **1.22x** |
| Medium (44KB) | **27.1 ms** | 34.9 ms | **1.29x** |
| Large (449KB) | **50.7 ms** | 72.1 ms | **1.42x** |
**Key findings:**
- **Consistent advantage**: fast-yaml 1.15-1.42x faster across all scenarios
- **Best performance**: Large file dump operations (1.42x speedup)
- **V8 JIT competitive**: js-yaml benefits from TurboFan optimization, reducing speedup vs pure Python
- **Real-world servers**: In persistent processes without startup overhead, expect 2-4x speedup
### CLI Single-File vs yamlfmt (Apple M3 Pro, 12 cores)
| File Size | fast-yaml | yamlfmt | Result |
|-----------|-----------|---------|--------|
| Small (502 bytes) | **1.7 ms** | 3.1 ms | **1.80x faster** |
| Medium (45 KB) | **2.5 ms** | 2.9 ms | **1.19x faster** |
| Large (460 KB) | 8.4 ms | **2.9 ms** | yamlfmt 2.88x faster |
### CLI Batch Mode vs yamlfmt
| Workload | fast-yaml (parallel) | yamlfmt (sequential) | Speedup |
|----------|---------------------|----------------------|---------|
| 50 files (26 KB) | **4.3 ms** | 10.3 ms | **2.40x faster** |
| 200 files (204 KB) | **8.0 ms** | 52.7 ms | **6.63x faster** |
| 500 files (1 MB) | **15.5 ms** | 244.7 ms | **15.77x faster** |
| 1000 files (1 MB) | **23.4 ms** | 323.4 ms | **13.80x faster** |
**Key takeaway:** Batch mode with parallel workers provides 6-15x speedup on multi-file operations, making it ideal for formatting entire codebases.
```bash
# Run benchmarks
bash benches/comparison/scripts/run_python_benchmark.sh # Python API
bash benches/comparison/scripts/run_nodejs_benchmark.sh # Node.js API
bash benches/comparison/scripts/run_batch_benchmark.sh # CLI batch mode
```
**Test environment:** macOS 14, Apple M3 Pro (12 cores), fast-yaml 0.6.4, PyYAML 6.0.3, js-yaml 4.1.1, Node.js 25.2.1, yamlfmt 0.21.0
</details>
## YAML 1.2.2 Differences
<details>
<summary><b>Differences from PyYAML (YAML 1.1)</b></summary>
| Feature | PyYAML (YAML 1.1) | fast-yaml (YAML 1.2.2) |
|---------|-------------------|------------------------|
| `yes/no` | `True/False` | `"yes"/"no"` (strings) |
| `on/off` | `True/False` | `"on"/"off"` (strings) |
| `014` (octal) | `12` | `14` (decimal) |
| `0o14` (octal) | Error | `12` |
```python
fast_yaml.safe_load("yes") # "yes" (string, not True!)
fast_yaml.safe_load("0o14") # 12 (octal)
fast_yaml.safe_load("014") # 14 (decimal, NOT octal!)
```
</details>
## API Reference
<details>
<summary><b>Loading YAML</b></summary>
```python
# Single document
data = fast_yaml.safe_load(yaml_string)
# Multiple documents
for doc in fast_yaml.safe_load_all(yaml_string):
print(doc)
# PyYAML-compatible
data = fast_yaml.load(yaml_string, Loader=fast_yaml.SafeLoader)
```
</details>
<details>
<summary><b>Dumping YAML</b></summary>
```python
yaml_str = fast_yaml.safe_dump(data)
# With options
yaml_str = fast_yaml.dump(
data,
indent=2,
explicit_start=True,
sort_keys=False,
)
# Multiple documents
yaml_str = fast_yaml.safe_dump_all([doc1, doc2, doc3])
```
</details>
<details>
<summary><b>Type mappings</b></summary>
| YAML Type | Python Type |
|-----------|-------------|
| `null`, `~` | `None` |
| `true`, `false` | `bool` |
| `123`, `0x1F`, `0o17` | `int` |
| `1.23`, `.inf`, `.nan` | `float` |
| `"string"`, `'string'` | `str` |
| `[a, b, c]` | `list` |
| `{a: 1, b: 2}` | `dict` |
Floats are written with a dot and a signed exponent so YAML 1.1 readers such as PyYAML read them as floats (`1e300` becomes `1.0e+300`).
</details>
## Security
Input validation prevents denial-of-service attacks.
<details>
<summary><b>Security limits</b></summary>
| Limit | Default | Configurable |
|-------|---------|--------------|
| Max input size | 100 MiB | Yes (1 byte to 1 GiB; `fy --max-input-bytes`, `fy lint` config key `max-input-bytes`) |
| Max documents | 100,000 | Yes (up to 10M) |
| Max threads | 128 | Yes |
| Max nesting depth | 256 | Yes (1 to 512, `--max-depth`); flow collections (`[]`, `{}`) stop at 255 levels |
| Max alias expansion | 64 MiB | Yes (1 byte to 1 GiB, `--max-alias-bytes`) |
| Max parser scan-ahead | 4 Mi characters | Yes (1 to 1 Gi, `--max-scan-ahead`, `fy lint` config key `max-scan-ahead`) |
</details>
## Project
<details>
<summary><b>Project structure</b></summary>
```
fast-yaml/
├── crates/
│ ├── fast-yaml-cli/ # fy command-line tool
│ ├── fast-yaml-core/ # Core YAML parser/emitter
│ ├── fast-yaml-linter/ # Linting engine
│ └── fast-yaml-parallel/ # Multi-threaded processing
├── python/ # PyO3 Python bindings
├── nodejs/ # NAPI-RS Node.js bindings
└── Cargo.toml # Workspace manifest
```
</details>
<details>
<summary><b>Technology stack</b></summary>
| Component | Library |
|-----------|---------|
| YAML Parser | [saphyr-parser](https://github.com/saphyr-rs/saphyr) |
| Python Bindings | [PyO3](https://pyo3.rs/) |
| Node.js Bindings | [NAPI-RS](https://napi.rs/) |
| Parallelism | [Rayon](https://github.com/rayon-rs/rayon) |
**Rust 2024 Edition** • **Python 3.10+** • **Node.js 22+**
</details>
## Contributing
Contributions welcome! All PRs must pass CI checks:
```bash
cargo +nightly fmt --check
cargo clippy --all-targets --all-features --workspace -- -D warnings
cargo nextest run --workspace --all-features --exclude fast-yaml --exclude fast-yaml-nodejs --lib --bins
```
The repository also ships a [mise](https://mise.jdx.dev) configuration for development: `mise install --locked` installs the pinned dev tools and `mise run ci` runs the checks CI runs. It is only for working on this repository; using `fy` or the Python and Node.js packages does not require it. See [CONTRIBUTING.md](CONTRIBUTING.md) for the task list.
## FAQ
<details>
<summary><b>Why not just use PyYAML?</b></summary>
PyYAML is excellent. Use fast-yaml when you need performance (2-4x faster than pure-Python PyYAML, on par with PyYAML C on small files), YAML 1.2.2 compliance, built-in linting, or parallel processing.
</details>
<details>
<summary><b>Is this a drop-in replacement?</b></summary>
For most `safe_*` calls, yes: change `import yaml` to `import fast_yaml as yaml`. Not for every call: `safe_dump(width=..., allow_unicode=...)` (and `dump`, `safe_dump_all`) now raises `TypeError`, because the emitter never wrapped lines or escaped unicode; drop those arguments. Also, `default_flow_style=None` writes block style. Note that YAML 1.2.2 has different boolean/octal handling.
</details>
<details>
<summary><b>When should I use parallel processing?</b></summary>
**Document-level parallelism** (`parse_parallel()` in Python/Node.js):
- Use for single large files with multiple `---` separated documents
- File size > 1MB with dozens/hundreds of documents
- Example: Log files, data dumps
**File-level parallelism** (CLI batch mode):
- Use for processing multiple separate files
- Example: `fy format -i -j 8 src/` for entire directories
For single-document files, use `safe_load()`.
</details>
## License
Licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at your option.