diff --git a/CHANGELOG.md b/CHANGELOG.md
index de391252afd079f9cde739653362ec67423e8f6b..3656f0ce0677644108e382c170eb7d2923e792e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,2 +1,3 @@
 # v2.17.12
 
+* added deep comparison
diff --git a/docs/coding/functions.rst b/docs/coding/functions.rst
index 828ab5fddd37d18bcbb07520b01fa0be210abcff..a929934062f5d55757d6e714f87cf04da7ce9e93 100644
--- a/docs/coding/functions.rst
+++ b/docs/coding/functions.rst
@@ -110,3 +110,16 @@ Without running into `TypeError: metaclass conflict: the metaclass of a derived
 Following function will help with that:
 
 .. autofunction:: satella.coding.metaclass_maker
+
+Deep comparison
+---------------
+
+To analyze why two objects don't compare the same, you can use the following functions:
+
+.. autofunction:: satella.coding.assert_equal
+
+.. autoclass:: satella.coding.Inequal
+    :members:
+
+.. autoclass:: satella.coding.InequalityReason
+    :members:
diff --git a/satella/__init__.py b/satella/__init__.py
index 18ad53ff5ab5f25beda8c3983a04ea4f9465bbcf..154feec1b6f40b8d9ae8fb7374a4d4d3c0596361 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1 +1 @@
-__version__ = '2.17.12a1'
+__version__ = '2.17.12a2'
diff --git a/satella/coding/__init__.py b/satella/coding/__init__.py
index d30cbf4308887def9778fd7c804695170ee2e77c..1d58a54bb1864415f7401fb26b4b3b241c3680f3 100644
--- a/satella/coding/__init__.py
+++ b/satella/coding/__init__.py
@@ -20,8 +20,10 @@ from .overloading import overload, class_or_instancemethod
 from .recast_exceptions import rethrow_as, silence_excs, catch_exception, log_exceptions, \
     raises_exception, reraise_as
 from .expect_exception import expect_exception
+from .deep_compare import assert_equal, InequalityReason, Inequal
 
 __all__ = [
+    'assert_equal', 'InequalityReason', 'Inequal',
     'Closeable', 'contains', 'enum_value', 'reraise_as',
     'expect_exception',
     'overload', 'class_or_instancemethod',
diff --git a/satella/coding/deep_compare.py b/satella/coding/deep_compare.py
new file mode 100644
index 0000000000000000000000000000000000000000..530b70d8979b366ea48b27b129a79c200af442a3
--- /dev/null
+++ b/satella/coding/deep_compare.py
@@ -0,0 +1,63 @@
+import enum
+
+
+class InequalityReason(enum.IntEnum):
+    NOT_EQUAL = 0           #: direct eq yielded not equal
+    LENGTH_MISMATCH = 1     #: length didn't match
+    KEY_NOT_FOUND = 2       #: key given as obj1 was not found
+
+
+class Inequal(Exception):
+    """
+    An exception raised by :meth:`~satella.coding.deep_compare` if two objects don't match
+
+    :ivar obj1: first object that was not equal, or key name
+    :ivar obj2: second object that was not equal, or None
+    :ivar reason: (:class:`~satella.coding.InequalityReason`) reason for inequality
+    """
+    def __init__(self, obj1, obj2, reason: InequalityReason):
+        self.obj1 = obj1
+        self.obj2 = obj2
+        self.reason = reason
+
+
+def assert_equal(a, b):
+    """
+    Assert that two values are equal. If not, an :class:`satella.coding.Inequality` exception
+    will be thrown.
+
+    Objects are tried to compare using it's :code:`__eq__`.
+
+    :param a: first value to compare
+    :param b: second value to compare
+    :raises Inequal: objects were not equal
+    """
+    if isinstance(a, (int, float, str, bytes, bytearray, type(None))):
+        if a != b:
+            raise Inequal(a, b, InequalityReason.NOT_EQUAL)
+        return
+    elif isinstance(a, (set, frozenset)):
+        if len(a) != len(b):
+            raise Inequal(a, b, InequalityReason.LENGTH_MISMATCH)
+        if a != b:
+            raise Inequal(a, b, InequalityReason.LENGTH_MISMATCH)
+    elif isinstance(a, (list, tuple)):
+        if len(a) != len(b):
+            raise Inequal(a, b, InequalityReason.LENGTH_MISMATCH)
+        for c, d in zip(a, b):
+            assert_equal(c, d)
+    elif isinstance(a, dict):
+        if len(a) != len(b):
+            raise Inequal(a, b, InequalityReason.LENGTH_MISMATCH)
+        for key in a.keys():
+            if key not in b:
+                raise Inequal(key, None, InequalityReason.KEY_NOT_FOUND)
+            assert_equal(a[key], b[key])
+    else:
+        try:
+            if a == b:
+                return
+            raise Inequal(a, b, InequalityReason.NOT_EQUAL)
+        except TypeError:
+            pass
+
diff --git a/tests/test_coding/test_deep_compare.py b/tests/test_coding/test_deep_compare.py
new file mode 100644
index 0000000000000000000000000000000000000000..5d046c3478d971306f35383f7efd3211efd4b328
--- /dev/null
+++ b/tests/test_coding/test_deep_compare.py
@@ -0,0 +1,28 @@
+import unittest
+
+from satella.coding import assert_equal, Inequal
+
+
+class TestDeepCompare(unittest.TestCase):
+    def assertInequal(self, a, b):
+        self.assertRaises(Inequal, lambda: assert_equal(a, b))
+
+    def test_compare(self):
+        assert_equal(2, 2)
+        self.assertInequal(3, 2)
+        self.assertInequal([], [6])
+        self.assertInequal({1: 2}, {3: 4})
+        self.assertInequal({1: 2}, {1: 4})
+        self.assertInequal({1: 2}, {3: 4, 5: 6})
+        assert_equal([1], [1])
+        assert_equal(set([1, 2]), set([2, 1]))
+
+        class Object:
+            def __init__(self, a):
+                self.a = a
+
+            def __eq__(self, other):
+                return self.a == other.a
+
+        assert_equal(Object(3), Object(3))
+        self.assertInequal(Object(3), Object(4))