syrupy: pytest snapshots that fail when the snapshot is missing
:pancakes: The most popular pytest snapshot plugin
At a glance
- What is it?
- syrupy is a zero-dependency pytest plugin that lets a test assert that a computed result matches a stored snapshot, and its distinguishing rule is that a missing snapshot fails the test rather than silently creating one. It is MIT licensed, supports Python 3.11 upward, and its maintainer handles the day to day work.
- Who is it for?
- syrupy fits a Python team that wants snapshot testing without a dependency tree and wants the missing snapshot to be an error rather than a silent write, and the amber format it uses is readable enough to review in a diff. It is the wrong choice if you need to snapshot very large or binary payloads, because the format is human-oriented text, and it is the wrong choice if your team cannot review a file per test that changes whenever the output changes.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The assertion reads like an ordinary equality check
The core usage is three lines, and the reason the plugin reads well is that the snapshot is used as a value on the right of an equality assertion rather than as an object you call a method on.
In a pytest test file test_file.py:
def test_foo(snapshot):
actual = "Some computed value!"
assert actual == snapshotThe project documentation states the intent directly in its principles, with the idiomatic example given as assert x == snapshot against snapshot.assert_match(x). That comparison matters, because the other style is what the plugin is positioning itself against.
The second principle is soundness, and it is the one that makes syrupy different from the snapshot libraries it names in its topic list, which include snapshottest. The documentation says that unlike other snapshot libraries, Syrupy will fail a test suite if a snapshot does not exist, not just on snapshot differences.
That single rule removes the most common failure mode of snapshot testing. In a plugin that creates a missing snapshot on first run, a test that has never been meaningfully executed looks identical to a test that passes, and the difference only shows up later, when the output changes and the absence of a baseline means there is nothing to compare against. Syrupy makes the first run fail, which means the snapshot has to be created deliberately, with the update flag, and reviewed.
The third principle is extensibility: if a particular data type is not supported, users should be able to easily and quickly add support. That matters because the set of types a project actually serialises is usually wider than anyone anticipated, and the cost of that support being in the plugin rather than in your test code is the difference between a library you can use and one you have to fork.
The update flag is how snapshots get created, and it is deliberately explicit
The workflow the documentation describes is a three step loop, and the middle step is where the design decision shows.
Run pytest and the test fails because the snapshot does not exist. Then re-run pytest with the update snapshots flag:
pytest --snapshot-updateAnd a snapshot file is generated under a __snapshots__ directory in the same directory as test_file.py. The documentation says the __snapshots__ directory and all its children should be committed along with your test code.
That last instruction is the operational contract. A snapshot test without its snapshot file committed is a test that will fail in CI for a reason that has nothing to do with the code, and the whole model depends on those files being in version control next to the tests that use them.
There is a second update invocation, and it addresses a real annoyance. Syrupy automatically marks tests that directly or indirectly use the snapshot fixture with syrupy_snapshot. To update snapshots without running unrelated tests, combine the marker with the update option:
pytest --snapshot-update -m syrupy_snapshotThe marker is auto-applied, so you do not have to remember to add it, but you can select on it. That matters on a large suite where regenerating every snapshot to fix one is both slow and produces a diff full of noise, because an unrelated snapshot that happens to differ would be silently rewritten along with the one you meant to change.
So the creation of a baseline is always a separate, explicit act from running the tests, and it can be scoped to the tests that use snapshots. Both of those are small design choices with a disproportionate effect on whether snapshot testing stays trustworthy in a codebase.
Serialisation falls back to repr, and you can override it per class
The default serializer supports all Python built-in types and provides a sensible default for custom objects, and the documentation then explains how to change that default.
If you need to customise your object snapshot, it is as easy as overriding the default __repr__ implementation:
def __repr__(self) -> str:
return "MyCustomClass(...)"That is a smaller intervention than it sounds, because it says the snapshot of an object is derived from its repr, and the repr is already the thing Python shows you when you print an object. Overriding it makes the snapshot readable and makes it stable, which is the second requirement.
If you need to bypass a custom object representation to use the amber standard, the documentation gives a helper:
def test_object_as_named_tuple(snapshot):
assert snapshot == AmberDataSerializer.object_as_named_tuple(obj_with_custom_repr)The amber standard is the default output format, and this helper converts an object into the same shape the standard uses for a named tuple, so a class with a custom repr can be compared against the plain data representation instead.
The next problem is the one that catches most teams. If a class has attributes you do not want in the snapshot, there are three documented ways to handle it, and the documentation is explicit that all three generate equivalent snapshots.
The first is a filter function passed to the snapshot exclude configuration option, and the second is the same thing with the include option:
def limit_foo_attrs(prop, path):
allowed_foo_attrs = {"only", "serialize", "these", "attrs"}
return isinstance(path[-1][1], Foo) and prop in allowed_foo_attrs
def test_bar(snapshot):
actual = Foo(...)
assert actual == snapshot(include=limit_foo_attrs)The third is overriding the __dir__ implementation on the class to control the attribute list, and the documentation notes the caveat that the latter is only viable when you have control over the class implementation and do not need to share the exclusion logic with other objects.
The snapshot fixture does not work in unittest.TestCase
There is one documented incompatibility, and it is a real one for Django users.
Due to limitations in unittest and pytest, the snapshot fixture is not directly usable in TestCase subclasses, including Django's TestCase. The documentation links the pytest page explaining the limitation.
Two workarounds are given. The first wraps the snapshot fixture in a pytest fixture that attaches it to the test class instance:
from unittest import TestCase
import pytest
@pytest.fixture(scope="function")
def snapshot_in_class(request, snapshot):
"""
Wraps snapshot fixture to provide instance snapshot property for
unittest.TestCase tests
"""
request.cls.snapshot = snapshot
class MyTest(TestCase):
@pytest.mark.usefixtures("snapshot_in_class")
def test_foo(self):
actual = "Some computed value!"
assert actual == self.snapshotThe second uses a fixture with autouse to do the same assignment without a separate helper function:
from unittest import TestCase
import pytest
class MyTest(TestCase):
@pytest.fixture(autouse=True)
def setupSnapshot(self, snapshot):
self.snapshot = snapshot
def test_foo(self):
actual = "Some computed value!"
assert actual == self.snapshotBoth are a few lines of boilerplate per test class. The trade-off is that in a Django project with a large unittest-style suite, you are adding a pytest fixture into a class that is otherwise not a pytest test, and the autouse form has to be repeated in every class that needs it unless you put it in a shared base class or a conftest.
That is a real cost for a specific stack, and it is the kind of thing worth checking before you adopt the plugin for a Django codebase rather than after.
Zero dependencies and a pinned pytest floor
syrupy is described as a zero-dependency pytest plugin, and the package manifest confirms it in the most literal way possible. The dependencies field contains exactly one entry, pytest (>= 8.0.0), and that is the plugin host rather than a library it pulls in. There is no serialization library, no diffing library, no colour library.
The manifest is otherwise conventional and modern. The build backend is hatchling, with a src layout where the wheel source maps src/syrupy to syrupy, and the sdist includes the conftest, the source and the tests. The plugin registers itself through the pytest11 entry point group, with the entry point syrupy = "syrupy", which is the standard way a pytest plugin declares itself without the user configuring anything.
The Python floor is worth noticing, because it is higher than the compatibility table in the README suggests for the current release line. The manifest says requires-python is >= 3.11, and the classifiers list 3.11 through 3.15. The README table is a broader historical view, with the 6.x.x row marked as latest and giving >=3.10 with pytest >=8, and rows for older major versions going back to 2.x.x.
The compatibility table also shows the pytest side, and this is the more disruptive axis. The 6.x.x line needs pytest 8 or newer, the 5.x.x line needs pytest 8 but below 9, the 4.x.x line needs pytest 7 to below 9, and the 3.x.x and 2.x.x lines need pytest 5.1 up to below 8.
The documentation states the policy directly: Syrupy will always be compatible with the latest version of Python and Pytest, and if you are running an old version of Python or Pytest you will need to use an older major version of Syrupy. In practice that means a team on pytest 7 is on syrupy 4.x, and a team that upgrades pytest to 9 has to move to syrupy 6.x at the same time. The compatibility table is the thing to read before pinning.
One maintainer, a sponsor link, and a community around it
The project states its own maintenance arrangement, which is unusual to see written plainly. Day-to-day maintenance, issue triage and development are led by the maintainer, Noah, with contributions welcome and a GitHub Sponsors link offered for anyone who finds the project useful. The README is signed by Noah with the handle @noahnu.
The contributor count is given as over 43 members of the open source community who have contributed code, reported issues, or provided other feedback, and the project description calls itself the most popular pytest snapshot plugin, with a pancakes emoji attached.
The repository layout shows a project that takes its own tooling seriously. There is a benchmarks directory, which is where you would look if you wanted to compare snapshot performance, a mypy.ini for static typing, a codecov configuration, a devcontainer, a pre-commit setup through py-githooks, an .all-contributorsrc, a tasks directory and a script directory, plus prettier and ruff configuration for formatting and linting. The dependency groups are well organised too, with a test group carrying invoke, coverage, hypothesis, pytest-benchmark, pytest-xdist, pydantic and attrs, and a dev group carrying mypy, py-githooks, debugpy and ruff.
That test group is the interesting one. Hypothesis is a property-based testing library, and having it as a direct test dependency suggests the snapshot machinery is itself tested against generated inputs rather than only against hand written cases. pytest-benchmark suggests the performance claims, if any, are at least measured. attrs and pydantic together indicate the serialisers are tested against the two most common ways Python objects are declared.
The release record is current. The recent versions are v6.1.1 on 2026-09-13, v6.0.0 on 2026-08-22 and v5.5.3 on 2026-07-11, and the last push to the repository was on 2026-10-18.
Where snapshot testing is the wrong tool
Two situations rule it out, and one of them is a property of snapshot testing rather than of this plugin.
The first is output that is not stable. A snapshot test is a bet that the value will be the same next time. If the computed result contains a timestamp, a random identifier, a hash of memory layout, a set iteration order or a floating point rounding difference, the snapshot will churn and the test will train people to update snapshots without reading them, which is the failure mode that makes snapshot suites worthless. Syrupy does not fix this and cannot, because the issue is in the value being asserted rather than in the plugin.
The second is very large or binary payloads. The amber format is designed to be read and reviewed by a human in a diff, which is the right trade for a config file or a rendered template, and the wrong trade for a 40 megabyte binary blob or a parquet file. The format is text-oriented, and the extension points are about choosing a representation and filtering attributes, not about streaming large blobs into a store.
There is also the cost of the review, which is real and which the plugin's design makes unavoidable rather than optional. Every snapshot file is a new file in the repository that has to be read before it is committed, and a change in output produces a diff that a reviewer has to interpret. For a small number of stable, meaningful outputs, that is cheap and worth it. For a hundred tests each producing a page of text, the review cost exceeds the value, and asserting on specific properties is the better tool.
The right question is not whether syrupy is good but whether snapshot assertions are the right level of specificity for the thing you are testing. A snapshot tells you that something changed. It does not tell you that what changed is correct, and the plugin's strict rule about missing snapshots is a guard against one specific way of fooling yourself, not a substitute for reading the diff.
Editorial conclusion
syrupy fits a Python team that wants snapshot testing without a dependency tree and wants the missing snapshot to be an error rather than a silent write, and the amber format it uses is readable enough to review in a diff. It is the wrong choice if you need to snapshot very large or binary payloads, because the format is human-oriented text, and it is the wrong choice if your team cannot review a file per test that changes whenever the output changes. Before adopting it, read the compatibility table in the project documentation, because the supported Python and pytest floors move between major versions, and check that the object types you serialise have a stable representation, since the whole scheme depends on the snapshot being deterministic.
Frequently asked questions
What does syrupy do and how is it used?
It is a zero-dependency pytest plugin that lets a test assert that a computed result matches a stored snapshot, written as assert actual == snapshot rather than a method call on a snapshot object. The snapshot file is generated under a __snapshots__ directory next to the test file and should be committed with the test code.
How do I create or update snapshots in syrupy?
Run pytest with the update flag, which is pytest --snapshot-update. A missing snapshot fails the test by design, so the first run of a new test always fails until you update deliberately. Tests using the snapshot fixture are marked syrupy_snapshot automatically, so you can combine the marker with the update option to regenerate only those.
Can I use syrupy with unittest.TestCase or Django tests?
Not directly. Due to limitations in unittest and pytest, the snapshot fixture is not directly usable in TestCase subclasses, including Django's TestCase. The documentation gives two workarounds, a fixture that assigns the snapshot to the test class instance, and an autouse fixture doing the same.
How do I control which attributes of a custom class are serialized?
There are three documented options that generate equivalent snapshots: a filter function passed to the snapshot include or exclude configuration option, and overriding the __dir__ implementation on the class. The class approach only works when you control the class and do not need to share the exclusion logic with other objects.
Which Python and pytest versions does syrupy support?
The package manifest requires Python 3.11 or newer and lists pytest 8 or newer as the only dependency. The compatibility table in the documentation maps older major versions: 5.x.x needs Python 3.10 and pytest 8 but below 9, 4.x.x needs Python 3.8.1 and pytest 7 to below 9, and the 3.x.x and 2.x.x lines need pytest 5.1 to below 8.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/syrupy-project-syrupy)Was this useful?