Syrupy: Snapshot Testing for pytest — Complete Guide

Syrupy: Snapshot Testing for pytest — Complete Guide

Syrupy is the most popular snapshot testing plugin for pytest. It stores snapshots in __snapshots__/ directories alongside your test files, uses YAML-like .ambr format for human-readable diffs, and integrates with pytest's assertion rewriting for clear failure messages. This guide covers installation, writing snapshot tests, updating snapshots, and handling non-deterministic data.

Key Takeaways

snapshot is a pytest fixture — no imports needed. Just add snapshot to your test function signature and use assert result == snapshot. Syrupy injects it automatically.

Snapshots are stored as .ambr files in __snapshots__/. These files are YAML-like text — human-readable in code review. Commit them.

Update snapshots with --snapshot-update. When behavior changes intentionally, run pytest --snapshot-update to regenerate snapshot files. Review the diff before committing.

First run creates the snapshot automatically. Unlike some tools, Syrupy creates the snapshot on first run and passes (not fails). Subsequent runs compare against it.

Use snapshot.with_name() to give meaningful snapshot names. By default the snapshot key is the test name. Parameterized tests benefit from explicit names.

What Is Syrupy?

Syrupy is a pytest plugin that adds snapshot testing to your test suite. A snapshot test captures the output of your code in a file, then compares against that file on subsequent runs.

Unlike Jest snapshots (JavaScript) or Verify (C#), Syrupy stores snapshots in .ambr files (Amber format) — a YAML-like text format designed to be readable in code review diffs.

Installation

pip install syrupy

No further configuration needed — Syrupy registers its fixtures with pytest automatically.

Basic Snapshot Test

# tests/test_order.py
from myapp.orders import format_order_summary

def test_order_summary_snapshot(snapshot):
    order = {
        "id": "ORD-001",
        "customer": "Alice",
        "items": [{"product": "Widget", "qty": 2, "price": 14.99}],
        "total": 29.98
    }

    result = format_order_summary(order)

    assert result == snapshot

Run it for the first time:

pytest tests/test_order.py

Syrupy creates the snapshot file and the test passes on first run:

# tests/__snapshots__/test_order.ambr
# serializer version: 1
# name: test_order_summary_snapshot
  '''
  Order ORD-001 for Alice
  2x Widget @ $14.99 each
  Total: $29.98
  '''

Future runs compare against this file. Change the code and the test fails with a clear diff.

Snapshot Format for Different Types

Syrupy handles Python objects, not just strings:

def test_user_dict_snapshot(snapshot):
    user = {"id": 1, "name": "Bob", "role": "admin", "active": True}
    assert user == snapshot

def test_list_snapshot(snapshot):
    items = [{"id": i, "value": i * 10} for i in range(3)]
    assert items == snapshot

def test_nested_object_snapshot(snapshot):
    from dataclasses import dataclass

    @dataclass
    class Product:
        id: str
        name: str
        price: float

    product = Product(id="p1", name="Gadget", price=99.99)
    assert product == snapshot

Generated .ambr file:

# name: test_user_dict_snapshot
  {
    'active': True,
    'id': 1,
    'name': 'Bob',
    'role': 'admin',
  }
# name: test_list_snapshot
  [
    {
      'id': 0,
      'value': 0,
    },
    ...
  ]

Updating Snapshots

When behavior changes intentionally, update the snapshots:

# Update all snapshots
pytest --snapshot-update

<span class="hljs-comment"># Update snapshots for a specific test file
pytest tests/test_order.py --snapshot-update

<span class="hljs-comment"># Update snapshot for a specific test
pytest tests/test_order.py::test_order_summary_snapshot --snapshot-update

After updating, review the diff in git:

git diff tests/__snapshots__/

If the diff looks correct, commit it. The updated snapshot becomes the new baseline.

Handling Non-Deterministic Data

Non-deterministic values (timestamps, UUIDs, random IDs) cause snapshots to fail on every run. Handle them by normalizing before snapshotting:

Option 1: Scrub before assertion

import re

def scrub_snapshot(data: dict) -> dict:
    """Remove non-deterministic fields before snapshotting."""
    result = dict(data)
    for key in ['created_at', 'updated_at', 'id']:
        if key in result:
            result[key] = f'<{key.upper()}>'
    return result

def test_order_with_timestamp(snapshot):
    order = OrderService.create({"customer": "Alice", "items": []})

    # Scrub before snapshot
    assert scrub_snapshot(order) == snapshot

Option 2: Freeze time

from freezegun import freeze_time

@freeze_time("2024-01-15 10:00:00")
def test_order_created_at(snapshot):
    order = OrderService.create({"customer": "Alice"})
    # created_at is now deterministic
    assert order == snapshot

Option 3: Use pytest fixtures to control IDs

@pytest.fixture(autouse=True)
def fixed_uuid(monkeypatch):
    counter = itertools.count(1)
    monkeypatch.setattr(uuid, 'uuid4', lambda: f"test-id-{next(counter)}")

def test_with_stable_ids(snapshot):
    result = create_entities(count=3)
    assert result == snapshot  # IDs are now test-id-1, test-id-2, test-id-3

Parameterized Snapshot Tests

Syrupy works well with @pytest.mark.parametrize:

@pytest.mark.parametrize("discount_code,expected_discount", [
    ("SAVE10", 10),
    ("SAVE20", 20),
    ("INVALID", 0),
    (None, 0),
])
def test_discount_application(snapshot, discount_code, expected_discount):
    result = apply_discount(base_price=100.0, code=discount_code)
    assert result == snapshot

Each parameter combination gets its own snapshot entry:

# name: test_discount_application[SAVE10-10]
  {
    'discount_percent': 10,
    'final_price': 90.0,
  }
# name: test_discount_application[None-0]
  {
    'discount_percent': 0,
    'final_price': 100.0,
  }

Custom Snapshot Names

For clearer snapshot files:

def test_complex_report(snapshot):
    report = generate_monthly_report(month="2024-01")

    # Custom name instead of test function name
    assert report == snapshot(name="monthly_report_january_2024")

Testing API Responses

Syrupy works well for HTTP response bodies:

from fastapi.testclient import TestClient
from myapp.main import app

client = TestClient(app)

def test_get_users_response(snapshot):
    response = client.get("/api/users")

    # Normalize non-deterministic fields
    data = response.json()
    for user in data.get("users", []):
        user["created_at"] = "<TIMESTAMP>"
        user["id"] = "<ID>"

    assert {
        "status_code": response.status_code,
        "body": data
    } == snapshot

Detecting Unused Snapshots

Over time, tests get deleted but their snapshots remain. Find unused snapshots:

pytest --snapshot-warn-unused

Delete unused snapshots:

pytest --snapshot-delete-unused

Run this periodically to keep your snapshot files clean.

CI Configuration

Snapshots should never auto-update in CI. Configure the pipeline to fail if snapshots are stale:

# .github/workflows/test.yml
- name: Run snapshot tests
  run: pytest --snapshot-default-extension syrupy.extensions.amber.AmberSnapshotSerializer
  # Do NOT pass --snapshot-update in CI
  # If snapshots don't match, the test fails with a diff

If a developer merges a code change that affects snapshots without updating them, CI catches it.

Syrupy vs. Other Python Snapshot Libraries

Syrupy pytest-snapshot inline-snapshot
pytest integration Native fixture Native Native
File format Amber (.ambr) .txt Inline in source
Object serialization Built-in Manual Built-in
Parameterized test support Limited
Custom serializers Limited
Update command --snapshot-update --snapshot-update --update-snapshots

Syrupy is the most widely used with the most active maintenance. Use it for new projects.

Summary

Syrupy integrates snapshot testing into pytest cleanly:

  • Add snapshot fixture to any test function
  • assert result == snapshot — first run creates the file, subsequent runs compare
  • Update with pytest --snapshot-update when behavior changes intentionally
  • Commit .ambr files — they're human-readable specifications
  • Scrub non-deterministic data (timestamps, IDs) before snapshotting

Start with your most complex output format — the kind you'd otherwise need 20 assertions to verify. One snapshot assertion replaces them all.

Read more