codecov[bot]: # [Codecov](https://codecov.io/gh/tophat/syrupy/pull/95?src=pr&el=h1) Report
> Merging [#95](https://codecov.io/gh/tophat/syrupy/pull/95?src=pr&el=desc) into [master](https://codecov.io/gh/tophat/syrupy/commit/cb17855e01fa79ffe49b7114a4b5cf1b960509b3?src=pr&el=desc) will **decrease** coverage by `0.13%`.
> The diff coverage is `100%`.

```diff
@@            Coverage Diff             @@
##           master      #95      +/-   ##
==========================================
- Coverage     100%   99.86%   -0.14%     
==========================================
  Files          16       16              
  Lines         714      720       +6     
==========================================
+ Hits          714      719       +5     
- Misses          0        1       +1
```

diff --git a/README.md b/README.md
index db9bdcb..f0c8c56 100644
--- a/README.md
+++ b/README.md
@@ -55,32 +55,30 @@ These are the cli options exposed to `pytest` by the plugin.
 | `--snapshot-update`      | When supplied updates existing snapshots of any run tests, as well as deleting unused and generating new snapshots.          |
 | `--snapshot-warn-unused` | Syrupy default behaviour is to fail the test session when there any unused snapshots. This instructs the plugin not to fail. |
 
-### Serializers
+### Syrupy Extensions
 
-Syrupy comes with a few built-in serializers for you to choose from. You should also feel free to extend the AbstractSnapshotSerializer if your project has a need not captured by one our built-ins.
+Syrupy comes with a few built-in preset configurations for you to choose from. You should also feel free to extend the `AbstractSyrupyExtension` if your project has a need not captured by one our built-ins.
 
-- **`AmberSnapshotSerializer`**: This is the default serializer which generates `.ambr` files. Serialization of most data types are supported, however non-sortable types such as frozenset are experimental.
-- **`RawSingleSnapshotSerializer`**: Unlike the `AmberSnapshotSerializer`, which groups all tests within a single test file into a singular snapshot file, the Raw Single serializer creates one `.raw` file per test case.
-- **`PNGSnapshotSerializer`**: An extension of the Raw Single serializer, this should be used to produce `.png` files.
-- **`SVGSnapshotSerializer`**: Another extension of Raw Single. This produces `.svg` files from an svg string.
+- **`AmberSnapshotExtension`**: This is the default extension which generates `.ambr` files. Serialization of most data types are supported, however non-sortable types such as frozenset are experimental.
+- **`SingleFileSnapshotExtension`**: Unlike the `AmberSnapshotExtension`, which groups all tests within a single test file into a singular snapshot file, and creates one `.raw` file per test case.
+- **`PNGSnapshotExtension`**: An extension of single file, this should be used to produce `.png` files.
+- **`SVGSnapshotExtension`**: Another extension of single file. This produces `.svg` files from an svg string.
 
-### Advanced Usage, Plugin Support
+### Advanced Usage, Extending Syrupy
 
 ```python
 import pytest
 
 @pytest.fixture
 def snapshot_custom(snapshot):
-    return snapshot.with_class(
-        serializer_class=CustomSerializerClass,
-    )
+    return snapshot.use_extension(CustomExtensionClass)
 
 def test_image(snapshot_custom):
     actual = "..."
     assert actual == snapshot_custom
 ```
 
-`CustomSerializerClass` should extend `syrupy.serializers.base.AbstractSnapshotSerializer`.
+`CustomExtensionClass` should extend `syrupy.extensions.base.AbstractSyrupyExtension`.
 
 ## Uninstalling
 
diff --git a/src/syrupy/__init__.py b/src/syrupy/__init__.py
index 3cf21f3..1836904 100644
--- a/src/syrupy/__init__.py
+++ b/src/syrupy/__init__.py
@@ -7,8 +7,8 @@ from typing import (
 import pytest
 
 from .assertion import SnapshotAssertion
+from .extensions import DEFAULT_EXTENSION
 from .location import TestLocation
-from .serializers import DEFAULT_SERIALIZER
 from .session import SnapshotSession
 from .terminal import (
     green,
@@ -99,7 +99,7 @@ def pytest_sessionfinish(session: Any, exitstatus: int) -> None:
 def snapshot(request: Any) -> "SnapshotAssertion":
     return SnapshotAssertion(
         update_snapshots=request.config.option.update_snapshots,
-        serializer_class=DEFAULT_SERIALIZER,
+        extension_class=DEFAULT_EXTENSION,
         test_location=TestLocation(request.node),
         session=request.session._syrupy,
     )
diff --git a/src/syrupy/assertion.py b/src/syrupy/assertion.py
index 64b2572..ec43635 100644
--- a/src/syrupy/assertion.py
+++ b/src/syrupy/assertion.py
@@ -14,14 +14,14 @@ from .exceptions import SnapshotDoesNotExist
 
 if TYPE_CHECKING:
     from .location import TestLocation
-    from .serializers.base import AbstractSnapshotSerializer
+    from .extensions.base import AbstractSyrupyExtension
     from .session import SnapshotSession
     from .types import SerializableData, SerializedData  # noqa: F401
 
 
 @attr.s
 class AssertionResult(object):
-    snapshot_filepath: str = attr.ib()
+    snapshot_location: str = attr.ib()
     snapshot_name: str = attr.ib()
     asserted_data: Optional["SerializedData"] = attr.ib()
     recalled_data: Optional["SerializedData"] = attr.ib()
@@ -40,7 +40,7 @@ class AssertionResult(object):
 class SnapshotAssertion:
     name: str = attr.ib(default="snapshot")
     _session: "SnapshotSession" = attr.ib(kw_only=True)
-    _serializer_class: Type["AbstractSnapshotSerializer"] = attr.ib(kw_only=True)
+    _extension_class: Type["AbstractSyrupyExtension"] = attr.ib(kw_only=True)
     _test_location: "TestLocation" = attr.ib(kw_only=True)
     _update_snapshots: bool = attr.ib(kw_only=True)
     _executions: int = attr.ib(init=False, default=0, kw_only=True)
@@ -52,12 +52,12 @@ class SnapshotAssertion:
         self._session.register_request(self)
 
     @property
-    def serializer(self) -> "AbstractSnapshotSerializer":
-        if not getattr(self, "_serializer", None):
-            self._serializer: "AbstractSnapshotSerializer" = self._serializer_class(
+    def extension(self) -> "AbstractSyrupyExtension":
+        if not getattr(self, "_extension", None):
+            self._extension: "AbstractSyrupyExtension" = self._extension_class(
                 test_location=self._test_location
             )
-        return self._serializer
+        return self._extension
 
     @property
     def num_executions(self) -> int:
@@ -67,13 +67,13 @@ class SnapshotAssertion:
     def executions(self) -> Dict[int, AssertionResult]:
         return self._execution_results
 
-    def with_class(
-        self, serializer_class: Optional[Type["AbstractSnapshotSerializer"]] = None,
+    def use_extension(
+        self, extension_class: Optional[Type["AbstractSyrupyExtension"]] = None,
     ) -> "SnapshotAssertion":
         return self.__class__(
             update_snapshots=self._update_snapshots,
             test_location=self._test_location,
-            serializer_class=serializer_class or self._serializer_class,
+            extension_class=extension_class or self._extension_class,
             session=self._session,
         )
 
@@ -83,13 +83,13 @@ class SnapshotAssertion:
     def get_assert_diff(self, data: "SerializableData") -> List[str]:
         assertion_result = self._execution_results[self.num_executions - 1]
         snapshot_data = assertion_result.recalled_data
-        serialized_data = self.serializer.serialize(data)
+        serialized_data = self.extension.serialize(data)
         if snapshot_data is None:
             return [gettext("Snapshot does not exist!")]
 
         diff: List[str] = []
         if not assertion_result.success:
-            diff.extend(self.serializer.diff_lines(serialized_data, snapshot_data))
+            diff.extend(self.extension.diff_lines(serialized_data, snapshot_data))
         return diff
 
     def __repr__(self) -> str:
@@ -106,15 +106,15 @@ class SnapshotAssertion:
         snapshot_data: Optional["SerializedData"] = None
         serialized_data: Optional["SerializedData"] = None
         try:
-            snapshot_filepath = self.serializer.get_filepath(self.num_executions)
-            snapshot_name = self.serializer.get_snapshot_name(self.num_executions)
+            snapshot_location = self.extension.get_location(index=self.num_executions)
+            snapshot_name = self.extension.get_snapshot_name(index=self.num_executions)
             snapshot_data = self._recall_data(index=self.num_executions)
-            serialized_data = self.serializer.serialize(data)
+            serialized_data = self.extension.serialize(data)
             matches = snapshot_data is not None and serialized_data == snapshot_data
             assertion_success = matches
             if not matches and self._update_snapshots:
-                self.serializer.create_or_update_snapshot(
-                    data=data, index=self.num_executions
+                self.extension.write_snapshot(
+                    data=serialized_data, index=self.num_executions
                 )
                 assertion_success = True
             return assertion_success
@@ -122,7 +122,7 @@ class SnapshotAssertion:
             snapshot_created = snapshot_data is None and assertion_success
             snapshot_updated = matches is False and assertion_success
             self._execution_results[self._executions] = AssertionResult(
-                snapshot_filepath=snapshot_filepath,
+                snapshot_location=snapshot_location,
                 snapshot_name=snapshot_name,
                 recalled_data=snapshot_data,
                 asserted_data=serialized_data,
@@ -134,6 +134,6 @@ class SnapshotAssertion:
 
     def _recall_data(self, index: int) -> Optional["SerializableData"]:
         try:
-            return self.serializer.read_snapshot(index=index)
+            return self.extension.read_snapshot(index=index)
         except SnapshotDoesNotExist:
             return None
diff --git a/src/syrupy/constants.py b/src/syrupy/constants.py
index 41d0a84..c5e0c6e 100644
--- a/src/syrupy/constants.py
+++ b/src/syrupy/constants.py
@@ -1,5 +1,5 @@
 SNAPSHOT_DIRNAME = "__snapshots__"
-SNAPSHOT_EMPTY_FILE_KEY = "empty snapshot file"
-SNAPSHOT_UNKNOWN_FILE_KEY = "unknown snapshot file"
+SNAPSHOT_EMPTY_FOSSIL_KEY = "empty snapshot fossil"
+SNAPSHOT_UNKNOWN_FOSSIL_KEY = "unknown snapshot fossil"
 
 EXIT_STATUS_FAIL_UNUSED = 1
diff --git a/src/syrupy/data.py b/src/syrupy/data.py
index bc39ba3..d8c79dd 100644
--- a/src/syrupy/data.py
+++ b/src/syrupy/data.py
@@ -9,8 +9,8 @@ from typing import (
 import attr
 
 from .constants import (
-    SNAPSHOT_EMPTY_FILE_KEY,
-    SNAPSHOT_UNKNOWN_FILE_KEY,
+    SNAPSHOT_EMPTY_FOSSIL_KEY,
+    SNAPSHOT_UNKNOWN_FOSSIL_KEY,
 )
 
 
@@ -26,17 +26,19 @@ class Snapshot(object):
 
 @attr.s(frozen=True)
 class SnapshotEmpty(Snapshot):
-    name: str = attr.ib(default=SNAPSHOT_EMPTY_FILE_KEY, init=False)
+    name: str = attr.ib(default=SNAPSHOT_EMPTY_FOSSIL_KEY, init=False)
 
 
 @attr.s(frozen=True)
 class SnapshotUnknown(Snapshot):
-    name: str = attr.ib(default=SNAPSHOT_UNKNOWN_FILE_KEY, init=False)
+    name: str = attr.ib(default=SNAPSHOT_UNKNOWN_FOSSIL_KEY, init=False)
 
 
 @attr.s
-class SnapshotFile(object):
-    filepath: str = attr.ib()
+class SnapshotFossil(object):
+    """A collection of snapshots at a save location"""
+
+    location: str = attr.ib()
     _snapshots: Dict[str, "Snapshot"] = attr.ib(factory=dict)
 
     @property
@@ -49,8 +51,8 @@ class SnapshotFile(object):
     def add(self, snapshot: "Snapshot") -> None:
         self._snapshots[snapshot.name] = snapshot
 
-    def merge(self, snapshot_file: "SnapshotFile") -> None:
-        for snapshot in snapshot_file:
+    def merge(self, snapshot_fossil: "SnapshotFossil") -> None:
+        for snapshot in snapshot_fossil:
             self.add(snapshot)
 
     def remove(self, snapshot_name: str) -> None:
@@ -68,7 +70,9 @@ SNAPSHOTS_UNKNOWN = MappingProxyType({SnapshotUnknown().name: SnapshotUnknown()}
 
 
 @attr.s(frozen=True)
-class SnapshotEmptyFile(SnapshotFile):
+class SnapshotEmptyFossil(SnapshotFossil):
+    """This is a saved fossil that is known to be empty and thus can be removed"""
+
     _snapshots: Dict[str, "Snapshot"] = attr.ib(default=SNAPSHOTS_EMPTY, init=False)
 
     @property
@@ -77,33 +81,37 @@ class SnapshotEmptyFile(SnapshotFile):
 
 
 @attr.s(frozen=True)
-class SnapshotUnknownFile(SnapshotFile):
+class SnapshotUnknownFossil(SnapshotFossil):
+    """This is a saved fossil that is unclaimed by any extension currenly in use"""
+
     _snapshots: Dict[str, "Snapshot"] = attr.ib(default=SNAPSHOTS_UNKNOWN, init=False)
 
 
 @attr.s
-class SnapshotFiles(object):
-    _snapshot_files: Dict[str, "SnapshotFile"] = attr.ib(factory=dict)
+class SnapshotFossils(object):
+    _snapshot_fossils: Dict[str, "SnapshotFossil"] = attr.ib(factory=dict)
 
-    def get(self, filepath: str) -> Optional["SnapshotFile"]:
-        return self._snapshot_files.get(filepath)
+    def get(self, location: str) -> Optional["SnapshotFossil"]:
+        return self._snapshot_fossils.get(location)
 
-    def add(self, snapshot_file: "SnapshotFile") -> None:
-        self._snapshot_files[snapshot_file.filepath] = snapshot_file
+    def add(self, snapshot_fossil: "SnapshotFossil") -> None:
+        self._snapshot_fossils[snapshot_fossil.location] = snapshot_fossil
 
-    def update(self, snapshot_file: "SnapshotFile") -> None:
-        snapshot_file_to_update = self.get(snapshot_file.filepath)
-        if snapshot_file_to_update is None:
-            snapshot_file_to_update = SnapshotFile(filepath=snapshot_file.filepath)
-            self.add(snapshot_file_to_update)
-        snapshot_file_to_update.merge(snapshot_file)
+    def update(self, snapshot_fossil: "SnapshotFossil") -> None:
+        snapshot_fossil_to_update = self.get(snapshot_fossil.location)
+        if snapshot_fossil_to_update is None:
+            snapshot_fossil_to_update = SnapshotFossil(
+                location=snapshot_fossil.location
+            )
+            self.add(snapshot_fossil_to_update)
+        snapshot_fossil_to_update.merge(snapshot_fossil)
 
-    def merge(self, snapshot_files: "SnapshotFiles") -> None:
-        for snapshot_file in snapshot_files:
-            self.update(snapshot_file)
+    def merge(self, snapshot_fossils: "SnapshotFossils") -> None:
+        for snapshot_fossil in snapshot_fossils:
+            self.update(snapshot_fossil)
 
-    def __iter__(self) -> Iterator["SnapshotFile"]:
-        return iter(self._snapshot_files.values())
+    def __iter__(self) -> Iterator["SnapshotFossil"]:
+        return iter(self._snapshot_fossils.values())
 
     def __contains__(self, key: str) -> bool:
-        return key in self._snapshot_files
+        return key in self._snapshot_fossils
diff --git a/src/syrupy/extensions/__init__.py b/src/syrupy/extensions/__init__.py
new file mode 100644
index 0000000..cd025a7
--- /dev/null
+++ b/src/syrupy/extensions/__init__.py
@@ -0,0 +1,4 @@
+from .amber import AmberSnapshotExtension
+
+
+DEFAULT_EXTENSION = AmberSnapshotExtension
diff --git a/src/syrupy/serializers/amber.py b/src/syrupy/extensions/amber.py
similarity index 82%
rename from src/syrupy/serializers/amber.py
rename to src/syrupy/extensions/amber.py
index 807a3d5..774f1ee 100644
--- a/src/syrupy/serializers/amber.py
+++ b/src/syrupy/extensions/amber.py
@@ -11,10 +11,10 @@ from typing import (
 
 from syrupy.data import (
     Snapshot,
-    SnapshotFile,
+    SnapshotFossil,
 )
 
-from .base import AbstractSnapshotSerializer
+from .base import AbstractSyrupyExtension
 
 
 if TYPE_CHECKING:
@@ -32,13 +32,13 @@ class DataSerializer:
             return "..."
 
     @classmethod
-    def write_file(cls, snapshot_file: "SnapshotFile") -> None:
+    def write_file(cls, snapshot_fossil: "SnapshotFossil") -> None:
         """
         Writes the snapshot data into the snapshot file that be read later.
         """
-        filepath = snapshot_file.filepath
+        filepath = snapshot_fossil.location
         with open(filepath, "w") as f:
-            for snapshot in sorted(snapshot_file, key=lambda s: s.name):
+            for snapshot in sorted(snapshot_fossil, key=lambda s: s.name):
                 snapshot_data = str(snapshot.data)
                 if snapshot_data is not None:
                     f.write(f"{cls._marker_name} {snapshot.name}\n")
@@ -47,7 +47,7 @@ class DataSerializer:
                     f.write(f"{cls._marker_divider}\n")
 
     @classmethod
-    def read_file(cls, filepath: str) -> "SnapshotFile":
+    def read_file(cls, filepath: str) -> "SnapshotFossil":
         """
         Read the raw snapshot data (str) from the snapshot file into a dict
         of snapshot name to raw data. This does not attempt any deserialization
@@ -55,7 +55,7 @@ class DataSerializer:
         """
         name_marker_len = len(cls._marker_name)
         indent_len = len(cls._indent)
-        snapshot_file = SnapshotFile(filepath=filepath)
+        snapshot_fossil = SnapshotFossil(location=filepath)
         try:
             with open(filepath, "r") as f:
                 test_name = None
@@ -69,13 +69,13 @@ class DataSerializer:
                         if line.startswith(cls._indent):
                             snapshot_data += line[indent_len:]
                         elif line.startswith(cls._marker_divider) and snapshot_data:
-                            snapshot_file.add(
+                            snapshot_fossil.add(
                                 Snapshot(name=test_name, data=snapshot_data[:-1])
                             )
         except FileNotFoundError:
             pass
 
-        return snapshot_file
+        return snapshot_fossil
 
     @classmethod
     def sort(cls, iterable: Iterable[Any]) -> Iterable[Any]:
@@ -226,7 +226,7 @@ class DataSerializer:
         return serialize_method(**serialize_kwargs)
 
 
-class AmberSnapshotSerializer(AbstractSnapshotSerializer):
+class AmberSnapshotExtension(AbstractSyrupyExtension):
     """
     An amber snapshot file stores data in the following format:
 
@@ -239,39 +239,39 @@ class AmberSnapshotSerializer(AbstractSnapshotSerializer):
     ```
     """
 
+    def serialize(self, data: "SerializableData") -> str:
+        """
+        Returns the serialized form of 'data' to be compared
+        with the snapshot data written to disk.
+        """
+        return DataSerializer.serialize(data)
+
+    def delete_snapshots(
+        self, snapshot_location: str, snapshot_names: Set[str]
+    ) -> None:
+        snapshot_fossil_to_update = DataSerializer.read_file(snapshot_location)
+        for snapshot_name in snapshot_names:
+            snapshot_fossil_to_update.remove(snapshot_name)
+
+        if snapshot_fossil_to_update.has_snapshots:
+            DataSerializer.write_file(snapshot_fossil_to_update)
+        else:
+            os.remove(snapshot_location)
+
     @property
-    def file_extension(self) -> str:
+    def _file_extension(self) -> str:
         return "ambr"
 
-    def _discover_snapshots(self, filepath: str) -> "SnapshotFile":
-        return DataSerializer.read_file(filepath)
+    def _read_snapshot_fossil(self, snapshot_location: str) -> "SnapshotFossil":
+        return DataSerializer.read_file(snapshot_location)
 
-    def _read_snapshot_from_file(
-        self, snapshot_filepath: str, snapshot_name: str
+    def _read_snapshot_data_from_location(
+        self, snapshot_location: str, snapshot_name: str
     ) -> Optional["SerializableData"]:
-        snapshot = DataSerializer.read_file(snapshot_filepath).get(snapshot_name)
+        snapshot = self._read_snapshot_fossil(snapshot_location).get(snapshot_name)
         return snapshot.data if snapshot else None
 
-    def _write_snapshot_to_file(self, snapshot_file: "SnapshotFile") -> None:
-        snapshot_file_to_update = DataSerializer.read_file(snapshot_file.filepath)
-        snapshot_file_to_update.merge(snapshot_file)
-        DataSerializer.write_file(snapshot_file_to_update)
-
-    def delete_snapshots_from_file(
-        self, snapshot_file: str, snapshot_names: Set[str]
-    ) -> None:
-        snapshot_file_to_update = DataSerializer.read_file(snapshot_file)
-        for snapshot_name in snapshot_names:
-            snapshot_file_to_update.remove(snapshot_name)
-
-        if snapshot_file_to_update.has_snapshots:
-            DataSerializer.write_file(snapshot_file_to_update)
-        else:
-            os.remove(snapshot_file)
-
-    def serialize(self, data: "SerializableData") -> str:
-        """
-        Returns the serialized form of 'data' to be compared
-        with the snapshot data written to disk.
-        """
-        return DataSerializer.serialize(data)
+    def _write_snapshot_fossil(self, *, snapshot_fossil: "SnapshotFossil") -> None:
+        snapshot_fossil_to_update = DataSerializer.read_file(snapshot_fossil.location)
+        snapshot_fossil_to_update.merge(snapshot_fossil)
+        DataSerializer.write_file(snapshot_fossil_to_update)
diff --git a/src/syrupy/serializers/base.py b/src/syrupy/extensions/base.py
similarity index 50%
rename from src/syrupy/serializers/base.py
rename to src/syrupy/extensions/base.py
index d65830b..24bc215 100644
--- a/src/syrupy/serializers/base.py
+++ b/src/syrupy/extensions/base.py
@@ -20,9 +20,9 @@ from typing_extensions import final
 from syrupy.constants import SNAPSHOT_DIRNAME
 from syrupy.data import (
     Snapshot,
-    SnapshotEmptyFile,
-    SnapshotFile,
-    SnapshotFiles,
+    SnapshotEmptyFossil,
+    SnapshotFossil,
+    SnapshotFossils,
 )
 from syrupy.exceptions import SnapshotDoesNotExist
 from syrupy.terminal import (
@@ -40,32 +40,7 @@ if TYPE_CHECKING:
     from syrupy.types import SerializableData, SerializedData
 
 
-class AbstractSnapshotSerializer(ABC):
-    def __init__(self, test_location: "TestLocation"):
-        self._test_location = test_location
-
-    @property
-    @abstractmethod
-    def file_extension(self) -> str:
-        raise NotImplementedError
-
-    @property
-    def test_location(self) -> "TestLocation":
-        return self._test_location
-
-    @property
-    def dirname(self) -> str:
-        test_dirname = os.path.dirname(self.test_location.filename)
-        subdir_name = self.snapshot_subdirectory_name
-        if subdir_name is not None:
-            return os.path.join(test_dirname, SNAPSHOT_DIRNAME, subdir_name)
-        return os.path.join(test_dirname, SNAPSHOT_DIRNAME)
-
-    @property
-    def snapshot_subdirectory_name(self) -> Optional[str]:
-        """Optional subdirectory in which to store snapshots."""
-        return None
-
+class SnapshotSerializer(ABC):
     @abstractmethod
     def serialize(self, data: "SerializableData") -> "SerializedData":
         """
@@ -74,55 +49,93 @@ class AbstractSnapshotSerializer(ABC):
         """
         raise NotImplementedError
 
-    def discover_snapshots(self) -> "SnapshotFiles":
-        """
-        Returns all snapshot files relating to serializer test location
-        """
-        discovered_files: "SnapshotFiles" = SnapshotFiles()
-        for filepath in walk_snapshot_dir(self.dirname):
-            if filepath.endswith(self.file_extension):
-                snapshot_file = self._discover_snapshots(filepath)
-                if not snapshot_file.has_snapshots:
-                    snapshot_file = SnapshotEmptyFile(filepath=filepath)
-            else:
-                snapshot_file = SnapshotFile(filepath=filepath)
-            discovered_files.add(snapshot_file)
-        return discovered_files
 
+class SnapshotFossilizer(ABC):
+    @property
     @abstractmethod
-    def _discover_snapshots(self, snapshot_file: str) -> "SnapshotFile":
+    def test_location(self) -> "TestLocation":
+        raise NotImplementedError
+
+    def get_snapshot_name(self, *, index: int = 0) -> str:
+        """Get the snapshot name for the assertion index in a test location"""
+        index_suffix = f".{index}" if index > 0 else ""
+        testname = self.test_location.testname
+
+        if self.test_location.classname is not None:
+            return f"{self.test_location.classname}.{testname}{index_suffix}"
+        return f"{testname}{index_suffix}"
+
+    def get_location(self, *, index: int) -> str:
+        """Returns full location where snapshot data is stored."""
+        basename = self._get_file_basename(index=index)
+        return os.path.join(self._dirname, f"{basename}.{self._file_extension}")
+
+    def is_snapshot_location(self, *, location: str) -> bool:
+        """Checks if supplied location is valid for this snapshot extension"""
+        return location.endswith(self._file_extension)
+
+    def discover_snapshots(self) -> "SnapshotFossils":
         """
-        Given a path to a snapshot file, returns all snapshots in the file.
-        Snapshot name is dependent on serializer implementation.
+        Returns all snapshot fossils in test site
         """
-        raise NotImplementedError
+        discovered: "SnapshotFossils" = SnapshotFossils()
+        for filepath in walk_snapshot_dir(self._dirname):
+            if self.is_snapshot_location(location=filepath):
+                snapshot_fossil = self._read_snapshot_fossil(snapshot_location=filepath)
+                if not snapshot_fossil.has_snapshots:
+                    snapshot_fossil = SnapshotEmptyFossil(location=filepath)
+            else:
+                snapshot_fossil = SnapshotFossil(location=filepath)
+            discovered.add(snapshot_fossil)
+        return discovered
 
     @final
-    def read_snapshot(self, index: int) -> "SerializedData":
+    def read_snapshot(self, *, index: int) -> "SerializedData":
         """
         Utility method for reading the contents of a snapshot assertion.
-        Will call `pre_read`, then `read` and finally `post_read`,
+        Will call `_pre_read`, then perform `read` and finally `post_read`,
         returning the contents parsed from the `read` method.
+
+        Override `_read_snapshot_data_from_location` in subclass to change behaviour
         """
         try:
-            self.pre_read(index=index)
-            return self.read(index=index)
+            self._pre_read(index=index)
+            snapshot_location = self.get_location(index=index)
+            snapshot_name = self.get_snapshot_name(index=index)
+            snapshot_data = self._read_snapshot_data_from_location(
+                snapshot_location=snapshot_location, snapshot_name=snapshot_name
+            )
+            if snapshot_data is None:
+                raise SnapshotDoesNotExist()
+            return snapshot_data
         finally:
-            self.post_read(index=index)
+            self._post_read(index=index)
 
     @final
-    def create_or_update_snapshot(self, data: "SerializableData", index: int) -> None:
+    def write_snapshot(self, *, data: "SerializedData", index: int) -> None:
         """
         Utility method for writing the contents of a snapshot assertion.
-        Will call `pre_write`, then `write` and finally `post_write`.
+        Will call `_pre_write`, then perform `write` and finally `_post_write`.
+
+        Override `_write_snapshot_fossil` in subclass to change behaviour
         """
-        self.pre_write(data, index=index)
-        self.write(data, index=index)
-        self.post_write(data, index=index)
+        self._pre_write(data=data, index=index)
+        snapshot_location = self.get_location(index=index)
+        snapshot_name = self.get_snapshot_name(index=index)
+        if not self.test_location.matches_snapshot_name(snapshot_name):
+            warning_msg = f"""
+            Can not relate snapshot name '{snapshot_name}' to the test location.
+            Consider adding '{self.test_location.testname}' to the generated name.
+            """
+            warnings.warn(warning_msg)
+        snapshot_fossil = SnapshotFossil(location=snapshot_location)
+        snapshot_fossil.add(Snapshot(name=snapshot_name, data=data))
+        self._write_snapshot_fossil(snapshot_fossil=snapshot_fossil)
+        self._post_write(data=data, index=index)
 
     @abstractmethod
-    def delete_snapshots_from_file(
-        self, snapshot_filepath: str, snapshot_names: Set[str]
+    def delete_snapshots(
+        self, *, snapshot_location: str, snapshot_names: Set[str]
     ) -> None:
         """
         Remove snapshots from a snapshot file.
@@ -130,89 +143,74 @@ class AbstractSnapshotSerializer(ABC):
         """
         raise NotImplementedError
 
-    def pre_read(self, index: int = 0) -> None:
+    def _pre_read(self, *, index: int = 0) -> None:
         pass
 
-    @final
-    def read(self, index: int = 0) -> "SerializedData":
-        """
-        Override `_read_snapshot_from_file` in subclass to change behaviour
-        """
-        snapshot_file = self.get_filepath(index)
-        snapshot_name = self.get_snapshot_name(index)
-        snapshot = self._read_snapshot_from_file(snapshot_file, snapshot_name)
-        if snapshot is None:
-            raise SnapshotDoesNotExist()
-        return snapshot
+    def _post_read(self, *, index: int = 0) -> None:
+        pass
+
+    def _pre_write(self, *, data: "SerializedData", index: int = 0) -> None:
+        self.__ensure_snapshot_dir(index=index)
 
-    def post_read(self, index: int = 0) -> None:
+    def _post_write(self, *, data: "SerializedData", index: int = 0) -> None:
         pass
 
-    def pre_write(self, data: "SerializableData", index: int = 0) -> None:
-        self.__ensure_snapshot_dir(index)
+    @abstractmethod
+    def _read_snapshot_fossil(self, *, snapshot_location: str) -> "SnapshotFossil":
+        """
+        Read the snapshot location and construct a snapshot fossil object
+        """
+        raise NotImplementedError
 
-    @final
-    def write(self, data: "SerializableData", index: int = 0) -> None:
+    @abstractmethod
+    def _read_snapshot_data_from_location(
+        self, *, snapshot_location: str, snapshot_name: str
+    ) -> Optional["SerializedData"]:
         """
-        Override `_write_snapshot_to_file` in subclass to change behaviour
+        Get only the snapshot data from location for assertion
         """
-        snapshot_filepath = self.get_filepath(index)
-        snapshot_name = self.get_snapshot_name(index)
-        if not self.test_location.matches_snapshot_name(snapshot_name):
-            warning_msg = f"""
-            Can not relate snapshot name '{snapshot_name}' to the test location.
-            Consider adding '{self.test_location.testname}' to the generated name.
-            """
-            warnings.warn(warning_msg)
-        snapshot_file = SnapshotFile(filepath=snapshot_filepath)
-        snapshot_file.add(Snapshot(name=snapshot_name, data=self.serialize(data)))
-        self._write_snapshot_to_file(snapshot_file)
+        raise NotImplementedError
 
-    def post_write(self, data: "SerializableData", index: int = 0) -> None:
-        pass
+    @abstractmethod
+    def _write_snapshot_fossil(self, *, snapshot_fossil: "SnapshotFossil") -> None:
+        """
+        Adds the snapshot data to the snapshots in fossil location
+        """
+        raise NotImplementedError
 
-    def get_snapshot_name(self, index: int = 0) -> str:
-        index_suffix = f".{index}" if index > 0 else ""
-        testname = self._test_location.testname
+    @property
+    def _snapshot_subdirectory_name(self) -> Optional[str]:
+        """Optional subdirectory in which to store snapshots."""
+        return None
 
-        if self._test_location.classname is not None:
-            return f"{self._test_location.classname}.{testname}{index_suffix}"
-        return f"{testname}{index_suffix}"
+    @property
+    def _dirname(self) -> str:
+        test_dirname = os.path.dirname(self.test_location.filename)
+        subdir_name = self._snapshot_subdirectory_name
+        if subdir_name is not None:
+            return os.path.join(test_dirname, SNAPSHOT_DIRNAME, subdir_name)
+        return os.path.join(test_dirname, SNAPSHOT_DIRNAME)
 
-    def get_filepath(self, index: int) -> str:
-        """Returns full filepath where snapshot data is stored."""
-        basename = self.get_file_basename(index=index)
-        return os.path.join(self.dirname, f"{basename}.{self.file_extension}")
+    @property
+    @abstractmethod
+    def _file_extension(self) -> str:
+        raise NotImplementedError
 
-    def get_file_basename(self, index: int) -> str:
+    def _get_file_basename(self, *, index: int) -> str:
         """Returns file basename without extension. Used to create full filepath."""
-        return f"{os.path.splitext(os.path.basename(self._test_location.filename))[0]}"
+        return f"{os.path.splitext(os.path.basename(self.test_location.filename))[0]}"
 
-    def __ensure_snapshot_dir(self, index: int) -> None:
+    def __ensure_snapshot_dir(self, *, index: int) -> None:
         """
         Ensures the folder path for the snapshot file exists.
         """
         try:
-            os.makedirs(os.path.dirname(self.get_filepath(index)))
+            os.makedirs(os.path.dirname(self.get_location(index=index)))
         except FileExistsError:
             pass
 
-    @abstractmethod
-    def _read_snapshot_from_file(
-        self, snapshot_filepath: str, snapshot_name: str
-    ) -> Optional["SerializedData"]:
-        """
-        Read the snapshot file and get only the snapshot data for assertion
-        """
-        raise NotImplementedError
-
-    @abstractmethod
-    def _write_snapshot_to_file(self, snapshot_file: "SnapshotFile") -> None:
-        """
-        Adds the snapshot data to the snapshots read from the file
-        """
-        raise NotImplementedError
 
+class SnapshotReporter(ABC):
     def diff_lines(
         self, serialized_data: "SerializedData", snapshot_data: "SerializedData"
     ) -> Generator[str, None, None]:
@@ -244,3 +242,12 @@ class AbstractSnapshotSerializer(ABC):
             emphasize(line_style(char)) if str(marker) in "-+^" else line_style(char)
             for marker, char in zip_longest(marker_line.strip(), line)
         )
+
+
+class AbstractSyrupyExtension(SnapshotSerializer, SnapshotFossilizer, SnapshotReporter):
+    def __init__(self, test_location: "TestLocation"):
+        self._test_location = test_location
+
+    @property
+    def test_location(self) -> "TestLocation":
+        return self._test_location
diff --git a/src/syrupy/serializers/image.py b/src/syrupy/extensions/image.py
similarity index 52%
rename from src/syrupy/serializers/image.py
rename to src/syrupy/extensions/image.py
index 05ac16d..72002fb 100644
--- a/src/syrupy/serializers/image.py
+++ b/src/syrupy/extensions/image.py
@@ -1,21 +1,21 @@
 from typing import TYPE_CHECKING
 
-from .raw_single import RawSingleSnapshotSerializer
+from .single_file import SingleFileSnapshotExtension
 
 
 if TYPE_CHECKING:
     from syrupy.types import SerializableData
 
 
-class PNGImageSnapshotSerializer(RawSingleSnapshotSerializer):
+class PNGImageSnapshotExtension(SingleFileSnapshotExtension):
     @property
-    def file_extension(self) -> str:
+    def _file_extension(self) -> str:
         return "png"
 
 
-class SVGImageSnapshotSerializer(RawSingleSnapshotSerializer):
+class SVGImageSnapshotExtension(SingleFileSnapshotExtension):
     @property
-    def file_extension(self) -> str:
+    def _file_extension(self) -> str:
         return "svg"
 
     def serialize(self, data: "SerializableData") -> bytes:
diff --git a/src/syrupy/extensions/single_file.py b/src/syrupy/extensions/single_file.py
new file mode 100644
index 0000000..47ceb53
--- /dev/null
+++ b/src/syrupy/extensions/single_file.py
@@ -0,0 +1,74 @@
+import os
+import re
+from gettext import gettext
+from typing import (
+    TYPE_CHECKING,
+    Optional,
+    Set,
+)
+
+from syrupy.data import (
+    Snapshot,
+    SnapshotFossil,
+)
+
+from .base import AbstractSyrupyExtension
+
+
+if TYPE_CHECKING:
+    from syrupy.types import SerializableData, SerializedData  # noqa: F401
+
+
+class SingleFileSnapshotExtension(AbstractSyrupyExtension):
+    def serialize(self, data: "SerializableData") -> bytes:
+        return bytes(data)
+
+    def get_snapshot_name(self, *, index: int = 0) -> str:
+        return self.__clean_filename(
+            super(SingleFileSnapshotExtension, self).get_snapshot_name(index=index)
+        )
+
+    def delete_snapshots(
+        self, *, snapshot_location: str, snapshot_names: Set[str]
+    ) -> None:
+        os.remove(snapshot_location)
+
+    @property
+    def _file_extension(self) -> str:
+        return "raw"
+
+    def _get_file_basename(self, *, index: int) -> str:
+        return self.get_snapshot_name(index=index)
+
+    @property
+    def _snapshot_subdirectory_name(self) -> str:
+        return os.path.splitext(os.path.basename(str(self.test_location.filename)))[0]
+
+    def _read_snapshot_fossil(self, *, snapshot_location: str) -> "SnapshotFossil":
+        snapshot_fossil = SnapshotFossil(location=snapshot_location)
+        snapshot_fossil.add(
+            Snapshot(name=os.path.splitext(os.path.basename(snapshot_location))[0])
+        )
+        return snapshot_fossil
+
+    def _read_snapshot_data_from_location(
+        self, *, snapshot_location: str, snapshot_name: str
+    ) -> Optional["SerializableData"]:
+        try:
+            with open(snapshot_location, "rb") as f:
+                return f.read()
+        except FileNotFoundError:
+            return None
+
+    def _write_snapshot_fossil(self, *, snapshot_fossil: "SnapshotFossil") -> None:
+        filepath, data = snapshot_fossil.location, next(iter(snapshot_fossil)).data
+        if not isinstance(data, bytes):
+            error_text = gettext("Can write non binary data. Expected '{}', got '{}'")
+            raise TypeError(error_text.format(bytes.__name__, type(data).__name__))
+        with open(filepath, "wb") as f:
+            f.write(data)
+
+    def __clean_filename(self, filename: str) -> str:
+        filename = str(filename).strip().replace(" ", "_")
+        max_filename_length = 255 - len(self._file_extension or "")
+        return re.sub(r"(?u)[^-\w.]", "", filename)[:max_filename_length]
diff --git a/src/syrupy/report.py b/src/syrupy/report.py
index c4be648..07a31f3 100644
--- a/src/syrupy/report.py
+++ b/src/syrupy/report.py
@@ -15,9 +15,9 @@ import attr
 
 from .data import (
     Snapshot,
-    SnapshotFile,
-    SnapshotFiles,
-    SnapshotUnknownFile,
+    SnapshotFossil,
+    SnapshotFossils,
+    SnapshotUnknownFossil,
 )
 from .location import TestLocation
 from .terminal import (
@@ -41,31 +41,30 @@ class SnapshotReport(object):
     update_snapshots: bool = attr.ib()
     warn_unused_snapshots: bool = attr.ib()
     assertions: List["SnapshotAssertion"] = attr.ib()
-    discovered: "SnapshotFiles" = attr.ib(factory=SnapshotFiles)
-    created: "SnapshotFiles" = attr.ib(factory=SnapshotFiles)
-    failed: "SnapshotFiles" = attr.ib(factory=SnapshotFiles)
-    matched: "SnapshotFiles" = attr.ib(factory=SnapshotFiles)
-    updated: "SnapshotFiles" = attr.ib(factory=SnapshotFiles)
-    used: "SnapshotFiles" = attr.ib(factory=SnapshotFiles)
+    discovered: "SnapshotFossils" = attr.ib(factory=SnapshotFossils)
+    created: "SnapshotFossils" = attr.ib(factory=SnapshotFossils)
+    failed: "SnapshotFossils" = attr.ib(factory=SnapshotFossils)
+    matched: "SnapshotFossils" = attr.ib(factory=SnapshotFossils)
+    updated: "SnapshotFossils" = attr.ib(factory=SnapshotFossils)
+    used: "SnapshotFossils" = attr.ib(factory=SnapshotFossils)
 
     def __attrs_post_init__(self) -> None:
         for assertion in self.assertions:
-            self.discovered.merge(assertion.serializer.discover_snapshots())
+            self.discovered.merge(assertion.extension.discover_snapshots())
             for result in assertion.executions.values():
-                filepath = result.snapshot_filepath
-                snapshot_file = SnapshotFile(filepath=filepath)
-                snapshot_file.add(
+                snapshot_fossil = SnapshotFossil(location=result.snapshot_location)
+                snapshot_fossil.add(
                     Snapshot(name=result.snapshot_name, data=result.final_data)
                 )
-                self.used.update(snapshot_file)
+                self.used.update(snapshot_fossil)
                 if result.created:
-                    self.created.update(snapshot_file)
+                    self.created.update(snapshot_fossil)
                 elif result.updated:
-                    self.updated.update(snapshot_file)
+                    self.updated.update(snapshot_fossil)
                 elif result.success:
-                    self.matched.update(snapshot_file)
+                    self.matched.update(snapshot_fossil)
                 else:
-                    self.failed.update(snapshot_file)
+                    self.failed.update(snapshot_fossil)
 
     @property
     def num_created(self) -> int:
@@ -92,34 +91,36 @@ class SnapshotReport(object):
         return self.all_items == self.ran_items
 
     @property
-    def unused(self) -> "SnapshotFiles":
-        unused_files = SnapshotFiles()
-        for unused_snapshot_file in self._diff_snapshot_files(
+    def unused(self) -> "SnapshotFossils":
+        unused_fossils = SnapshotFossils()
+        for unused_snapshot_fossil in self._diff_snapshot_fossils(
             self.discovered, self.used
         ):
-            snapshot_filepath = unused_snapshot_file.filepath
+            snapshot_location = unused_snapshot_fossil.location
             if self.ran_all_collected_tests:
-                unused_snapshots = {*unused_snapshot_file}
-                mark_file_for_removal = snapshot_filepath not in self.used
+                unused_snapshots = {*unused_snapshot_fossil}
+                mark_for_removal = snapshot_location not in self.used
             else:
                 unused_snapshots = {
                     snapshot
-                    for snapshot in unused_snapshot_file
+                    for snapshot in unused_snapshot_fossil
                     if any(
                         TestLocation(node).matches_snapshot_name(snapshot.name)
                         for node in self.ran_items
                     )
                 }
-                mark_file_for_removal = False
+                mark_for_removal = False
 
             if unused_snapshots:
-                marked_unused_snapshot_file = SnapshotFile(filepath=snapshot_filepath)
+                marked_unused_snapshot_fossil = SnapshotFossil(
+                    location=snapshot_location
+                )
                 for snapshot in unused_snapshots:
-                    marked_unused_snapshot_file.add(snapshot)
-                unused_files.add(marked_unused_snapshot_file)
-            elif mark_file_for_removal:
-                unused_files.add(SnapshotUnknownFile(filepath=snapshot_filepath))
-        return unused_files
+                    marked_unused_snapshot_fossil.add(snapshot)
+                unused_fossils.add(marked_unused_snapshot_fossil)
+            elif mark_for_removal:
+                unused_fossils.add(SnapshotUnknownFossil(location=snapshot_location))
+        return unused_fossils
 
     @property
     def lines(self) -> Generator[str, None, None]:
@@ -170,9 +171,9 @@ class SnapshotReport(object):
         if self.num_unused:
             yield ""
             if self.update_snapshots:
-                for snapshot_file in self.unused:
-                    filepath = snapshot_file.filepath
-                    snapshots = (snapshot.name for snapshot in snapshot_file)
+                for snapshot_fossil in self.unused:
+                    filepath = snapshot_fossil.location
+                    snapshots = (snapshot.name for snapshot in snapshot_fossil)
                     path_to_file = os.path.relpath(filepath, self.base_dir)
                     deleted_snapshots = ", ".join(map(bold, sorted(snapshots)))
                     yield warning_style(gettext("Deleted {} ({})")).format(
@@ -187,20 +188,20 @@ class SnapshotReport(object):
                 else:
                     yield error_style(message)
 
-    def _diff_snapshot_files(
-        self, snapshot_files1: "SnapshotFiles", snapshot_files2: "SnapshotFiles"
-    ) -> "SnapshotFiles":
-        diffed_snapshot_files: "SnapshotFiles" = SnapshotFiles()
-        for snapshot_file1 in snapshot_files1:
-            snapshot_file2 = snapshot_files2.get(
-                snapshot_file1.filepath
-            ) or SnapshotFile(filepath=snapshot_file1.filepath)
-            diffed_snapshot_file = SnapshotFile(filepath=snapshot_file1.filepath)
-            for snapshot in snapshot_file1:
-                if not snapshot_file2.get(snapshot.name):
-                    diffed_snapshot_file.add(snapshot)
-            diffed_snapshot_files.add(diffed_snapshot_file)
-        return diffed_snapshot_files
-
-    def _count_snapshots(self, snapshot_files: "SnapshotFiles") -> int:
-        return sum(len(snapshot_file) for snapshot_file in snapshot_files)
+    def _diff_snapshot_fossils(
+        self, snapshot_fossils1: "SnapshotFossils", snapshot_fossils2: "SnapshotFossils"
+    ) -> "SnapshotFossils":
+        diffed_snapshot_fossils: "SnapshotFossils" = SnapshotFossils()
+        for snapshot_fossil1 in snapshot_fossils1:
+            snapshot_fossil2 = snapshot_fossils2.get(
+                snapshot_fossil1.location
+            ) or SnapshotFossil(location=snapshot_fossil1.location)
+            diffed_snapshot_fossil = SnapshotFossil(location=snapshot_fossil1.location)
+            for snapshot in snapshot_fossil1:
+                if not snapshot_fossil2.get(snapshot.name):
+                    diffed_snapshot_fossil.add(snapshot)
+            diffed_snapshot_fossils.add(diffed_snapshot_fossil)
+        return diffed_snapshot_fossils
+
+    def _count_snapshots(self, snapshot_fossils: "SnapshotFossils") -> int:
+        return sum(len(snapshot_fossil) for snapshot_fossil in snapshot_fossils)
diff --git a/src/syrupy/serializers/__init__.py b/src/syrupy/serializers/__init__.py
deleted file mode 100644
index a1fa533..0000000
--- a/src/syrupy/serializers/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .amber import AmberSnapshotSerializer
-
-
-DEFAULT_SERIALIZER = AmberSnapshotSerializer
diff --git a/src/syrupy/serializers/raw_single.py b/src/syrupy/serializers/raw_single.py
deleted file mode 100644
index 7ef1cdf..0000000
--- a/src/syrupy/serializers/raw_single.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import os
-import re
-from gettext import gettext
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Optional,
-    Set,
-)
-
-from syrupy.data import (
-    Snapshot,
-    SnapshotFile,
-)
-
-from .base import AbstractSnapshotSerializer
-
-
-if TYPE_CHECKING:
-    from syrupy.types import SerializableData, SerializedData  # noqa: F401
-
-
-class RawSingleSnapshotSerializer(AbstractSnapshotSerializer):
-    @property
-    def file_extension(self) -> str:
-        return "raw"
-
-    def _discover_snapshots(self, filepath: str) -> "SnapshotFile":
-        """Parse the snapshot name from the filename."""
-        snapshot_file = SnapshotFile(filepath=filepath)
-        snapshot_file.add(
-            Snapshot(name=os.path.splitext(os.path.basename(filepath))[0])
-        )
-        return snapshot_file
-
-    def get_file_basename(self, index: int) -> str:
-        return self.get_snapshot_name(index=index)
-
-    def get_snapshot_name(self, index: int = 0) -> str:
-        return self.__clean_filename(
-            super(RawSingleSnapshotSerializer, self).get_snapshot_name(index=index)
-        )
-
-    @property
-    def snapshot_subdirectory_name(self) -> str:
-        return os.path.splitext(os.path.basename(str(self.test_location.filename)))[0]
-
-    def _read_snapshot_from_file(
-        self, snapshot_filepath: str, snapshot_name: str
-    ) -> Optional["SerializableData"]:
-        return self._read_file(snapshot_filepath)
-
-    def serialize(self, data: "SerializableData") -> bytes:
-        return bytes(data)
-
-    def _read_file(self, filepath: str) -> Any:
-        try:
-            with open(filepath, "rb") as f:
-                return f.read()
-        except FileNotFoundError:
-            return None
-
-    def _write_snapshot_to_file(self, snapshot_file: "SnapshotFile") -> None:
-        self.__write_file(snapshot_file.filepath, next(iter(snapshot_file)).data)
-
-    def delete_snapshots_from_file(self, snapshot_filepath: str, _: Set[str]) -> None:
-        os.remove(snapshot_filepath)
-
-    def __write_file(self, filepath: str, data: Optional["SerializedData"]) -> None:
-        if not isinstance(data, bytes):
-            error_text = gettext("Can write non binary data. Expected '{}', got '{}'")
-            raise TypeError(error_text.format(bytes.__name__, type(data).__name__))
-        with open(filepath, "wb") as f:
-            f.write(data)
-
-    def __clean_filename(self, filename: str) -> str:
-        filename = str(filename).strip().replace(" ", "_")
-        max_filename_length = 255 - len(self.file_extension or "")
-        return re.sub(r"(?u)[^-\w.]", "", filename)[:max_filename_length]
diff --git a/src/syrupy/session.py b/src/syrupy/session.py
index 490ba80..3f3b0d1 100644
--- a/src/syrupy/session.py
+++ b/src/syrupy/session.py
@@ -9,13 +9,13 @@ from typing import (
 )
 
 from .constants import EXIT_STATUS_FAIL_UNUSED
-from .data import SnapshotFiles
+from .data import SnapshotFossils
 from .report import SnapshotReport
 
 
 if TYPE_CHECKING:
     from .assertion import SnapshotAssertion
-    from .serializers.base import AbstractSnapshotSerializer  # noqa: F401
+    from .extensions.base import AbstractSyrupyExtension  # noqa: F401
 
 
 class SnapshotSession:
@@ -29,14 +29,14 @@ class SnapshotSession:
         self._all_items: Set[Any] = set()
         self._ran_items: Set[Any] = set()
         self._assertions: List["SnapshotAssertion"] = []
-        self._serializers: Dict[str, "AbstractSnapshotSerializer"] = {}
+        self._extensions: Dict[str, "AbstractSyrupyExtension"] = {}
 
     def start(self) -> None:
         self.report = None
         self._all_items = set()
         self._ran_items = set()
         self._assertions = []
-        self._serializers = {}
+        self._extensions = {}
 
     def finish(self) -> int:
         exitstatus = 0
@@ -51,8 +51,8 @@ class SnapshotSession:
         if self.report.num_unused:
             if self.update_snapshots:
                 self.remove_unused_snapshots(
-                    unused_snapshot_files=self.report.unused,
-                    used_snapshot_files=self.report.used,
+                    unused_snapshot_fossils=self.report.unused,
+                    used_snapshot_fossils=self.report.used,
                 )
             elif not self.warn_unused_snapshots:
                 exitstatus |= EXIT_STATUS_FAIL_UNUSED
@@ -60,24 +60,27 @@ class SnapshotSession:
 
     def register_request(self, assertion: "SnapshotAssertion") -> None:
         self._assertions.append(assertion)
-        discovered_serializers = {
-            discovered.filepath: assertion.serializer
-            for discovered in assertion.serializer.discover_snapshots()
+        discovered_extensions = {
+            discovered.location: assertion.extension
+            for discovered in assertion.extension.discover_snapshots()
             if discovered.has_snapshots
         }
-        self._serializers.update(discovered_serializers)
+        self._extensions.update(discovered_extensions)
 
     def remove_unused_snapshots(
         self,
-        unused_snapshot_files: "SnapshotFiles",
-        used_snapshot_files: "SnapshotFiles",
+        unused_snapshot_fossils: "SnapshotFossils",
+        used_snapshot_fossils: "SnapshotFossils",
     ) -> None:
-        for unused_snapshot_file in unused_snapshot_files:
-            snapshot_file = unused_snapshot_file.filepath
-            serializer = self._serializers.get(snapshot_file)
-            if serializer:
-                serializer.delete_snapshots_from_file(
-                    snapshot_file, {snapshot.name for snapshot in unused_snapshot_file}
+        for unused_snapshot_fossil in unused_snapshot_fossils:
+            snapshot_location = unused_snapshot_fossil.location
+            extension = self._extensions.get(snapshot_location)
+            if extension:
+                extension.delete_snapshots(
+                    snapshot_location=snapshot_location,
+                    snapshot_names={
+                        snapshot.name for snapshot in unused_snapshot_fossil
+                    },
                 )
-            elif snapshot_file not in used_snapshot_files:
-                os.remove(snapshot_file)
+            elif snapshot_location not in used_snapshot_fossils:
+                os.remove(snapshot_location)

SerializerClass vs IOClass
## Discussion

Would it be more idiomatic to have the current `SerializerClass` be replaced in name by `IOClass` but retain the same functionality.

### Arguments

| For replacing with `AbstractSnapshotIO` | For keeping `Serializer` nomenclature |
| --- | --- |
| IO handles the file system, the serialization is just a step in the process | Serializer handles writing and reading from file system |
| IO (input, output) is more encompassing allowing for extensions to functionality like preparing assertion diffs | Serializer is a more familiar concept to most people making the naming appropriate for its function

### [Example](https://github.com/tophat/syrupy/compare/7cde8a7...feat-22)

https://github.com/tophat/syrupy/blob/44b0f6ccdc1036c531a76dfac87ed5ca75d6be51/README.md#L71
https://github.com/tophat/syrupy/blob/44b0f6ccdc1036c531a76dfac87ed5ca75d6be51/src/syrupy/__init__.py#L66-L71
https://github.com/tophat/syrupy/blob/44b0f6ccdc1036c531a76dfac87ed5ca75d6be51/src/syrupy/assertion.py#L71-L75
https://github.com/tophat/syrupy/blob/44b0f6ccdc1036c531a76dfac87ed5ca75d6be51/src/syrupy/assertion.py#L135-L138
diff --git a/tests/test_custom_serializer.py b/tests/test_custom_serializer.py
index 39d847e..0fe0f3e 100644
--- a/tests/test_custom_serializer.py
+++ b/tests/test_custom_serializer.py
@@ -9,35 +9,35 @@ def testcases(testdir):
         """
         import pytest
 
-        from syrupy.serializers.base import AbstractSnapshotSerializer
+        from syrupy.extensions.base import AbstractSyrupyExtension
 
-        class CustomSnapshotSerializer(AbstractSnapshotSerializer):
-            def file_extension(self):
+        class CustomSnapshotExtension(AbstractSyrupyExtension):
+            def _file_extension(self):
                 return ""
 
             def serialize(self, data):
                 return str(data)
 
-            def _discover_snapshots(self, filepath):
-                pass
-
-            def get_snapshot_name(self, index = 0):
+            def get_snapshot_name(self, *, index = 0):
                 testname = self._test_location.testname[::-1]
                 return f"{testname}.{index}"
 
-            def _read_snapshot_from_file(self, file, name):
+            def _read_snapshot_fossil(self, **kwargs):
+                pass
+
+            def _read_snapshot_data_from_location(self, **kwargs):
                 pass
 
-            def _write_snapshot_to_file(self, snapshot_file):
+            def _write_snapshot_fossil(self, **kwargs):
                 pass
 
-            def delete_snapshots_from_file(self, file, names):
+            def delete_snapshots(self, **kwargs):
                 pass
 
 
         @pytest.fixture
         def snapshot_custom(snapshot):
-            return snapshot.with_class(serializer_class=CustomSnapshotSerializer)
+            return snapshot.use_extension(CustomSnapshotExtension)
         """
     )
     return {
diff --git a/tests/test_image_serializer.py b/tests/test_image_serializer.py
index 57cce05..b3a4d7c 100644
--- a/tests/test_image_serializer.py
+++ b/tests/test_image_serializer.py
@@ -2,20 +2,20 @@ import base64
 
 import pytest
 
-from syrupy.serializers.image import (
-    PNGImageSnapshotSerializer,
-    SVGImageSnapshotSerializer,
+from syrupy.extensions.image import (
+    PNGImageSnapshotExtension,
+    SVGImageSnapshotExtension,
 )
 
 
 @pytest.fixture
 def snapshot_png(snapshot):
-    return snapshot.with_class(serializer_class=PNGImageSnapshotSerializer)
+    return snapshot.use_extension(PNGImageSnapshotExtension)
 
 
 @pytest.fixture
 def snapshot_svg(snapshot):
-    return snapshot.with_class(serializer_class=SVGImageSnapshotSerializer)
+    return snapshot.use_extension(SVGImageSnapshotExtension)
 
 
 def test_image(snapshot_png, snapshot_svg):
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 2471037..4d2acf1 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -283,7 +283,7 @@ def test_removed_snapshots(stubs):
     assert os.path.isfile(filepath)
 
 
-def test_removed_snapshot_file(stubs):
+def test_removed_snapshot_fossil(stubs):
     _, testdir, tests, filepath = stubs
     assert os.path.isfile(filepath)
     testdir.makepyfile(test_file=tests["inject"])
@@ -295,7 +295,7 @@ def test_removed_snapshot_file(stubs):
     assert not os.path.isfile(filepath)
 
 
-def test_removed_empty_snapshot_file_only(stubs):
+def test_removed_empty_snapshot_fossil_only(stubs):
     _, testdir, _, filepath = stubs
     empty_filepath = os.path.join(os.path.dirname(filepath), "test_empty.ambr")
     with open(empty_filepath, "w") as empty_snapfile:
@@ -305,14 +305,14 @@ def test_removed_empty_snapshot_file_only(stubs):
     result_stdout = clean_output(result.stdout.str())
     assert os.path.relpath(filepath) not in result_stdout
     assert "1 unused snapshot deleted" in result_stdout
-    assert "empty snapshot file" in result_stdout
+    assert "empty snapshot" in result_stdout
     assert os.path.relpath(empty_filepath) in result_stdout
     assert result.ret == 0
     assert os.path.isfile(filepath)
     assert not os.path.isfile(empty_filepath)
 
 
-def test_removed_hanging_snapshot_file(stubs):
+def test_removed_hanging_snapshot_fossil(stubs):
     _, testdir, _, filepath = stubs
     hanging_filepath = os.path.join(os.path.dirname(filepath), "test_hanging.abc")
     with open(hanging_filepath, "w") as empty_snapfile:
@@ -322,7 +322,7 @@ def test_removed_hanging_snapshot_file(stubs):
     result_stdout = clean_output(result.stdout.str())
     assert os.path.relpath(filepath) not in result_stdout
     assert "1 unused snapshot deleted" in result_stdout
-    assert "unknown snapshot file" in result_stdout
+    assert "unknown snapshot" in result_stdout
     assert os.path.relpath(hanging_filepath) in result_stdout
     assert result.ret == 0
     assert os.path.isfile(filepath)
diff --git a/tests/test_raw_single_serializer.py b/tests/test_raw_single_serializer.py
index 2014145..072fd47 100644
--- a/tests/test_raw_single_serializer.py
+++ b/tests/test_raw_single_serializer.py
@@ -5,9 +5,9 @@ import pytest
 
 from syrupy.data import (
     Snapshot,
-    SnapshotFile,
+    SnapshotFossil,
 )
-from syrupy.serializers.raw_single import RawSingleSnapshotSerializer
+from syrupy.extensions.single_file import SingleFileSnapshotExtension
 
 
 if TYPE_CHECKING:
@@ -16,17 +16,17 @@ if TYPE_CHECKING:
 
 @pytest.fixture
 def snapshot_raw(snapshot):
-    return snapshot.with_class(serializer_class=RawSingleSnapshotSerializer)
+    return snapshot.use_extension(SingleFileSnapshotExtension)
 
 
 def test_does_not_write_non_binary(testdir, snapshot_raw: "SnapshotAssertion"):
-    snapshot_file = SnapshotFile(
-        filepath=os.path.join(testdir.tmpdir, "snapshot_file.raw"),
+    snapshot_fossil = SnapshotFossil(
+        location=os.path.join(testdir.tmpdir, "snapshot_fossil.raw"),
     )
-    snapshot_file.add(Snapshot(name="snapshot_name", data="non binary data"))
+    snapshot_fossil.add(Snapshot(name="snapshot_name", data="non binary data"))
     with pytest.raises(TypeError, match="Expected 'bytes', got 'str'"):
-        snapshot_raw.serializer._write_snapshot_to_file(snapshot_file)
-    assert not os.path.exists(snapshot_file.filepath)
+        snapshot_raw.extension._write_snapshot_fossil(snapshot_fossil=snapshot_fossil)
+    assert not os.path.exists(snapshot_fossil.location)
 
 
 class TestClass:
diff --git a/tests/test_single_serializer.py b/tests/test_single_serializer.py
index 0b1b89c..29ffd9a 100644
--- a/tests/test_single_serializer.py
+++ b/tests/test_single_serializer.py
@@ -9,26 +9,26 @@ def testcases(testdir):
         """
         import pytest
 
-        from syrupy.serializers.raw_single import RawSingleSnapshotSerializer
-        from syrupy.serializers.image import (
-            PNGImageSnapshotSerializer,
-            SVGImageSnapshotSerializer,
+        from syrupy.extensions.single_file import SingleFileSnapshotExtension
+        from syrupy.extensions.image import (
+            PNGImageSnapshotExtension,
+            SVGImageSnapshotExtension,
         )
 
 
         @pytest.fixture
         def snapshot_raw(snapshot):
-            return snapshot.with_class(serializer_class=RawSingleSnapshotSerializer)
+            return snapshot.use_extension(SingleFileSnapshotExtension)
 
 
         @pytest.fixture
         def snapshot_png(snapshot):
-            return snapshot.with_class(serializer_class=PNGImageSnapshotSerializer)
+            return snapshot.use_extension(PNGImageSnapshotExtension)
 
 
         @pytest.fixture
         def snapshot_svg(snapshot):
-            return snapshot.with_class(serializer_class=SVGImageSnapshotSerializer)
+            return snapshot.use_extension(SVGImageSnapshotExtension)
         """
     )
     return {

pip install -e .[dev,test]
parse_log_pytest
requirements.txt
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==19.3.0
backports.tarfile==1.2.0
black==23.7.0
build==1.2.2.post1
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
codecov==2.1.13
colored==1.4.4
configparser==7.2.0
coverage==7.8.0
cryptography==44.0.2
docutils==0.21.2
exceptiongroup==1.2.2
flake8==7.2.0
flake8-bugbear==24.8.19
flake8-builtins==2.5.0
flake8-comprehensions==3.16.0
flake8-i18n==0.1.0
id==1.5.0
idna==3.10
importlib_metadata==8.6.1
iniconfig==2.1.0
invoke==2.2.0
isort==6.0.1
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jeepney==0.9.0
keyring==25.6.0
markdown-it-py==3.0.0
mccabe==0.7.0
mdurl==0.1.2
more-itertools==10.6.0
mypy==1.4.0
mypy-extensions==1.0.0
nh3==0.2.21
packaging==24.2
pathspec==0.12.1
pip-tools==7.4.1
platformdirs==4.3.7
pluggy==1.5.0
py-githooks==1.1.1
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.1
Pygments==2.19.1
pyproject_hooks==1.2.0
pytest==8.3.5
readme_renderer==44.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==13.9.0
SecretStorage==3.3.3
semver==3.0.4
-e git+https://github.com/tophat/syrupy.git@cb17855e01fa79ffe49b7114a4b5cf1b960509b3#egg=syrupy
tomli==2.2.1
twine==6.1.0
typing-extensions==3.10.0.2
urllib3==2.3.0
zipp==3.21.0

name: syrupy
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==19.3.0
      - backports-tarfile==1.2.0
      - black==23.7.0
      - build==1.2.2.post1
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - click==8.1.8
      - codecov==2.1.13
      - colored==1.4.4
      - configparser==7.2.0
      - coverage==7.8.0
      - cryptography==44.0.2
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - flake8-bugbear==24.8.19
      - flake8-builtins==2.5.0
      - flake8-comprehensions==3.16.0
      - flake8-i18n==0.1.0
      - id==1.5.0
      - idna==3.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - invoke==2.2.0
      - isort==6.0.1
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jeepney==0.9.0
      - keyring==25.6.0
      - markdown-it-py==3.0.0
      - mccabe==0.7.0
      - mdurl==0.1.2
      - more-itertools==10.6.0
      - mypy==1.4.0
      - mypy-extensions==1.0.0
      - nh3==0.2.21
      - packaging==24.2
      - pathspec==0.12.1
      - pip-tools==7.4.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - py-githooks==1.1.1
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pyproject-hooks==1.2.0
      - pytest==8.3.5
      - readme-renderer==44.0
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==13.9.0
      - secretstorage==3.3.3
      - semver==3.0.4
      - syrupy==2025.3.31.114819728671
      - tomli==2.2.1
      - twine==6.1.0
      - typing-extensions==3.10.0.2
      - urllib3==2.3.0
      - zipp==3.21.0
prefix: /opt/conda/envs/syrupy

tests/test_custom_serializer.py::test_warns_on_snapshot_name tests/test_image_serializer.py::test_image tests/test_integration.py::test_collection tests/test_integration.py::test_collection_parametrized tests/test_integration.py::test_missing_snapshots tests/test_integration.py::test_injected_fixture tests/test_integration.py::test_generated_snapshots tests/test_integration.py::test_updated_snapshots tests/test_integration.py::test_unused_snapshots tests/test_integration.py::test_unused_snapshots_warning tests/test_integration.py::test_removed_snapshots tests/test_integration.py::test_removed_snapshot_fossil tests/test_integration.py::test_removed_empty_snapshot_fossil_only tests/test_integration.py::test_removed_hanging_snapshot_fossil tests/test_raw_single_serializer.py::test_does_not_write_non_binary tests/test_raw_single_serializer.py::TestClass::test_class_method_name tests/test_raw_single_serializer.py::TestClass::test_class_method_parametrized[x] tests/test_raw_single_serializer.py::TestClass::test_class_method_parametrized[y] tests/test_raw_single_serializer.py::TestClass::test_class_method_parametrized[z] tests/test_single_serializer.py::test_unsaved_snapshots tests/test_single_serializer.py::test_failed_snapshots tests/test_single_serializer.py::test_generated_snapshots tests/test_single_serializer.py::test_unmatched_snapshots tests/test_single_serializer.py::test_updated_snapshots
test_file.py::test_used test_file.py::test_passed_raw test_file.py::test_failed_raw test_file.py::test_failed_image tests/test_integration.py::test_failing_snapshots_diff
src/syrupy/extensions/single_file.py src/syrupy/serializers/base.py src/syrupy/constants.py src/syrupy/serializers/__init__.py src/syrupy/data.py src/syrupy/serializers/amber.py src/syrupy/report.py README.md src/syrupy/serializers/image.py src/syrupy/serializers/raw_single.py src/syrupy/session.py src/syrupy/__init__.py src/syrupy/assertion.py src/syrupy/extensions/__init__.py
src/syrupy/extensions/single_file.py src/syrupy/constants.py src/syrupy/extensions/amber.py src/syrupy/extensions/image.py src/syrupy/serializers/__init__.py src/syrupy/data.py src/syrupy/report.py README.md src/syrupy/session.py src/syrupy/serializers/raw_single.py src/syrupy/extensions/base.py src/syrupy/__init__.py src/syrupy/assertion.py src/syrupy/extensions/__init__.py