diff --git a/swh/model/hypothesis_strategies.py b/swh/model/hypothesis_strategies.py
index 3a205a3ca779def557ab2eb11b8d615b8e9d2c8e..ca568ee7e909421192f1cb9ae62111ca86ea5670 100644
--- a/swh/model/hypothesis_strategies.py
+++ b/swh/model/hypothesis_strategies.py
@@ -3,6 +3,7 @@
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+import attr
 import datetime
 
 from hypothesis.strategies import (
@@ -94,9 +95,10 @@ def releases(draw):
         author=none(),
         date=none(),
         target=sha1_git()))
-    rel.date = date
-    rel.author = author
-    return rel
+    return attr.evolve(
+        rel,
+        date=date,
+        author=author)
 
 
 def revision_metadata():
diff --git a/swh/model/model.py b/swh/model/model.py
index 217e3632b6d2976a55b90f54f581ccf68bcb7776..baf4f54110b860e6967de9871065a747acd0a9e7 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -51,7 +51,7 @@ class BaseModel:
         return cls(**d)
 
 
-@attr.s
+@attr.s(frozen=True)
 class Person(BaseModel):
     """Represents the author/committer of a revision or release."""
     name = attr.ib(type=bytes)
@@ -59,7 +59,7 @@ class Person(BaseModel):
     fullname = attr.ib(type=bytes)
 
 
-@attr.s
+@attr.s(frozen=True)
 class Timestamp(BaseModel):
     """Represents a naive timestamp from a VCS."""
     seconds = attr.ib(type=int)
@@ -78,7 +78,7 @@ class Timestamp(BaseModel):
             raise ValueError('Microseconds must be in [0, 1000000[.')
 
 
-@attr.s
+@attr.s(frozen=True)
 class TimestampWithTimezone(BaseModel):
     """Represents a TZ-aware timestamp from a VCS."""
     timestamp = attr.ib(type=Timestamp)
@@ -105,7 +105,7 @@ class TimestampWithTimezone(BaseModel):
             negative_utc=d['negative_utc'])
 
 
-@attr.s
+@attr.s(frozen=True)
 class Origin(BaseModel):
     """Represents a software source: a VCS and an URL."""
     url = attr.ib(type=str)
@@ -117,7 +117,7 @@ class Origin(BaseModel):
         return r
 
 
-@attr.s
+@attr.s(frozen=True)
 class OriginVisit(BaseModel):
     """Represents a visit of an origin at a given point in time, by a
     SWH loader."""
@@ -176,7 +176,7 @@ class ObjectType(Enum):
     SNAPSHOT = 'snapshot'
 
 
-@attr.s
+@attr.s(frozen=True)
 class SnapshotBranch(BaseModel):
     """Represents one of the branches of a snapshot."""
     target = attr.ib(type=bytes)
@@ -198,7 +198,7 @@ class SnapshotBranch(BaseModel):
             target_type=TargetType(d['target_type']))
 
 
-@attr.s
+@attr.s(frozen=True)
 class Snapshot(BaseModel):
     """Represents the full state of an origin at a given point in time."""
     id = attr.ib(type=Sha1Git)
@@ -214,7 +214,7 @@ class Snapshot(BaseModel):
             })
 
 
-@attr.s
+@attr.s(frozen=True)
 class Release(BaseModel):
     id = attr.ib(type=Sha1Git)
     name = attr.ib(type=bytes)
@@ -261,7 +261,7 @@ class RevisionType(Enum):
     MERCURIAL = 'hg'
 
 
-@attr.s
+@attr.s(frozen=True)
 class Revision(BaseModel):
     id = attr.ib(type=Sha1Git)
     message = attr.ib(type=bytes)
@@ -291,7 +291,7 @@ class Revision(BaseModel):
             **d)
 
 
-@attr.s
+@attr.s(frozen=True)
 class DirectoryEntry(BaseModel):
     name = attr.ib(type=bytes)
     type = attr.ib(type=str,
@@ -301,7 +301,7 @@ class DirectoryEntry(BaseModel):
     """Usually one of the values of `swh.model.from_disk.DentryPerms`."""
 
 
-@attr.s
+@attr.s(frozen=True)
 class Directory(BaseModel):
     id = attr.ib(type=Sha1Git)
     entries = attr.ib(type=List[DirectoryEntry])
@@ -314,7 +314,7 @@ class Directory(BaseModel):
                      for entry in d['entries']])
 
 
-@attr.s
+@attr.s(frozen=True)
 class Content(BaseModel):
     sha1 = attr.ib(type=bytes)
     sha1_git = attr.ib(type=Sha1Git)
diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py
index b2cc3edc5fa9c2a6cb361942b9ed9f56a6b83ca0..2900cd1f9fd6862a4bbd605e1e51fccec50cc6b6 100644
--- a/swh/model/tests/test_model.py
+++ b/swh/model/tests/test_model.py
@@ -3,6 +3,7 @@
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+import attr
 import copy
 
 from hypothesis import given
@@ -44,8 +45,9 @@ def test_todict_origin_visits(origin_visit):
     obj = origin_visit.to_dict()
 
     assert 'type' not in obj['origin']
-    origin_visit.origin.type = None
-    assert origin_visit == type(origin_visit).from_dict(obj)
+    origin2 = attr.evolve(origin_visit.origin, type=None)
+    origin_visit2 = attr.evolve(origin_visit, origin=origin2)
+    assert origin_visit2 == type(origin_visit).from_dict(obj)
 
 
 def test_content_get_hash():