diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0f9a258de99ba559d280fc5ace74a3f111a9e30e..8845e4070c685230a99958fbebd9377238df32de 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -113,15 +113,8 @@ unittest_py3.13:
   tags: [ docker ]
   stage: test
   needs: [ ]
-  image: python:3.13-rc
-  script:
-    # TODO: Replace by '*python_test_script' as soon as 3.13 has been officially released.
-    # Python docker has problems with tox and pip so use plain pytest here
-    - apt update && apt install -y cargo
-    - touch ~/.pylinkahead.ini
-    - pip install pynose pytest pytest-cov jsonschema>=4.4.0 setuptools
-    - pip install .
-    - python -m pytest unittests
+  image: python:3.13
+  script: *python_test_script
 
 # Trigger building of server image and integration tests
 trigger_build:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de363eb791697fcc53171b6e4d0694da1036e34a..de8099318cdc1480f2cd2c06497a7cd374e65495 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.16.0] - 2024-11-13 ##
+
+### Added ###
+
+* `ParentList` and `PropertyList` now have a `filter` function that allows to select a subset of
+  the contained elements by ID and/or name.
+* Official support for Python 3.13
+* Added arguments to `describe_diff` that allow customizing the labels for the 'old' and the 'new' diffs.
+* Optional `realm` argument for `linkahead_admin.py set_user_password`
+  which defaults to `None`, i.e., the server's default realm.
+
+### Changed ###
+
+* `compare_entities` is now case insensitive with respect to property and
+  recordtype names
+* `_ParentList` is now called `ParentList`
+* `_Properties` is now called `PropertyList`
+* `ParentList.remove` is now case insensitive when a name is used.
+
+### Deprecated ###
+
+* the use of the arguments `old_entity` and `new_entity` in `compare_entities`
+  is now deprecated. Please use `entity0` and `entity1` respectively instead.
+
+### Fixed ###
+
+* [gitlab.indiscale.com#200](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/merge_requests/153)
+  ``linkahead_admin.py`` prints reasonable error messages when users
+  or roles don't exist.
+
 ## [0.15.1] - 2024-08-21 ##
 
 ### Deprecated ###
diff --git a/CITATION.cff b/CITATION.cff
index 3f51bdf839a5e0451f3d3aaf7f128f61b29927fc..123289ca17e8b43446f8f368621debccd8c27469 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -20,6 +20,6 @@ authors:
     given-names: Stefan
     orcid: https://orcid.org/0000-0001-7214-8125
 title: CaosDB - Pylib
-version: 0.15.1
+version: 0.16.0
 doi: 10.3390/data4020083
-date-released: 2024-08-21
+date-released: 2024-11-13
diff --git a/examples/set_permissions.py b/examples/set_permissions.py
index a558bde73897cb6827c93373cc8327efc10e6e15..4657f2cca182b567c761a777df838825f8e89aef 100755
--- a/examples/set_permissions.py
+++ b/examples/set_permissions.py
@@ -37,13 +37,13 @@ from caosdb import administration as admin
 def assert_user_and_role():
     """Make sure that users and roles exist.
 
-After calling this function, there will be a user "jane" with the role "human"
-and the user "xaxys" with the role "alien".  These users and roles are returned.
+    After calling this function, there will be a user "jane" with the role "human"
+    and the user "xaxys" with the role "alien".  These users and roles are returned.
 
-Returns
--------
-out : tuple
-  ((human_user, human_role), (alien_user, alien_role))
+    Returns
+    -------
+    out : tuple
+      ((human_user, human_role), (alien_user, alien_role))
 
     """
     try:
@@ -81,15 +81,15 @@ out : tuple
 def get_entities(count=1):
     """Retrieve one or more entities.
 
-Parameters
-----------
-count : int, optional
-    How many entities to retrieve.
+    Parameters
+    ----------
+    count : int, optional
+        How many entities to retrieve.
 
-Returns
--------
-out : Container
-    A container of retrieved entities, the length is given by the parameter count.
+    Returns
+    -------
+    out : Container
+        A container of retrieved entities, the length is given by the parameter count.
     """
     cont = db.execute_query("FIND RECORD 'Human Food'", flags={
                             "P": "0L{n}".format(n=count)})
@@ -102,20 +102,20 @@ out : Container
 def set_permission(role_grant, role_deny, cont=None, general=False):
     """Set the permissions of some entities.
 
-Parameters
-----------
-role_grant : str
-    Role which is granted permissions.
+    Parameters
+    ----------
+    role_grant : str
+        Role which is granted permissions.
 
-role_deny : str
-    Role which is denied permissions.
+    role_deny : str
+        Role which is denied permissions.
 
-cont : Container
-    Entities for which permissions are set.
+    cont : Container
+        Entities for which permissions are set.
 
-general : bool, optional
-    If True, the permissions for the roles will be set.  If False (the default),
-    permissions for the entities in the container will be set.
+    general : bool, optional
+        If True, the permissions for the roles will be set.  If False (the default),
+        permissions for the entities in the container will be set.
     """
 
     # Set general permissions
@@ -143,23 +143,23 @@ general : bool, optional
 def test_permission(granted_user, denied_user, cont):
     """Tests if the permissions are set correctly for two users.
 
-Parameters
-----------
-granted_user : (str, str)
-    The user which should have permissions to retrieve the entities in `cont`.
-    Given as (user, password).
+    Parameters
+    ----------
+    granted_user : (str, str)
+        The user which should have permissions to retrieve the entities in `cont`.
+        Given as (user, password).
 
-denied_user : (str, str)
-    The user which should have no permission to retrieve the entities in `cont`.
-    Given as (user, password).
+    denied_user : (str, str)
+        The user which should have no permission to retrieve the entities in `cont`.
+        Given as (user, password).
 
-cont :  Container
-    Entities for which permissions are tested.
+    cont :  Container
+        Entities for which permissions are tested.
 
 
-Returns
--------
-None
+    Returns
+    -------
+    None
 
     """
 
diff --git a/setup.py b/setup.py
index 6ad2d0b9ef1e4c07d6519562a0c75c72c51b5b75..b8a04adefa9d8891c10576a733f53f2fa88b981d 100755
--- a/setup.py
+++ b/setup.py
@@ -48,8 +48,8 @@ from setuptools import find_packages, setup
 
 ISRELEASED = True
 MAJOR = 0
-MINOR = 15
-MICRO = 1
+MINOR = 16
+MICRO = 0
 # Do not tag as pre-release until this commit
 # https://github.com/pypa/packaging/pull/515
 # has made it into a release. Probably we should wait for pypa/packaging>=21.4
diff --git a/src/doc/conf.py b/src/doc/conf.py
index 7b127420c281e37e82ee0e64768ae831e30e2798..f25ed399575bb208e699476b06f014abe43ee967 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -25,14 +25,14 @@ import sphinx_rtd_theme  # noqa: E402
 # -- Project information -----------------------------------------------------
 
 project = 'pylinkahead'
-copyright = '2023, IndiScale GmbH'
+copyright = '2024, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.15.1'
+version = '0.16.0'
 # The full version, including alpha/beta/rc tags
 # release = '0.5.2-rc2'
-release = '0.15.1'
+release = '0.16.0'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/src/doc/tutorials/complex_data_models.rst b/src/doc/tutorials/complex_data_models.rst
index 569acdae174a9df9d0d2b5eae9a0084d793cc90c..168cf3b9f0d6839ed8f78beb01ae24fb9d489e88 100644
--- a/src/doc/tutorials/complex_data_models.rst
+++ b/src/doc/tutorials/complex_data_models.rst
@@ -51,18 +51,18 @@ Examples
    # Very complex part of the data model:
    # Case 1: File added to another file
    f2.add_property(p1, value=f1)  # this adds a file property with value first file
-		                  # to the second file
+                          # to the second file
 
    # Case 2: Property added to a property
    p2.add_property(p3, value=27)  # this adds an integer property with value 27 to the
-		                  # double property
+                          # double property
 
    # Case 3: Reference property added to a property
    # The property p2 now has two sub properties, one is pointing to
    # record p2 which itself has the property p2, therefore this can be
    # considered a loop in the data model.
    p2.add_property(p4, value=r2)  # this adds a reference property pointing to
-		                  # record 2 to the double property
+                          # record 2 to the double property
 
    # Insert a container containing all the newly created entities:
    c = db.Container().extend([rt1, rt2, r1, r2, f1, p1, p2, p3, f2, p4])
@@ -75,3 +75,54 @@ Examples
    b = input("Press any key to cleanup.")
    # cleanup everything after the user presses any button.
    c.delete()
+
+
+Finding parents and properties
+--------
+To find a specific parent or property of an Entity, its
+ParentList or PropertyList can be filtered using names, ids, or
+entities. A short example:
+
+.. code-block:: python3
+
+   import linkahead as db
+
+   # Setup a record with six properties
+   r = db.Record()
+   p1_1 = db.Property(id=101, name="Property 1")
+   p1_2 = db.Property(name="Property 1")
+   p2_1 = db.Property(id=102, name="Property 2")
+   p2_2 = db.Property(id=102)
+   p2_3 = db.Property(id=102, name="Other Property")
+   p3 = db.Property(id=104, name="Other Property")
+   r.add_property(p1_1).add_property(p1_2).add_property(p2_1)
+   r.add_property(p2_2).add_property(p2_3).add_property(p3)
+   properties = r.properties
+
+   # As r only has one property with id 101, this returns a list containing only p1_1
+   properties.filter(pid=101)
+   # Result: [p1_1]
+
+   # Filtering with name="Property 1" returns both p1_1 and p1_2, as they share their name
+   properties.filter(name="Property 1")
+   # Result: [p1_1, p1_2]
+
+   #  If both name and pid are given, matching is based only on pid for all entities that have an id
+   properties.filter(pid="102", name="Other Property")
+   # Result: [p2_1, p2_2, p2_3]
+
+   # However, filtering with name="Property 1" and id=101 returns both p1_1 and p1_2, because
+   # p1_2 does not have an id and matches the name
+   properties.filter(pid="101", name="Property 1")
+   # Result: [p1_1, p1_2]
+
+   # We can also filter using an entity, in which case the name and id of the entity are used:
+   properties.filter(pid="102", name="Property 2") == properties.filter(p2_1)
+   # Result: True
+
+   # If we only need properties that match both id and name, we can set the parameter
+   # conjunction to True:
+   properties.filter(pid="102", name="Property 2", conjunction=True)
+   # Result: [p2_1]
+
+The filter function of ParentList works analogously.
diff --git a/src/linkahead/__init__.py b/src/linkahead/__init__.py
index cd54f8f4e05326579521fbbf226f027d32fa616e..567748e3b3a58fb73b91f652d82ed10f818d6014 100644
--- a/src/linkahead/__init__.py
+++ b/src/linkahead/__init__.py
@@ -42,7 +42,7 @@ from .common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, LIST,
                               REFERENCE, TEXT)
 # Import of the basic  API classes:
 from .common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED,
-                            SUGGESTED, Container, DropOffBox, Entity, File,
+                            SUGGESTED, Container, DropOffBox, Entity, File, Parent,
                             Info, Message, Permissions, Property, Query,
                             QueryTemplate, Record, RecordType, delete,
                             execute_query, get_global_acl,
diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py
index 4ae8edd16f1fdc00eb7ba2c17661eea6e114885e..49336aa8db24fba663337185c5c37a346330c4cd 100644
--- a/src/linkahead/apiutils.py
+++ b/src/linkahead/apiutils.py
@@ -28,10 +28,11 @@
 
 """
 from __future__ import annotations
+
 import logging
 import warnings
 from collections.abc import Iterable
-from typing import Any, Union, Optional
+from typing import Any, Optional, Union
 
 from .common.datatype import is_reference
 from .common.models import (SPECIAL_ATTRIBUTES, Container, Entity, File,
@@ -179,183 +180,313 @@ def getCommitIn(folder):
     return get_commit_in(folder)
 
 
-def compare_entities(old_entity: Entity,
-                     new_entity: Entity,
-                     compare_referenced_records: bool = False
+def compare_entities(entity0: Optional[Entity] = None,
+                     entity1: Optional[Entity] = None,
+                     compare_referenced_records: bool = False,
+                     entity_name_id_equivalency: bool = False,
+                     old_entity: Optional[Entity] = None,
+                     new_entity: Optional[Entity] = None,
                      ) -> tuple[dict[str, Any], dict[str, Any]]:
-    """Compare two entites.
-
-    Return a tuple of dictionaries, the first index belongs to additional information for old
-    entity, the second index belongs to additional information for new entity.
-
-    Additional information means in detail:
-    - Additional parents (a list under key "parents")
-    - Information about properties:
-      - Each property lists either an additional property or a property with a changed:
+    """Compare two entities.
+
+    Returns two dicts listing the differences between the two entities. The
+    order of the two returned dicts corresponds to the two input entities.
+    The dicts contain two keys, 'parents' and 'properties'. The list saved
+    under the 'parents' key contains those parents of the respective entity
+    that are missing in the other entity, and the 'properties' dict contains
+    properties and SPECIAL_ATTRIBUTES if they are missing or different from
+    their counterparts in the other entity.
+
+    The value of the properties dict for each listed property is again a dict
+    detailing the differences between this property and its counterpart.
+    The characteristics that are checked to determine whether two properties
+    match are the following:
         - datatype
-        - importance or
-        - value (not implemented yet)
-
-        In case of changed information the value listed under the respective key shows the
-        value that is stored in the respective entity.
-
-    If `compare_referenced_records` is `True`, also referenced entities will be
-    compared using this function (which is then called with
-    `compare_referenced_records = False` to prevent infinite recursion in case
-    of circular references).
-
-    Parameters
-    ----------
-    old_entity, new_entity : Entity
-        Entities to be compared
-    compare_referenced_records : bool, optional
-        Whether to compare referenced records in case of both, `old_entity` and
-        `new_entity`, have the same reference properties and both have a Record
-        object as value. If set to `False`, only the corresponding Python
-        objects are compared which may lead to unexpected behavior when
-        identical records are stored in different objects. Default is False.
-
+        - importance
+        - value
+    If any of these characteristics differ for a property, the respective
+    string (datatype, importance, value) is added as a key to the dict of the
+    property with its value being the characteristics value,
+    e.g. {"prop": {"value": 6, 'importance': 'SUGGESTED'}}. Except: None as
+    value is not added to the dict.
+    If a property is of type LIST, the comparison is order-sensitive.
+
+    Comparison of multi-properties is not yet supported, so should either
+    entity have several instances of one Property, the comparison is aborted
+    and an error is raised.
+
+    Two parents match if their name and id are the same, any further
+    differences are ignored.
+
+    Should records referenced in the value field not be checked for equality
+    between the entities but for equivalency, this is possible by setting the
+    parameter compare_referenced_records.
+
+    Params
+    ------
+    entity0                : Entity
+                                First entity to be compared.
+    entity1                : Entity
+                                Second entity to be compared.
+    compare_referenced_records: bool, default: False
+                                If set to True, values with referenced records
+                                are not checked for equality but for
+                                equivalency using this function.
+                                compare_referenced_records is set to False for
+                                these recursive calls, so references of
+                                references need to be equal. If set to `False`,
+                                only the Python objects are compared, which may
+                                lead to unexpected behavior.
+    entity_name_id_equivalency: bool, default: False
+                                If set to True, the comparison between an
+                                entity and an int or str also checks whether
+                                the int/str matches the name or id of the
+                                entity, so Entity(id=100) == 100 == "100".
     """
-    olddiff: dict[str, Any] = {"properties": {}, "parents": []}
-    newdiff: dict[str, Any] = {"properties": {}, "parents": []}
-
-    if old_entity is new_entity:
-        return (olddiff, newdiff)
-
-    if type(old_entity) is not type(new_entity):
+    # ToDo: Discuss intended behaviour
+    # Questions that need clarification:
+    #    - What is intended behaviour for multi-properties and multi-parents?
+    #    - Do different inheritance levels for parents count as a difference?
+    #    - Do we care about parents and properties of properties?
+    #    - Should there be a more detailed comparison of parents without id?
+    #    - Revisit filter - do we care about RecordType when matching?
+    #      How to treat None?
+    #    - Should matching of parents also take the recordtype into account
+    #      for parents that have a name but no id?
+    # Suggestions for enhancements:
+    #    - For the comparison of entities in value and properties, consider
+    #      keeping a list of traversed entities, not only look at first layer
+    #    - Make the empty_diff functionality faster by adding a parameter to
+    #      this function so that it returns after the first found difference?
+    #    - Add parameter to restrict diff to some characteristics
+    #    - Implement comparison of date where one is a string and the other is
+    #      datetime
+    if entity0 is None and old_entity is None:
+        raise ValueError("Please provide the first entity as first argument (`entity0`)")
+    if entity1 is None and new_entity is None:
+        raise ValueError("Please provide the second entity as second argument (`entity1`)")
+    if old_entity is not None:
+        warnings.warn("Please use 'entity0' instead of 'old_entity'.", DeprecationWarning)
+        if entity0 is not None:
+            raise ValueError("You cannot use both entity0 and old_entity")
+        entity0 = old_entity
+    if new_entity is not None:
+        warnings.warn("Please use 'entity1' instead of 'new_entity'.", DeprecationWarning)
+        if entity1 is not None:
+            raise ValueError("You cannot use both entity1 and new_entity")
+        entity1 = new_entity
+
+    diff: tuple = ({"properties": {}, "parents": []},
+                   {"properties": {}, "parents": []})
+
+    if entity0 is entity1:
+        return diff
+
+    if type(entity0) is not type(entity1):
         raise ValueError(
             "Comparison of different Entity types is not supported.")
 
+    # compare special attributes
     for attr in SPECIAL_ATTRIBUTES:
-        try:
-            oldattr = old_entity.__getattribute__(attr)
-            old_entity_attr_exists = True
-        except BaseException:
-            old_entity_attr_exists = False
-        try:
-            newattr = new_entity.__getattribute__(attr)
-            new_entity_attr_exists = True
-        except BaseException:
-            new_entity_attr_exists = False
-
-        if old_entity_attr_exists and (oldattr == "" or oldattr is None):
-            old_entity_attr_exists = False
-
-        if new_entity_attr_exists and (newattr == "" or newattr is None):
-            new_entity_attr_exists = False
-
-        if not old_entity_attr_exists and not new_entity_attr_exists:
+        if attr == "value":
             continue
 
-        if ((old_entity_attr_exists ^ new_entity_attr_exists)
-                or (oldattr != newattr)):
-
-            if old_entity_attr_exists:
-                olddiff[attr] = oldattr
+        attr0 = entity0.__getattribute__(attr)
+        # we consider "" and None to be nonexistent
+        attr0_unset = (attr0 == "" or attr0 is None)
 
-            if new_entity_attr_exists:
-                newdiff[attr] = newattr
+        attr1 = entity1.__getattribute__(attr)
+        # we consider "" and None to be nonexistent
+        attr1_unset = (attr1 == "" or attr1 is None)
 
-    # properties
-
-    for prop in old_entity.properties:
-        matching = [p for p in new_entity.properties if p.name == prop.name]
+        # in both entities the current attribute is not set
+        if attr0_unset and attr1_unset:
+            continue
 
+        # treat datatype separately if one datatype is an object and the other
+        # a string or int, and therefore may be a name or id
+        if attr == "datatype":
+            if not attr0_unset and not attr1_unset:
+                if isinstance(attr0, RecordType):
+                    if attr0.name == attr1:
+                        continue
+                    if str(attr0.id) == str(attr1):
+                        continue
+                if isinstance(attr1, RecordType):
+                    if attr1.name == attr0:
+                        continue
+                    if str(attr1.id) == str(attr0):
+                        continue
+
+        # add to diff if attr has different values or is not set for one entity
+        if (attr0_unset != attr1_unset) or (attr0 != attr1):
+            diff[0][attr] = attr0
+            diff[1][attr] = attr1
+
+    # compare value
+    ent0_val, ent1_val = entity0.value, entity1.value
+    if ent0_val != ent1_val:
+        same_value = False
+
+        # Surround scalar values with a list to avoid code duplication -
+        # this way, the scalar values can be checked against special cases
+        # (compare refs, entity id equivalency etc.) in the list loop
+        if not isinstance(ent0_val, list) and not isinstance(ent1_val, list):
+            ent0_val, ent1_val = [ent0_val], [ent1_val]
+
+        if isinstance(ent0_val, list) and isinstance(ent1_val, list):
+            # lists can't be the same if the lengths are different
+            if len(ent0_val) == len(ent1_val):
+                lists_match = True
+                for val0, val1 in zip(ent0_val, ent1_val):
+                    if val0 == val1:
+                        continue
+                    # Compare Entities
+                    if (compare_referenced_records and
+                            isinstance(val0, Entity) and isinstance(val1, Entity)):
+                        try:
+                            same = empty_diff(val0, val1, False,
+                                              entity_name_id_equivalency)
+                        except (ValueError, NotImplementedError):
+                            same = False
+                        if same:
+                            continue
+                    # Compare Entity name and id
+                    if entity_name_id_equivalency:
+                        if (isinstance(val0, Entity)
+                                and isinstance(val1, (int, str))):
+                            if (str(val0.id) == str(val1)
+                                    or str(val0.name) == str(val1)):
+                                continue
+                        if (isinstance(val1, Entity)
+                                and isinstance(val0, (int, str))):
+                            if (str(val1.id) == str(val0)
+                                    or str(val1.name) == str(val0)):
+                                continue
+                    # val0 and val1 could not be matched
+                    lists_match = False
+                    break
+                if lists_match:
+                    same_value = True
+
+        if not same_value:
+            diff[0]["value"] = entity0.value
+            diff[1]["value"] = entity1.value
+
+    # compare properties
+    for prop in entity0.properties:
+        matching = entity1.properties.filter(name=prop.name, pid=prop.id)
         if len(matching) == 0:
-            olddiff["properties"][prop.name] = {}
+            # entity1 has prop, entity0 does not
+            diff[0]["properties"][prop.name] = {}
         elif len(matching) == 1:
-            newdiff["properties"][prop.name] = {}
-            olddiff["properties"][prop.name] = {}
-
-            if (old_entity.get_importance(prop.name) !=
-                    new_entity.get_importance(prop.name)):
-                olddiff["properties"][prop.name]["importance"] = \
-                    old_entity.get_importance(prop.name)
-                newdiff["properties"][prop.name]["importance"] = \
-                    new_entity.get_importance(prop.name)
-
-            if (prop.datatype != matching[0].datatype):
-                olddiff["properties"][prop.name]["datatype"] = prop.datatype
-                newdiff["properties"][prop.name]["datatype"] = \
-                    matching[0].datatype
-
-            if (prop.unit != matching[0].unit):
-                olddiff["properties"][prop.name]["unit"] = prop.unit
-                newdiff["properties"][prop.name]["unit"] = \
-                    matching[0].unit
-
-            if (prop.value != matching[0].value):
-                # basic comparison of value objects says they are different
-                same_value = False
-                if compare_referenced_records:
-                    # scalar reference
-                    if isinstance(prop.value, Entity) and isinstance(matching[0].value, Entity):
-                        # explicitely not recursive to prevent infinite recursion
-                        same_value = empty_diff(
-                            prop.value, matching[0].value, compare_referenced_records=False)
-                    # list of references
-                    elif isinstance(prop.value, list) and isinstance(matching[0].value, list):
-                        # all elements in both lists actually are entity objects
-                        # TODO: check, whether mixed cases can be allowed or should lead to an error
-                        if (all([isinstance(x, Entity) for x in prop.value])
-                                and all([isinstance(x, Entity) for x in matching[0].value])):
-                            # can't be the same if the lengths are different
-                            if len(prop.value) == len(matching[0].value):
-                                # do a one-by-one comparison:
-                                # the values are the same if all diffs are empty
-                                same_value = all(
-                                    [empty_diff(x, y, False) for x, y
-                                     in zip(prop.value, matching[0].value)])
-
-                if not same_value:
-                    olddiff["properties"][prop.name]["value"] = prop.value
-                    newdiff["properties"][prop.name]["value"] = \
-                        matching[0].value
-
-            if (len(newdiff["properties"][prop.name]) == 0
-                    and len(olddiff["properties"][prop.name]) == 0):
-                newdiff["properties"].pop(prop.name)
-                olddiff["properties"].pop(prop.name)
+            diff[0]["properties"][prop.name] = {}
+            diff[1]["properties"][prop.name] = {}
+            propdiff = (diff[0]["properties"][prop.name],
+                        diff[1]["properties"][prop.name])
+
+            # We should compare the wrapped properties instead of the
+            # wrapping entities if possible:
+            comp1, comp2 = prop, matching[0]
+            if (comp1._wrapped_entity is not None
+                    and comp2._wrapped_entity is not None):
+                comp1, comp2 = comp1._wrapped_entity, comp2._wrapped_entity
+            # Recursive call to determine the differences between properties
+            # Note: Can lead to infinite recursion if two properties have
+            # themselves or each other as subproperties
+            od, nd = compare_entities(comp1, comp2, compare_referenced_records,
+                                      entity_name_id_equivalency)
+            # We do not care about parents and properties here, discard
+            od.pop("parents")
+            od.pop("properties")
+            nd.pop("parents")
+            nd.pop("properties")
+            # use the remaining diff
+            propdiff[0].update(od)
+            propdiff[1].update(nd)
+
+            # As the importance of a property is an attribute of the record
+            # and not the property, it is not contained in the diff returned
+            # by compare_entities and needs to be added separately
+            if (entity0.get_importance(prop) !=
+                    entity1.get_importance(matching[0])):
+                propdiff[0]["importance"] = entity0.get_importance(prop)
+                propdiff[1]["importance"] = entity1.get_importance(matching[0])
+
+            # in case there is no difference, we remove the dict keys again
+            if len(propdiff[0]) == 0 and len(propdiff[1]) == 0:
+                diff[0]["properties"].pop(prop.name)
+                diff[1]["properties"].pop(prop.name)
 
         else:
             raise NotImplementedError(
                 "Comparison not implemented for multi-properties.")
 
-    for prop in new_entity.properties:
-        if len([0 for p in old_entity.properties if p.name == prop.name]) == 0:
-            newdiff["properties"][prop.name] = {}
-
-    # parents
-
-    for parent in old_entity.parents:
-        if len([0 for p in new_entity.parents if p.name == parent.name]) == 0:
-            olddiff["parents"].append(parent.name)
-
-    for parent in new_entity.parents:
-        if len([0 for p in old_entity.parents if p.name == parent.name]) == 0:
-            newdiff["parents"].append(parent.name)
-
-    return (olddiff, newdiff)
-
+    # we have not yet compared properties that do not exist in entity0
+    for prop in entity1.properties:
+        # check how often the property appears in entity0
+        num_prop_in_ent0 = len(entity0.properties.filter(prop))
+        if num_prop_in_ent0 == 0:
+            # property is only present in entity0 - add to diff
+            diff[1]["properties"][prop.name] = {}
+        if num_prop_in_ent0 > 1:
+            # Check whether the property is present multiple times in entity0
+            # and raise error - result would be incorrect
+            raise NotImplementedError(
+                "Comparison not implemented for multi-properties.")
 
-def empty_diff(old_entity: Entity, new_entity: Entity,
-               compare_referenced_records: bool = False) -> bool:
+    # compare parents
+    for index, parents, other_entity in [(0, entity0.parents, entity1),
+                                         (1, entity1.parents, entity0)]:
+        for parent in parents:
+            matching = other_entity.parents.filter(parent)
+            if len(matching) == 0:
+                diff[index]["parents"].append(parent.name)
+                continue
+
+    return diff
+
+
+def empty_diff(entity0: Entity,
+               entity1: Entity,
+               compare_referenced_records: bool = False,
+               entity_name_id_equivalency: bool = False,
+               old_entity: Optional[Entity] = None,
+               new_entity: Optional[Entity] = None,
+               ) -> bool:
     """Check whether the `compare_entities` found any differences between
-    old_entity and new_entity.
+    entity0 and entity1.
 
     Parameters
     ----------
-    old_entity, new_entity : Entity
+    entity0, entity1 : Entity
         Entities to be compared
     compare_referenced_records : bool, optional
-        Whether to compare referenced records in case of both, `old_entity` and
-        `new_entity`, have the same reference properties and both have a Record
+        Whether to compare referenced records in case of both, `entity0` and
+        `entity1`, have the same reference properties and both have a Record
         object as value.
-
+    entity_name_id_equivalency : bool, optional
+        If set to True, the comparison between an entity and an int or str also
+        checks whether the int/str matches the name or id of the entity, so
+        Entity(id=100) == 100 == "100".
     """
-    olddiff, newdiff = compare_entities(
-        old_entity, new_entity, compare_referenced_records)
-    for diff in [olddiff, newdiff]:
+    if entity0 is None and old_entity is None:
+        raise ValueError("Please provide the first entity as first argument (`entity0`)")
+    if entity1 is None and new_entity is None:
+        raise ValueError("Please provide the second entity as second argument (`entity1`)")
+    if old_entity is not None:
+        warnings.warn("Please use 'entity0' instead of 'old_entity'.", DeprecationWarning)
+        if entity0 is not None:
+            raise ValueError("You cannot use both entity0 and old_entity")
+        entity0 = old_entity
+    if new_entity is not None:
+        warnings.warn("Please use 'entity1' instead of 'new_entity'.", DeprecationWarning)
+        if entity1 is not None:
+            raise ValueError("You cannot use both entity1 and new_entity")
+        entity1 = new_entity
+    e0diff, e1diff = compare_entities(entity0, entity1, compare_referenced_records,
+                                      entity_name_id_equivalency)
+    for diff in [e0diff, e1diff]:
         for key in ["parents", "properties"]:
             if len(diff[key]) > 0:
                 # There is a difference somewhere in the diff
@@ -376,9 +507,9 @@ def merge_entities(entity_a: Entity,
                    ) -> Entity:
     """Merge entity_b into entity_a such that they have the same parents and properties.
 
-    datatype, unit, value, name and description will only be changed in entity_a
-    if they are None for entity_a and set for entity_b. If there is a
-    corresponding value for entity_a different from None, an
+    The attributes datatype, unit, value, name and description will only be changed
+    in entity_a if they are None for entity_a and set for entity_b. If one of those attributes is
+    set in both entities and they differ, then an
     EntityMergeConflictError will be raised to inform about an unresolvable merge
     conflict.
 
@@ -386,8 +517,6 @@ def merge_entities(entity_a: Entity,
 
     Returns entity_a.
 
-    WARNING: This function is currently experimental and insufficiently tested. Use with care.
-
     Parameters
     ----------
     entity_a, entity_b : Entity
@@ -420,12 +549,10 @@ def merge_entities(entity_a: Entity,
 
     """
 
-    logger.warning(
-        "This function is currently experimental and insufficiently tested. Use with care.")
-
     # Compare both entities:
-    diff_r1, diff_r2 = compare_entities(
-        entity_a, entity_b, compare_referenced_records=merge_references_with_empty_diffs)
+    diff_r1, diff_r2 = compare_entities(entity_a, entity_b,
+                                        entity_name_id_equivalency=merge_id_with_resolved_entity,
+                                        compare_referenced_records=merge_references_with_empty_diffs)
 
     # Go through the comparison and try to apply changes to entity_a:
     for key in diff_r2["parents"]:
@@ -445,7 +572,8 @@ def merge_entities(entity_a: Entity,
             for attribute in ("datatype", "unit", "value"):
                 if (attribute in diff_r2["properties"][key] and
                         diff_r2["properties"][key][attribute] is not None):
-                    if (diff_r1["properties"][key][attribute] is None):
+                    if (attribute not in diff_r1["properties"][key] or
+                            diff_r1["properties"][key][attribute] is None):
                         setattr(entity_a.get_property(key), attribute,
                                 diff_r2["properties"][key][attribute])
                     elif force:
@@ -512,43 +640,103 @@ def merge_entities(entity_a: Entity,
     return entity_a
 
 
-def describe_diff(olddiff, newdiff, name=None, as_update=True):
+def describe_diff(entity0_diff: dict[str, Any], entity1_diff: dict[str, Any],
+                  name: Optional[str] = None,
+                  as_update: Optional[bool] = None,
+                  label_e0: str = "first version",
+                  label_e1: str = "second version",
+                  olddiff: Any = None,
+                  newdiff: Any = None,
+                  ) -> str:
+    """
+    Generate a textual description of the differences between two entities.
+    These can be generated using :func:`compare_entities` and used within this function like this:
+
+    `describe_diff(*compare_entities(...))`
+
+    Arguments:
+    ----------
+
+    entity0_diff: dict[str, Any]
+      First element of the tuple output of :func:`compare_entities`.
+      This is referred to as the "first" version.
+
+    entity1_diff: dict[str, Any]
+      Second element of the tuple output of :func:`compare_entities`.
+      This is referred to as the "second" version.
+
+
+    name: Optional[str]
+      Default None. Name of the entity that will be shown in the output text.
+
+    as_update: Optional[bool]
+      Default None. Not used anymore.
+
+    label_e0: str
+      Can be used to set a custom label for the diff that is associated with the first entity.
+
+    label_e1: str
+      Can be used to set a custom label for the diff that is associated with the second entity.
+
+    olddiff: Any
+      Deprecated. Replaced by entity0_diff.
+
+    newdiff: Any
+      Deprecated. Replaced by entity1_diff.
+
+    Returns:
+    --------
+    A text description of the differences.
+
+    """
     description = ""
 
-    for attr in list(set(list(olddiff.keys()) + list(newdiff.keys()))):
+    if as_update:
+        warnings.warn("'as_update' is deprecated. Do not use it.", DeprecationWarning)
+    if olddiff:
+        warnings.warn("'olddiff' is deprecated. Use 'entity0_diff' instead.", DeprecationWarning)
+        entity0_diff = olddiff
+    if newdiff:
+        warnings.warn("'newdiff' is deprecated. Use 'entity1_diff' instead.", DeprecationWarning)
+        entity1_diff = newdiff
+
+    for attr in list(set(list(entity0_diff.keys()) + list(entity1_diff.keys()))):
         if attr == "parents" or attr == "properties":
             continue
         description += "{} differs:\n".format(attr)
-        description += "old version: {}\n".format(
-            olddiff[attr] if attr in olddiff else "not set")
-        description += "new version: {}\n\n".format(
-            newdiff[attr] if attr in newdiff else "not set")
+        description += label_e0 + ": {}\n".format(
+            entity0_diff[attr] if attr in entity0_diff else "not set")
+        description += label_e1 + ": {}\n\n".format(
+            entity1_diff[attr] if attr in entity1_diff else "not set")
 
-    if len(olddiff["parents"]) > 0:
-        description += ("Parents that are only in the old version:\n"
-                        + ", ".join(olddiff["parents"]) + "\n")
+    if len(entity0_diff["parents"]) > 0:
+        description += ("Parents that are only in the " + label_e0 + ":\n"
+                        + ", ".join(entity0_diff["parents"]) + "\n")
 
-    if len(newdiff["parents"]) > 0:
-        description += ("Parents that are only in the new version:\n"
-                        + ", ".join(olddiff["parents"]) + "\n")
+    if len(entity1_diff["parents"]) > 0:
+        description += ("Parents that are only in the " + label_e1 + ":\n"
+                        + ", ".join(entity0_diff["parents"]) + "\n")
 
-    for prop in list(set(list(olddiff["properties"].keys())
-                         + list(newdiff["properties"].keys()))):
+    for prop in list(set(list(entity0_diff["properties"].keys())
+                         + list(entity1_diff["properties"].keys()))):
         description += "property {} differs:\n".format(prop)
 
-        if prop not in olddiff["properties"]:
-            description += "it does not exist in the old version: \n"
-        elif prop not in newdiff["properties"]:
-            description += "it does not exist in the new version: \n"
+        if prop not in entity0_diff["properties"]:
+            description += "it does not exist in the " + label_e0 + ":\n"
+        elif prop not in entity1_diff["properties"]:
+            description += "it does not exist in the " + label_e1 + ":\n"
         else:
-            description += "old version: {}\n".format(
-                olddiff["properties"][prop])
-            description += "new version: {}\n\n".format(
-                newdiff["properties"][prop])
+            description += label_e0 + ": {}\n".format(
+                entity0_diff["properties"][prop])
+            description += label_e1 + ": {}\n\n".format(
+                entity1_diff["properties"][prop])
 
     if description != "":
-        description = ("## Difference between the old and the new "
-                       "version of {}\n\n".format(name))+description
+        description = ("## Difference between the " +
+                       label_e0 +
+                       " and the " +
+                       label_e1 +
+                       " of {}\n\n".format(name)) + description
 
     return description
 
diff --git a/src/linkahead/cached.py b/src/linkahead/cached.py
index cf1d1d34362335f87c5eca094b5aa9d6b750f68d..11cb959ba10fd507c39eb4d1ddd00bf478859852 100644
--- a/src/linkahead/cached.py
+++ b/src/linkahead/cached.py
@@ -107,7 +107,7 @@ If a query phrase is given, the result must be unique.  If this is not what you
 def cached_query(query_string: str) -> Container:
     """A cached version of :func:`linkahead.execute_query<linkahead.common.models.execute_query>`.
 
-All additional arguments are at their default values.
+    All additional arguments are at their default values.
 
     """
     result = _cached_access(AccessType.QUERY, query_string, unique=False)
@@ -116,7 +116,7 @@ All additional arguments are at their default values.
     return result
 
 
-@ lru_cache(maxsize=DEFAULT_SIZE)
+@lru_cache(maxsize=DEFAULT_SIZE)
 def _cached_access(kind: AccessType, value: Union[str, int], unique: bool = True):
     # This is the function that is actually cached.
     # Due to the arguments, the cache has kind of separate sections for cached_query and
@@ -161,11 +161,12 @@ def cache_clear() -> None:
 def cache_info():
     """Return info about the cache that is used by `cached_query` and `cached_get_entity_by`.
 
-Returns
--------
+    Returns
+    -------
 
-out: named tuple
-  See the standard library :func:`functools.lru_cache` for details."""
+    out: named tuple
+      See the standard library :func:`functools.lru_cache` for details.
+    """
     return _cached_access.cache_info()
 
 
@@ -188,21 +189,21 @@ def cache_fill(items: dict[Union[str, int], Any],
     This allows to fill the cache without actually submitting queries.  Note that this does not
     overwrite existing entries with the same keys.
 
-Parameters
-----------
+    Parameters
+    ----------
 
-items: dict
-  A dictionary with the entries to go into the cache.  The keys must be compatible with the
-  AccessType given in ``kind``
+    items: dict
+      A dictionary with the entries to go into the cache.  The keys must be compatible with the
+      AccessType given in ``kind``
 
-kind: AccessType, optional
-  The AccessType, for example ID, name, path or query.
+    kind: AccessType, optional
+      The AccessType, for example ID, name, path or query.
 
-unique: bool, optional
-  If True, fills the cache for :func:`cached_get_entity_by`, presumably with
-  :class:`linkahead.Entity<linkahead.common.models.Entity>` objects.  If False, the cache should be
-  filled with :class:`linkahead.Container<linkahead.common.models.Container>` objects, for use with
-  :func:`cached_query`.
+    unique: bool, optional
+      If True, fills the cache for :func:`cached_get_entity_by`, presumably with
+      :class:`linkahead.Entity<linkahead.common.models.Entity>` objects.  If False, the cache should be
+      filled with :class:`linkahead.Container<linkahead.common.models.Container>` objects, for use with
+      :func:`cached_query`.
 
     """
 
diff --git a/src/linkahead/common/administration.py b/src/linkahead/common/administration.py
index dee341fa84dd85cbd41a77c0e2d510a96f2c4824..28ef107579fccb689b7337aed65e054cfbf36c05 100644
--- a/src/linkahead/common/administration.py
+++ b/src/linkahead/common/administration.py
@@ -345,20 +345,20 @@ def _get_roles(username, realm=None, **kwargs):
 def _set_permissions(role, permission_rules, **kwargs):
     """Set permissions for a role.
 
-Parameters
-----------
+    Parameters
+    ----------
 
-role : str
-    The role for which the permissions are set.
+    role : str
+        The role for which the permissions are set.
 
-permission_rules : iterable<PermissionRule>
-    An iterable with PermissionRule objects.
+    permission_rules : iterable<PermissionRule>
+        An iterable with PermissionRule objects.
 
-**kwargs :
-    Additional arguments which are passed to the HTTP request.
+    **kwargs :
+        Additional arguments which are passed to the HTTP request.
 
-Returns
--------
+    Returns
+    -------
     None
     """
     xml = etree.Element("PermissionRules")
@@ -393,15 +393,15 @@ def _get_permissions(role, **kwargs):
 class PermissionRule():
     """Permission rules.
 
-Parameters
-----------
-action : str
-    Either "grant" or "deny"
+    Parameters
+    ----------
+    action : str
+        Either "grant" or "deny"
 
-permission : str
-    For example ``RETRIEVE:*``.
+    permission : str
+        For example ``RETRIEVE:*``.
 
-priority : bool, optional
+    priority : bool, optional
     Whether the priority shall be set, defaults is False.
     """
 
diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index a8144286fdacefacadf2b823160e0eb9bfe00c77..1dbeb802311c7afaea2340af15e49537520ef57f 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -37,26 +37,26 @@ from __future__ import annotations  # Can be removed with 3.10.
 
 import re
 import sys
+import warnings
 from builtins import str
 from copy import deepcopy
 from datetime import date, datetime
+from enum import Enum
 from functools import cmp_to_key
 from hashlib import sha512
 from os import listdir
 from os.path import isdir
 from random import randint
 from tempfile import NamedTemporaryFile
-
-from typing import TYPE_CHECKING
-from typing import Any, Final, Literal, Optional, TextIO, Union
+from typing import TYPE_CHECKING, Any, Final, Literal, Optional, TextIO, Union
 
 if TYPE_CHECKING:
-    from .datatype import DATATYPE
-    from tempfile import _TemporaryFileWrapper
     from io import BufferedWriter
     from os import PathLike
-    QueryDict = dict[str, Optional[str]]
+    from tempfile import _TemporaryFileWrapper
 
+    from .datatype import DATATYPE
+    QueryDict = dict[str, Optional[str]]
 
 from warnings import warn
 
@@ -65,36 +65,17 @@ from lxml import etree
 from ..configuration import get_config
 from ..connection.connection import get_connection
 from ..connection.encode import MultipartParam, multipart_encode
-from ..exceptions import (
-    AmbiguousEntityError,
-    AuthorizationError,
-    ConsistencyError,
-    EmptyUniqueQueryError,
-    EntityDoesNotExistError,
-    EntityError,
-    EntityHasNoAclError,
-    EntityHasNoDatatypeError,
-    HTTPURITooLongError,
-    LinkAheadConnectionError,
-    LinkAheadException,
-    MismatchingEntitiesError,
-    PagingConsistencyError,
-    QueryNotUniqueError,
-    TransactionError,
-    UniqueNamesError,
-    UnqualifiedParentsError,
-    UnqualifiedPropertiesError,
-)
-from .datatype import (
-    BOOLEAN,
-    DATETIME,
-    DOUBLE,
-    INTEGER,
-    TEXT,
-    get_list_datatype,
-    is_list_datatype,
-    is_reference,
-)
+from ..exceptions import (AmbiguousEntityError, AuthorizationError,
+                          ConsistencyError, EmptyUniqueQueryError,
+                          EntityDoesNotExistError, EntityError,
+                          EntityHasNoAclError, EntityHasNoDatatypeError,
+                          HTTPURITooLongError, LinkAheadConnectionError,
+                          LinkAheadException, MismatchingEntitiesError,
+                          PagingConsistencyError, QueryNotUniqueError,
+                          TransactionError, UniqueNamesError,
+                          UnqualifiedParentsError, UnqualifiedPropertiesError)
+from .datatype import (BOOLEAN, DATETIME, DOUBLE, INTEGER, TEXT,
+                       get_list_datatype, is_list_datatype, is_reference)
 from .state import State
 from .timezone import TimeZone
 from .utils import uuid, xml2str
@@ -114,8 +95,8 @@ if TYPE_CHECKING:
     IMPORTANCE = Literal["OBLIGATORY", "RECOMMENDED", "SUGGESTED", "FIX", "NONE"]
     ROLE = Literal["Entity", "Record", "RecordType", "Property", "File"]
 
-SPECIAL_ATTRIBUTES = ["name", "role", "datatype", "description",
-                      "id", "path", "checksum", "size", "value"]
+SPECIAL_ATTRIBUTES = ["name", "role", "datatype", "description", "file",
+                      "id", "path", "checksum", "size", "value", "unit"]
 
 
 class Entity:
@@ -138,10 +119,10 @@ class Entity:
         description: Optional[str] = None,  # @ReservedAssignment
         datatype: Optional[DATATYPE] = None,
         value=None,
-        **kwargs,
+        role=None,
     ):
 
-        self.__role: Optional[ROLE] = kwargs["role"] if "role" in kwargs else None
+        self.__role: Optional[ROLE] = role
         self._checksum: Optional[str] = None
         self._size = None
         self._upload = None
@@ -156,8 +137,8 @@ class Entity:
         self.datatype: Optional[DATATYPE] = datatype
         self.value = value
         self.messages = Messages()
-        self.properties = _Properties()
-        self.parents = _ParentList()
+        self.properties = PropertyList()
+        self.parents = ParentList()
         self.path: Optional[str] = None
         self.file: Optional[File] = None
         self.unit: Optional[str] = None
@@ -873,29 +854,29 @@ class Entity:
         check. Note that, if checked, name or ID should not be None,
         lest the check fail.
 
-Parameters
-----------
+        Parameters
+        ----------
 
-parent: Entity
-  Check for this parent.
+        parent: Entity
+          Check for this parent.
 
-recursive: bool, optional
-  Whether to check recursively.
+        recursive: bool, optional
+          Whether to check recursively.
 
-check_name: bool, optional
-  Whether to use the name for ancestry check.
+        check_name: bool, optional
+          Whether to use the name for ancestry check.
 
-check_id: bool, optional
-  Whether to use the ID for ancestry check.
+        check_id: bool, optional
+          Whether to use the ID for ancestry check.
 
-retrieve: bool, optional
-  If False, do not retrieve parents from the server.
+        retrieve: bool, optional
+          If False, do not retrieve parents from the server.
 
-Returns
--------
-out: bool
-  True if ``parent`` is a true parent, False otherwise.
-"""
+        Returns
+        -------
+        out: bool
+          True if ``parent`` is a true parent, False otherwise.
+        """
 
         if recursive:
             parents = self.get_parents_recursively(retrieve=retrieve)
@@ -922,7 +903,7 @@ out: bool
     def get_parents(self):
         """Get all parents of this entity.
 
-        @return: _ParentList(list)
+        @return: ParentList(list)
         """
 
         return self.parents
@@ -930,17 +911,17 @@ out: bool
     def get_parents_recursively(self, retrieve: bool = True) -> list[Entity]:
         """Get all ancestors of this entity.
 
-Parameters
-----------
+        Parameters
+        ----------
 
-retrieve: bool, optional
-  If False, do not retrieve parents from the server.
+        retrieve: bool, optional
+          If False, do not retrieve parents from the server.
 
-Returns
--------
-out: list[Entity]
-  The parents of this Entity
-"""
+        Returns
+        -------
+        out: list[Entity]
+          The parents of this Entity
+        """
 
         all_parents: list[Entity] = []
         self._get_parent_recursively(all_parents, retrieve=retrieve)
@@ -1022,7 +1003,7 @@ out: list[Entity]
     def get_properties(self):
         """Get all properties of this entity.
 
-        @return: _Properties(list)
+        @return: PropertyList(list)
         """
 
         return self.properties
@@ -1598,15 +1579,15 @@ out: list[Entity]
                unique=True, flags=None, sync=True):
         """Update this entity.
 
-There are two possible work-flows to perform this update:
-First:
-    1) retrieve an entity
-    2) do changes
-    3) call update method
+        There are two possible work-flows to perform this update:
+        First:
+            1) retrieve an entity
+            2) do changes
+            3) call update method
 
-Second:
-    1) construct entity with id
-    2) call update method.
+        Second:
+            1) construct entity with id
+            2) call update method.
 
         For slight changes the second one it is more comfortable. Furthermore, it is possible to
         stay off-line until calling the update method. The name, description, unit, datatype, path,
@@ -2422,11 +2403,14 @@ class File(Record):
             value=value, unit=unit, importance=importance, inheritance=inheritance)
 
 
-class _Properties(list):
-    """FIXME: Add docstring."""
+class PropertyList(list):
+    """A list class for Property objects
+
+    This class provides addional functionality like get/set_importance or get_by_name.
+    """
 
     def __init__(self):
-        list.__init__(self)
+        super().__init__()
         self._importance: dict[Entity, IMPORTANCE] = dict()
         self._inheritance: dict[Entity, INHERITANCE] = dict()
         self._element_by_name: dict[str, Entity] = dict()
@@ -2519,6 +2503,40 @@ class _Properties(list):
 
         return xml2str(xml)
 
+    def filter(self, prop: Optional[Property] = None,
+               pid: Union[None, str, int] = None,
+               name: Optional[str] = None,
+               conjunction: bool = False) -> list:
+        """
+        Return all Properties from the given PropertyList that match the
+        selection criteria.
+
+        Please refer to the documentation of _filter_entity_list for a detailed
+        description of behaviour.
+
+        Params
+        ------
+        listobject        : Iterable(Property)
+                            List to be filtered
+        prop              : Property
+                            Property to match name and ID with. Cannot be set
+                            simultaneously with ID or name.
+        pid               : str, int
+                            Property ID to match
+        name              : str
+                            Property name to match
+        conjunction       : bool, defaults to False
+                            Set to return only entities that match both id and name
+                            if both are given.
+
+        Returns
+        -------
+        matches          : list
+                           List containing all matching Properties
+        """
+        return _filter_entity_list(self, pid=pid, name=name, entity=prop,
+                                   conjunction=conjunction)
+
     def _get_entity_by_cuid(self, cuid: str):
         '''
         Get the first entity which has the given cuid.
@@ -2576,9 +2594,7 @@ class _Properties(list):
         raise KeyError(str(prop) + " not found.")
 
 
-class _ParentList(list):
-    # TODO unclear why this class is private. Isn't it use full for users?
-
+class ParentList(list):
     def _get_entity_by_cuid(self, cuid):
         '''
         Get the first entity which has the given cuid.
@@ -2593,8 +2609,8 @@ class _ParentList(list):
                     return e
         raise KeyError("No entity with that cuid in this container.")
 
-    def __init__(self):
-        list.__init__(self)
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
         self._element_by_name = dict()
         self._element_by_id = dict()
 
@@ -2607,15 +2623,9 @@ class _ParentList(list):
         if isinstance(parent, list):
             for p in parent:
                 self.append(p)
-
             return
 
         if isinstance(parent, Entity):
-            if parent.id:
-                self._element_by_id[str(parent.id)] = parent
-
-            if parent.name:
-                self._element_by_name[parent.name] = parent
             list.append(self, parent)
         else:
             raise TypeError("Argument was not an Entity")
@@ -2657,7 +2667,55 @@ class _ParentList(list):
 
         return xml2str(xml)
 
+    def filter(self, parent: Optional[Parent] = None,
+               pid: Union[None, str, int] = None,
+               name: Optional[str] = None,
+               conjunction: bool = False) -> list:
+        """
+        Return all Parents from the given ParentList that match the selection
+        criteria.
+
+        Please refer to the documentation of _filter_entity_list for a detailed
+        description of behaviour.
+
+        Params
+        ------
+        listobject        : Iterable(Parent)
+                            List to be filtered
+        parent            : Parent
+                            Parent to match name and ID with. Cannot be set
+        pid               : str, int
+                            Parent ID to match
+        name              : str
+                            Parent name to match
+                            simultaneously with ID or name.
+        conjunction       : bool, defaults to False
+                            Set to return only entities that match both id and name
+                            if both are given.
+
+        Returns
+        -------
+        matches          : list
+                           List containing all matching Parents
+        """
+        return _filter_entity_list(self, pid=pid, name=name, entity=parent,
+                                   conjunction=conjunction)
+
     def remove(self, parent: Union[Entity, int, str]):
+        """
+        Remove first occurrence of parent.
+
+        Parameters
+        ----------
+        parent: Union[Entity, int, str], the parent to be removed identified via ID or name. If a
+        Parent object is provided the ID and then the name is used to identify the parent to be
+        removed.
+
+        Returns
+        -------
+        None
+        """
+
         if isinstance(parent, Entity):
             if parent in self:
                 list.remove(self, parent)
@@ -2675,11 +2733,11 @@ class _ParentList(list):
                     # by name
 
                     for e in self:
-                        if e.name is not None and e.name == parent.name:
+                        if e.name is not None and e.name.lower() == parent.name.lower():
                             list.remove(self, e)
 
                             return
-        elif hasattr(parent, "encode"):
+        elif isinstance(parent, str):
             # by name
 
             for e in self:
@@ -2698,6 +2756,19 @@ class _ParentList(list):
         raise KeyError(str(parent) + " not found.")
 
 
+class _Properties(PropertyList):
+    def __init__(self, *args, **kwargs):
+        warnings.warn(DeprecationWarning("This class is deprecated. Please use PropertyList."))
+        super().__init__(*args, **kwargs)
+
+
+class _ParentList(ParentList):
+    def __init__(self, *args, **kwargs):
+        warnings.warn(DeprecationWarning("This class is deprecated. Please use ParentList "
+                                         "(without underscore)."))
+        super().__init__(*args, **kwargs)
+
+
 class Messages(list):
     """This specialization of list stores error, warning, info, and other
     messages. The mentioned three messages types play a special role.
@@ -5392,3 +5463,91 @@ def delete(ids: Union[list[int], range], raise_exception_on_error: bool = True):
         c.append(Entity(id=ids))
 
     return c.delete(raise_exception_on_error=raise_exception_on_error)
+
+
+def _filter_entity_list(listobject: list[Entity],
+                        entity: Optional[Entity] = None,
+                        pid: Union[None, str, int] = None,
+                        name: Optional[str] = None,
+                        conjunction: bool = False) -> list:
+    """
+    Returns a subset of entities from the list based on whether their id and
+    name matches the selection criterion.
+
+    If both pid and name are given, entities from the list are first matched
+    based on id. If they do not have an id, they are matched based on name.
+    If only one parameter is given, only this parameter is considered.
+
+    If an Entity is given, neither name nor ID may be set. In this case, pid
+    and name are determined by the attributes of given entity.
+
+    This results in the following selection criteria:
+    If an entity in the list
+    - has both name and id, it is returned if the id matches the given not-None
+      value for pid. If no pid was given, it is returned if the name matches.
+    - has an id, but no name, it will be returned only if it matches the given
+      not-None value
+    - has no id, but a name, it will be returned if the name matches the given
+      not-None value
+    - has neither id nor name, it will never be returned
+
+    As IDs can be strings, integer IDs are cast to string for the comparison.
+
+    Params
+    ------
+    listobject        : Iterable(Entity)
+                        List to be filtered
+    entity            : Entity
+                        Entity to match name and ID for. Cannot be set
+                        simultaneously with ID or name.
+    pid               : str, int
+                        Entity ID to match
+    name              : str
+                        Entity name to match
+    conjunction       : bool, defaults to False
+                        Set to true to return only entities that match both id
+                        and name if both are given.
+
+    Returns
+    -------
+    matches          : list
+                       A List containing all matching Entities
+    """
+    # Check correct input params and setup
+    if entity is not None:
+        if pid is not None or name is not None:
+            raise ValueError("If an entity is given, pid and name must not be set.")
+        pid = entity.id
+        name = entity.name
+    if pid is None and name is None:
+        if entity is None:
+            raise ValueError("One of entity, pid or name must be set.")
+        else:
+            raise ValueError("A given entity must have at least one of name and id.")
+    if pid is None or name is None:
+        conjunction = False
+
+    # Iterate through list and match based on given criteria
+    matches = []
+    for candidate in listobject:
+        name_match, pid_match = False, False
+
+        # Check whether name/pid match
+        # Comparison is only possible if both are not None
+        pid_none = pid is None or candidate.id is None
+        # Cast to string in case one is f.e. "12" and the other is 12
+        if not pid_none and str(candidate.id) == str(pid):
+            pid_match = True
+        name_none = name is None or candidate.name is None
+        if not name_none and str(candidate.name).lower() == str(name).lower():
+            name_match = True
+
+        # If the criteria are satisfied, append the match.
+        if pid_match and name_match:
+            matches.append(candidate)
+        elif not conjunction:
+            if pid_match:
+                matches.append(candidate)
+            if pid_none and name_match:
+                matches.append(candidate)
+    return matches
diff --git a/src/linkahead/common/versioning.py b/src/linkahead/common/versioning.py
index 2e292e6bb031725fbd6da618c4b888c05072c46b..11cf5f6904b02954eb0b2bddc16478590df167e7 100644
--- a/src/linkahead/common/versioning.py
+++ b/src/linkahead/common/versioning.py
@@ -105,7 +105,7 @@ class Version():
                  is_head: Union[bool, str, None] = False,
                  is_complete_history: Union[bool, str, None] = False):
         """Typically the `predecessors` or `successors` should not "link back" to an existing Version
-object."""
+        object."""
         self.id = id
         self.date = date
         self.username = username
diff --git a/src/linkahead/utils/create_revision.py b/src/linkahead/utils/create_revision.py
index 5f6ecc8148859d0ee0908412ff80d20d465cdb25..cde4bae5b0d919977d220b2c35896dcb20e933e7 100644
--- a/src/linkahead/utils/create_revision.py
+++ b/src/linkahead/utils/create_revision.py
@@ -34,15 +34,15 @@ def bend_references(from_id, to_id, except_for=None):
     and those references are changed to point to to_id.
     entities having an id listed in except_for are excluded.
 
-Parameters
-----------
+    Parameters
+    ----------
 
-from_id : int
-  the old object to which references where pointing
-to_id : int
-  the new object to which references will be pointing
-except_for : list of int
-  entities with id of this list will not be changed
+    from_id : int
+      the old object to which references where pointing
+    to_id : int
+      the new object to which references will be pointing
+    except_for : list of int
+      entities with id of this list will not be changed
     """
     if except_for is None:
         except_for = [to_id]
@@ -73,16 +73,16 @@ def create_revision(old_id, prop, value):
     This function changes the record with id old_id. The value of the
     propertye prop is changed to value.
 
-Parameters
-----------
+    Parameters
+    ----------
 
-old_id : int
-    id of the record to be changed
-prop : string
-    name of the property to be changed
-value : type of corresponding property
-    the new value of the corresponding property
-"""
+    old_id : int
+        id of the record to be changed
+    prop : string
+        name of the property to be changed
+    value : type of corresponding property
+        the new value of the corresponding property
+    """
     record = db.execute_query("FIND {}".format(old_id))[0]
     new_rec = record.copy()
     new_rec.get_property(prop).value = value
diff --git a/src/linkahead/utils/get_entity.py b/src/linkahead/utils/get_entity.py
index 0ffd89e4dc7f214bbc72d4508f6ca4481dad7d9c..dd91cdc27b3f6adb52ddef36a59d1a0965fb662e 100644
--- a/src/linkahead/utils/get_entity.py
+++ b/src/linkahead/utils/get_entity.py
@@ -30,13 +30,13 @@ from .escape import escape_squoted_text
 def get_entity_by_name(name: str, role: Optional[str] = None) -> Entity:
     """Return the result of a unique query that uses the name to find the correct entity.
 
-Submits the query "FIND {role} WITH name='{name}'".
+    Submits the query "FIND {role} WITH name='{name}'".
 
-Parameters
-----------
+    Parameters
+    ----------
 
-role: str, optional
-  The role for the query, defaults to ``ENTITY``.
+    role: str, optional
+      The role for the query, defaults to ``ENTITY``.
     """
     name = escape_squoted_text(name)
     if role is None:
@@ -48,13 +48,13 @@ role: str, optional
 def get_entity_by_id(eid: Union[str, int], role: Optional[str] = None) -> Entity:
     """Return the result of a unique query that uses the id to find the correct entity.
 
-Submits the query "FIND {role} WITH id='{eid}'".
+    Submits the query "FIND {role} WITH id='{eid}'".
 
-Parameters
-----------
+    Parameters
+    ----------
 
-role: str, optional
-  The role for the query, defaults to ``ENTITY``.
+    role: str, optional
+      The role for the query, defaults to ``ENTITY``.
     """
     if role is None:
         role = "ENTITY"
@@ -65,13 +65,13 @@ role: str, optional
 def get_entity_by_path(path: str) -> Entity:
     """Return the result of a unique query that uses the path to find the correct file.
 
-Submits the query "FIND {role} WHICH IS STORED AT '{path}'".
+    Submits the query "FIND {role} WHICH IS STORED AT '{path}'".
 
-Parameters
-----------
+    Parameters
+    ----------
 
-role: str, optional
-  The role for the query, defaults to ``ENTITY``.
+    role: str, optional
+      The role for the query, defaults to ``ENTITY``.
     """
     # type hint can be ignored, it's a unique query
     return execute_query(f"FIND FILE WHICH IS STORED AT '{path}'", unique=True)  # type: ignore
diff --git a/src/linkahead/utils/linkahead_admin.py b/src/linkahead/utils/linkahead_admin.py
index f7e3b8b63f18e37e6210f2aa03f34ce5b0f688d4..ca5f3c01e0bbe95fe712761ec7f443ec88d406fd 100755
--- a/src/linkahead/utils/linkahead_admin.py
+++ b/src/linkahead/utils/linkahead_admin.py
@@ -33,7 +33,7 @@ from argparse import ArgumentParser, RawDescriptionHelpFormatter
 
 import linkahead as db
 from linkahead import administration as admin
-from linkahead.exceptions import HTTPClientError
+from linkahead.exceptions import HTTPClientError, HTTPResourceNotFoundError, HTTPForbiddenError
 
 __all__ = []
 __version__ = 0.3
@@ -42,19 +42,42 @@ __updated__ = '2018-12-11'
 
 
 def do_update_role(args):
-    admin._update_role(name=args.role_name, description=args.role_description)
+    """
+    Update the description of a role.
+
+    Allowed keyword arguments:
+    role_name: Name of the role to update
+    role_description: New description of the role
+    """
+    try:
+        admin._update_role(name=args.role_name, description=args.role_description)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot update role '{args.role_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_create_role(args):
-    admin._insert_role(name=args.role_name, description=args.role_description)
+    try:
+        admin._insert_role(name=args.role_name, description=args.role_description)
+    except (HTTPClientError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot create role '{args.role_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_retrieve_role(args):
-    print(admin._retrieve_role(name=args.role_name))
+    try:
+        print(admin._retrieve_role(name=args.role_name))
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot retrieve role '{args.role_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_delete_role(args):
-    admin._delete_role(name=args.role_name)
+    try:
+        admin._delete_role(name=args.role_name)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot delete role '{args.role_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_retrieve(args):
@@ -123,25 +146,27 @@ def do_create_user(args):
     try:
         admin._insert_user(name=args.user_name,
                            email=args.user_email, password=password)
-
         if args.activate_user:
             do_activate_user(args)
-    except HTTPClientError as e:
-        print(e.msg)
+    except (HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot create user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_activate_user(args):
     try:
         admin._update_user(name=args.user_name, status="ACTIVE")
-    except HTTPClientError as e:
-        print(e.msg)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot activate user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_deactivate_user(args):
     try:
         admin._update_user(name=args.user_name, status="INACTIVE")
-    except HTTPClientError as e:
-        print(e.msg)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot deactivate user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_set_user_password(args):
@@ -150,58 +175,110 @@ def do_set_user_password(args):
     else:
         password = args.user_password
     try:
-        admin._update_user(name=args.user_name, password=password)
-    except HTTPClientError as e:
-        print(e.msg)
+        admin._update_user(name=args.user_name, password=password, realm=args.realm)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot set password for user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_add_user_roles(args):
-    roles = admin._get_roles(username=args.user_name, realm=None)
+    try:
+        roles = admin._get_roles(username=args.user_name, realm=None)
+    except (HTTPForbiddenError, HTTPResourceNotFoundError) as e:
+        print(f"Error: Cannot access roles for user '{args.user_name}', "
+              f"reason: '{e.msg}'")
+        return
 
     for r in args.user_roles:
         roles.add(r)
-    admin._set_roles(username=args.user_name, roles=roles)
+    try:
+        admin._set_roles(username=args.user_name, roles=roles)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot add new roles for user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_remove_user_roles(args):
-    roles = admin._get_roles(username=args.user_name, realm=None)
+    try:
+        roles = admin._get_roles(username=args.user_name, realm=None)
+    except (HTTPForbiddenError, HTTPResourceNotFoundError) as e:
+        print(f"Error: Cannot access roles for user '{args.user_name}', "
+              f"reason: '{e.msg}'")
+        return
 
     for r in args.user_roles:
         if r in roles:
             roles.remove(r)
-    admin._set_roles(username=args.user_name, roles=roles)
+    try:
+        admin._set_roles(username=args.user_name, roles=roles)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot remove roles from user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_set_user_entity(args):
-    admin._update_user(name=args.user_name, entity=args.user_entity)
+    try:
+        admin._update_user(name=args.user_name, entity=args.user_entity)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot set entity for user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_reset_user_entity(args):
-    admin._update_user(name=args.user_name, entity="")
+    try:
+        admin._update_user(name=args.user_name, entity="")
+    except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot remove entity for user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_set_user_email(args):
-    admin._update_user(name=args.user_name, email=args.user_email)
+    try:
+        admin._update_user(name=args.user_name, email=args.user_email)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e:
+        print(f"Error: Cannot set email for user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_retrieve_user(args):
-    print(admin._retrieve_user(name=args.user_name))
+    try:
+        print(admin._retrieve_user(name=args.user_name))
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot retrieve user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_delete_user(args):
-    admin._delete_user(name=args.user_name)
+    try:
+        admin._delete_user(name=args.user_name)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot delete user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_retrieve_user_roles(args):
-    print(admin._get_roles(username=args.user_name))
+    try:
+        print(admin._get_roles(username=args.user_name))
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot retrieve roles for user '{args.user_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_retrieve_role_permissions(args):
-    print(admin._get_permissions(role=args.role_name))
+    try:
+        print(admin._get_permissions(role=args.role_name))
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot retrieve permissions for role '{args.role_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_grant_role_permissions(args):
-    perms = admin._get_permissions(args.role_name)
+    try:
+        perms = admin._get_permissions(role=args.role_name)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot access permissions for role '{args.role_name}', "
+              f"reason: '{e.msg}'")
+        return
 
     for p in args.role_permissions:
         g = admin.PermissionRule(
@@ -215,11 +292,20 @@ def do_grant_role_permissions(args):
         if d in perms:
             perms.remove(d)
         perms.add(g)
-    admin._set_permissions(role=args.role_name, permission_rules=perms)
+    try:
+        admin._set_permissions(role=args.role_name, permission_rules=perms)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot set permissions for role '{args.role_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_revoke_role_permissions(args):
-    perms = admin._get_permissions(args.role_name)
+    try:
+        perms = admin._get_permissions(role=args.role_name)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot access permissions for role '{args.role_name}', "
+              f"reason: '{e.msg}'")
+        return
 
     for p in args.role_permissions:
         g = admin.PermissionRule(
@@ -232,11 +318,20 @@ def do_revoke_role_permissions(args):
 
         if d in perms:
             perms.remove(d)
-    admin._set_permissions(role=args.role_name, permission_rules=perms)
+    try:
+        admin._set_permissions(role=args.role_name, permission_rules=perms)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot revoke permissions for role '{args.role_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_deny_role_permissions(args):
-    perms = admin._get_permissions(args.role_name)
+    try:
+        perms = admin._get_permissions(role=args.role_name)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot access permissions for role '{args.role_name}', "
+              f"reason: '{e.msg}'")
+        return
 
     for p in args.role_permissions:
         g = admin.PermissionRule(
@@ -250,7 +345,11 @@ def do_deny_role_permissions(args):
         if d in perms:
             perms.remove(d)
         perms.add(d)
-    admin._set_permissions(role=args.role_name, permission_rules=perms)
+    try:
+        admin._set_permissions(role=args.role_name, permission_rules=perms)
+    except (HTTPResourceNotFoundError, HTTPForbiddenError) as e:
+        print(f"Error: Cannot deny permissions for role '{args.role_name}', "
+              f"reason: '{e.msg}'")
 
 
 def do_retrieve_entity_acl(args):
@@ -364,6 +463,12 @@ USAGE
         metavar='USERNAME',
         dest="user_name",
         help="The name of the user who's password is to be set.")
+    subparser.add_argument(
+        metavar='REALM',
+        dest="realm",
+        nargs="?",
+        default=None,
+        help="The realm of the user who's password is to be set.")
     subparser.add_argument(
         metavar='PASSWORD',
         nargs="?",
diff --git a/tox.ini b/tox.ini
index 592c660c5bbbf5805a3ecbb3e60c41f597182a55..c63555c27b73224106109c1f675e9525d9e89b74 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,6 +17,6 @@ max-line-length=100
 [pytest]
 testpaths = unittests
 xfail_strict = True
-addopts = -x -vv --cov=caosdb
+addopts = -x -vv --cov=linkahead
 pythonpath = src
 
diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py
index 4705f19a1bdfbc4358790f787f2dce9ea97fee48..fdd5adda065a563b15008f1b840539c110921b65 100644
--- a/unittests/test_apiutils.py
+++ b/unittests/test_apiutils.py
@@ -1,6 +1,7 @@
 #
 # This file is a part of the LinkAhead Project.
 #
+# Copyright (C) 2024 Alexander Schlemmer <a.schlemmer@indiscale.com>
 # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
 # Copyright (C) 2022 Florian Spreckelsen <f.spreckelsen@indiscale.com>
 # Copyright (C) 2022 Daniel Hornung <d.hornung@indiscale.com>
@@ -25,13 +26,15 @@
 # Test apiutils
 # A. Schlemmer, 02/2018
 
+from io import StringIO
 
 import linkahead as db
 import linkahead.apiutils
 import pytest
 from linkahead.apiutils import (EntityMergeConflictError, apply_to_ids,
                                 compare_entities, create_id_query, empty_diff,
-                                merge_entities, resolve_reference)
+                                merge_entities, resolve_reference,
+                                describe_diff)
 from linkahead.common.models import SPECIAL_ATTRIBUTES
 
 
@@ -96,6 +99,7 @@ def test_resolve_reference():
 
 
 def test_compare_entities():
+    # test compare of parents, properties
     r1 = db.Record()
     r2 = db.Record()
     r1.add_parent("bla")
@@ -111,13 +115,27 @@ def test_compare_entities():
     r2.add_property("tester", )
     r1.add_property("tests_234234", value=45)
     r2.add_property("tests_TT", value=45)
+    r1.add_property("datatype", value=45, datatype=db.INTEGER)
+    r2.add_property("datatype", value=45)
+    r1.add_property("entity_id", value=2)
+    r2.add_property("entity_id", value=24)
+    r1.add_property("entity_mix_e", value=2)
+    r2.add_property("entity_mix_e", value=db.Entity(id=2))
+    r1.add_property("entity_mix_d", value=22)
+    r2.add_property("entity_mix_d", value=db.Entity(id=2))
+    r1.add_property("entity_mix_w", value=22)
+    r2.add_property("entity_mix_w", value=db.Entity())
+    r1.add_property("entity_Ent_e", value=db.Entity(id=2))
+    r2.add_property("entity_Ent_e", value=db.Entity(id=2))
+    r1.add_property("entity_Ent_d", value=db.Entity(id=2))
+    r2.add_property("entity_Ent_d", value=db.Entity(id=22))
 
     diff_r1, diff_r2 = compare_entities(r1, r2)
 
     assert len(diff_r1["parents"]) == 1
     assert len(diff_r2["parents"]) == 0
-    assert len(diff_r1["properties"]) == 4
-    assert len(diff_r2["properties"]) == 4
+    assert len(diff_r1["properties"]) == 11
+    assert len(diff_r2["properties"]) == 11
 
     assert "test" not in diff_r1["properties"]
     assert "test" not in diff_r2["properties"]
@@ -134,13 +152,89 @@ def test_compare_entities():
     assert "tests_234234" in diff_r1["properties"]
     assert "tests_TT" in diff_r2["properties"]
 
+    assert "datatype" in diff_r1["properties"]
+    assert "datatype" in diff_r1["properties"]["datatype"]
+    assert "datatype" in diff_r2["properties"]
+    assert "datatype" in diff_r2["properties"]["datatype"]
+
+    assert "entity_id" in diff_r1["properties"]
+    assert "entity_id" in diff_r2["properties"]
+
+    assert "entity_mix_e" in diff_r1["properties"]
+    assert "entity_mix_e" in diff_r2["properties"]
+    assert "entity_Ent_e" in diff_r1["properties"]
+    assert "entity_Ent_e" in diff_r2["properties"]
+
+    assert "entity_mix_d" in diff_r1["properties"]
+    assert "entity_mix_d" in diff_r2["properties"]
+    assert "entity_mix_w" in diff_r1["properties"]
+    assert "entity_mix_w" in diff_r2["properties"]
+    assert "entity_Ent_d" in diff_r1["properties"]
+    assert "entity_Ent_d" in diff_r2["properties"]
+
+    diff_r1, diff_r2 = compare_entities(r1, r2, compare_referenced_records=True)
+
+    assert len(diff_r1["parents"]) == 1
+    assert len(diff_r2["parents"]) == 0
+    assert len(diff_r1["properties"]) == 10
+    assert len(diff_r2["properties"]) == 10
+
+    assert "entity_id" in diff_r1["properties"]
+    assert "entity_id" in diff_r2["properties"]
+
+    assert "entity_mix_e" in diff_r1["properties"]
+    assert "entity_mix_e" in diff_r2["properties"]
+    assert "entity_mix_w" in diff_r1["properties"]
+    assert "entity_mix_w" in diff_r2["properties"]
+    assert "entity_Ent_e" not in diff_r1["properties"]
+    assert "entity_Ent_e" not in diff_r2["properties"]
+
+    assert "entity_mix_d" in diff_r1["properties"]
+    assert "entity_mix_d" in diff_r2["properties"]
+    assert "entity_Ent_d" in diff_r1["properties"]
+    assert "entity_Ent_d" in diff_r2["properties"]
+
+    diff_r1, diff_r2 = compare_entities(r1, r2,
+                                        entity_name_id_equivalency=True,
+                                        compare_referenced_records=True)
+
+    assert len(diff_r1["parents"]) == 1
+    assert len(diff_r2["parents"]) == 0
+    assert len(diff_r1["properties"]) == 9
+    assert len(diff_r2["properties"]) == 9
+
+    assert "entity_id" in diff_r1["properties"]
+    assert "entity_id" in diff_r2["properties"]
+
+    assert "entity_mix_e" not in diff_r1["properties"]
+    assert "entity_mix_e" not in diff_r2["properties"]
+    assert "entity_mix_w" in diff_r1["properties"]
+    assert "entity_mix_w" in diff_r2["properties"]
+    assert "entity_Ent_e" not in diff_r1["properties"]
+    assert "entity_Ent_e" not in diff_r2["properties"]
+
+    assert "entity_mix_d" in diff_r1["properties"]
+    assert "entity_mix_d" in diff_r2["properties"]
+    assert "entity_Ent_d" in diff_r1["properties"]
+    assert "entity_Ent_d" in diff_r2["properties"]
+
+    r1 = db.Record()
+    r2 = db.Record()
+    r1.add_property(id=20, name="entity_mix_d", value=2, datatype=db.LIST("B"))
+    r2.add_property("entity_mix_d", value=db.Entity())
+
+    diff_r1, diff_r2 = compare_entities(r1, r2, compare_referenced_records=True)
+
+    assert len(diff_r1["properties"]) == 1
+    assert len(diff_r2["properties"]) == 1
+
+    assert "entity_mix_d" in diff_r1["properties"]
+    assert "entity_mix_d" in diff_r2["properties"]
+
 
 def test_compare_entities_units():
     r1 = db.Record()
     r2 = db.Record()
-    r1.add_parent("bla")
-    r2.add_parent("bla")
-    r1.add_parent("lopp")
     r1.add_property("test", value=2, unit="cm")
     r2.add_property("test", value=2, unit="m")
     r1.add_property("tests", value=3, unit="cm")
@@ -152,8 +246,6 @@ def test_compare_entities_units():
 
     diff_r1, diff_r2 = compare_entities(r1, r2)
 
-    assert len(diff_r1["parents"]) == 1
-    assert len(diff_r2["parents"]) == 0
     assert len(diff_r1["properties"]) == 4
     assert len(diff_r2["properties"]) == 4
 
@@ -170,14 +262,229 @@ def test_compare_entities_units():
     assert diff_r2["properties"]["test"]["unit"] == "m"
 
 
+def test_compare_entities_battery():
+    par1, par3 = db.Record(name=""), db.RecordType(name="")
+    r1, r2, r3 = db.Record(), db.Record(), db.Record()
+    prop2 = db.Property(name="Property 2")
+    prop3 = db.Property(name="")
+
+    # Basic tests for Properties
+    prop_settings = {"datatype": db.REFERENCE, "description": "desc of prop",
+                     "value": db.Record().add_parent(par3), "unit": '°'}
+    t1 = db.Record().add_parent(db.RecordType(id=1))
+    t2 = db.Record().add_parent(db.RecordType(id=1))
+    # Change datatype
+    t1.add_property(db.Property(name="datatype", **prop_settings))
+    prop_settings["datatype"] = par3
+    t2.add_property(db.Property(name="datatype", **prop_settings))
+    # Change description
+    t1.add_property(db.Property(name="description", **prop_settings))
+    prop_settings["description"] = "diff desc"
+    t2.add_property(db.Property(name="description", **prop_settings))
+    # Change value to copy
+    t1.add_property(db.Property(name="value copy", **prop_settings))
+    prop_settings["value"] = db.Record().add_parent(par3)
+    t2.add_property(db.Property(name="value copy", **prop_settings))
+    # Change value to something different
+    t1.add_property(db.Property(name="value", **prop_settings))
+    prop_settings["value"] = db.Record(name="n").add_parent(par3)
+    t2.add_property(db.Property(name="value", **prop_settings))
+    # Change unit
+    t1.add_property(db.Property(name="unit", **prop_settings))
+    prop_settings["unit"] = db.Property(unit='°')
+    t2.add_property(db.Property(name="unit", **prop_settings))
+    # Change unit again
+    t1.add_property(db.Property(name="unit 2", **prop_settings))
+    prop_settings["unit"] = db.Property()
+    t2.add_property(db.Property(name="unit 2", **prop_settings))
+    # Compare
+    diff_0 = compare_entities(t1, t2)
+    diff_1 = compare_entities(t1, t2, compare_referenced_records=True)
+    # Check correct detection of changes
+    assert diff_0[0]["properties"]["datatype"] == {"datatype": db.REFERENCE}
+    assert diff_0[1]["properties"]["datatype"] == {"datatype": par3}
+    assert diff_0[0]["properties"]["description"] == {"description": "desc of prop"}
+    assert diff_0[1]["properties"]["description"] == {"description": "diff desc"}
+    assert "value" in diff_0[0]["properties"]["value copy"]
+    assert "value" in diff_0[1]["properties"]["value copy"]
+    assert "value" in diff_0[0]["properties"]["value"]
+    assert "value" in diff_0[1]["properties"]["value"]
+    assert "unit" in diff_0[0]["properties"]["unit"]
+    assert "unit" in diff_0[1]["properties"]["unit"]
+    assert "unit" in diff_0[0]["properties"]["unit 2"]
+    assert "unit" in diff_0[1]["properties"]["unit 2"]
+    # Check correct result for compare_referenced_records=True
+    assert "value copy" not in diff_1[0]["properties"]
+    assert "value copy" not in diff_1[1]["properties"]
+    diff_0[0]["properties"].pop("value copy")
+    diff_0[1]["properties"].pop("value copy")
+    assert diff_0 == diff_1
+
+    # Basic tests for Parents
+    t3 = db.Record().add_parent(db.RecordType("A")).add_parent(db.Record("B"))
+    t4 = db.Record().add_parent(db.RecordType("A"))
+    assert compare_entities(t3, t4)[0]['parents'] == ['B']
+    assert len(compare_entities(t3, t4)[1]['parents']) == 0
+    t4.add_parent(db.Record("B"))
+    assert empty_diff(t3, t4)
+    # The two following assertions document current behaviour but do not make a
+    # lot of sense
+    t4.add_parent(db.Record("B"))
+    assert empty_diff(t3, t4)
+    t3.add_parent(db.RecordType("A")).add_parent(db.Record("B"))
+    t4.add_parent(db.RecordType("B")).add_parent(db.Record("A"))
+    assert empty_diff(t3, t4)
+
+    # Basic tests for special attributes
+    prop_settings = {"id": 42, "name": "Property",
+                     "datatype": db.LIST(db.REFERENCE), "value": [db.Record(name="")],
+                     "unit": '€', "description": "desc of prop"}
+    alt_settings = {"id": 64, "name": "Property 2",
+                    "datatype": db.LIST(db.TEXT), "value": [db.RecordType(name="")],
+                    "unit": '€€', "description": " ę Ě ப ཾ ཿ ∛ ∜ ㅿ ㆀ 값 "}
+    t5 = db.Property(**prop_settings)
+    t6 = db.Property(**prop_settings)
+    assert empty_diff(t5, t6)
+    # ID
+    t5.id = alt_settings['id']
+    diff = compare_entities(t5, t6)
+    assert diff[0] == {'properties': {}, 'parents': [], 'id': alt_settings['id']}
+    assert diff[1] == {'properties': {}, 'parents': [], 'id': prop_settings['id']}
+    t6.id = alt_settings['id']
+    assert empty_diff(t5, t6)
+    # Name
+    t5.name = alt_settings['name']
+    diff = compare_entities(t5, t6)
+    assert diff[0] == {'properties': {}, 'parents': [], 'name': alt_settings['name']}
+    assert diff[1] == {'properties': {}, 'parents': [], 'name': prop_settings['name']}
+    t6.name = alt_settings['name']
+    assert empty_diff(t5, t6)
+    # Description
+    t6.description = alt_settings['description']
+    diff = compare_entities(t5, t6)
+    assert diff[0] == {'properties': {}, 'parents': [], 'description': prop_settings['description']}
+    assert diff[1] == {'properties': {}, 'parents': [], 'description': alt_settings['description']}
+    t5.description = alt_settings['description']
+    assert empty_diff(t5, t6)
+    # Unit
+    t5.unit = alt_settings['unit']
+    diff = compare_entities(t5, t6)
+    assert diff[0] == {'properties': {}, 'parents': [], 'unit': alt_settings['unit']}
+    assert diff[1] == {'properties': {}, 'parents': [], 'unit': prop_settings['unit']}
+    t6.unit = alt_settings['unit']
+    assert empty_diff(t5, t6)
+    # Value
+    t6.value = alt_settings['value']
+    diff = compare_entities(t5, t6)
+    assert diff[0] == {'properties': {}, 'parents': [], 'value': prop_settings['value']}
+    assert diff[1] == {'properties': {}, 'parents': [], 'value': alt_settings['value']}
+    t5.value = alt_settings['value']
+    assert empty_diff(t5, t6)
+    # Datatype
+    t6.datatype = alt_settings['datatype']
+    diff = compare_entities(t5, t6)
+    assert diff[0] == {'properties': {}, 'parents': [], 'datatype': prop_settings['datatype']}
+    assert diff[1] == {'properties': {}, 'parents': [], 'datatype': alt_settings['datatype']}
+    t5.datatype = alt_settings['datatype']
+    assert empty_diff(t5, t6)
+    # All at once
+    diff = compare_entities(db.Property(**prop_settings), db.Property(**alt_settings))
+    assert diff[0] == {'properties': {}, 'parents': [], **prop_settings}
+    assert diff[1] == {'properties': {}, 'parents': [], **alt_settings}
+    # Entity Type
+    diff = compare_entities(db.Property(value=db.Property(id=101)),
+                            db.Property(value=db.Record(id=101)))
+    assert "value" in diff[0]
+    assert "value" in diff[1]
+    diff = compare_entities(db.Property(value=db.Record(id=101)),
+                            db.Property(value=db.Record(id=101)))
+    assert "value" in diff[0]
+    assert "value" in diff[1]
+    assert empty_diff(db.Property(value=db.Record(id=101)),
+                      db.Property(value=db.Record(id=101)),
+                      compare_referenced_records=True)
+
+    # Special cases
+    # Files
+    assert not empty_diff(db.File(path='ABC', file=StringIO("ABC")),
+                          db.File(path='ABC', file=StringIO("Other")))
+    # Importance
+    assert empty_diff(db.Property().add_property(prop2),
+                      db.Property().add_property(prop2))
+    assert not empty_diff(db.Property().add_property(prop2, importance=db.SUGGESTED),
+                          db.Property().add_property(prop2, importance=db.OBLIGATORY))
+    # Mixed Lists
+    assert empty_diff(db.Property(value=[1, 2, 'a', r1]),
+                      db.Property(value=[1, 2, 'a', r1]))
+    # entity_name_id_equivalency
+    assert not empty_diff(db.Property(value=[1, db.Record(id=2), 3, db.Record(id=4)]),
+                          db.Property(value=[db.Record(id=1), 2, db.Record(id=3), 4]))
+    assert empty_diff(db.Property(value=[1, db.Record(id=2), 3, db.Record(id=4)]),
+                      db.Property(value=[db.Record(id=1), 2, db.Record(id=3), 4]),
+                      entity_name_id_equivalency=True)
+    assert empty_diff(db.Property(value=1), db.Property(value=db.Record(id=1)),
+                      entity_name_id_equivalency=True)
+    # entity_name_id_equivalency
+    prop4 = db.Property(**prop_settings).add_parent(par1).add_property(prop2)
+    prop4_c = db.Property(**prop_settings).add_parent(par1).add_property(prop2)
+    prop4.value = db.Record(id=12)
+    prop4_c.value = '12'
+    prop4.add_property(db.Property(name="diff", datatype=db.LIST(db.REFERENCE),
+                                   value=[12, db.Record(id=13), par1, "abc%"]))
+    prop4_c.add_property(db.Property(name="diff", datatype=db.LIST(db.REFERENCE),
+                                     value=[db.Record(id=12), "13", par1, "abc%"]))
+    assert not empty_diff(prop4, prop4_c, entity_name_id_equivalency=False)
+    assert empty_diff(prop4, prop4_c, entity_name_id_equivalency=True)
+    # Order invariance
+    t7 = db.Property(**prop_settings).add_parent(par1).add_property(prop2)
+    t8 = db.Property(**alt_settings).add_parent(par3).add_property(prop3)
+    diffs_0 = compare_entities(t7, t8), compare_entities(t7, t8, True)
+    diffs_1 = compare_entities(t8, t7)[::-1], compare_entities(t8, t7, True)[::-1]
+    assert diffs_0 == diffs_1
+    prop_settings = {"datatype": db.REFERENCE, "description": "desc of prop",
+                     "value": db.Record().add_parent(par3), "unit": '°'}
+    t1.add_property(db.Property(name="description", **prop_settings))
+    t2.add_property(db.Property(name="description", **prop_settings))
+    # Order invariance for multi-property - either both fail or same result
+    try:
+        diffs_0 = compare_entities(t1, t2), compare_entities(t1, t2, True)
+    except Exception as e:
+        diffs_0 = type(e)
+    try:
+        diffs_1 = compare_entities(t2, t1)[::-1], compare_entities(t2, t1, True)[::-1]
+    except Exception as e:
+        diffs_1 = type(e)
+    assert diffs_0 == diffs_1
+    # Property types
+    t09, t10 = db.RecordType(), db.RecordType()
+    for t, ex in [(db.INTEGER, [-12, 0]), (db.DATETIME, ["2030-01-01", "1012-02-29"]),
+                  (db.DOUBLE, [13.23, 7.1]), (db.BOOLEAN, [True, False])]:
+        t09.add_property(db.Property(name=f"{t}:{ex[0]}", datatype=t, value=ex[0]))
+        t10.add_property(db.Property(name=f"{t}:{ex[0]}", datatype=t, value=ex[0]))
+        t09.add_property(name=f"{t}:{ex[1]}", datatype=t, value=ex[1])
+        t10.add_property(name=f"{t}:{ex[1]}", datatype=t, value=ex[1])
+    assert empty_diff(t09, t10)
+    t09.add_property(name=f"diff", value=1)
+    t10.add_property(name=f"diff", value=2)
+    assert not empty_diff(t09, t10)
+    # Default values
+    t09, t10 = db.Record(), db.Record()
+    t09.add_property(db.Property(name=f"A1"), value="A")
+    t10.add_property(name=f"A1", value="A")
+    t09.add_property(db.Property(id=12, name=f"A2"), value="A")
+    t10.add_property(id=12, name=f"A2", value="A")
+    t09.add_property(db.Property(id=15), value="A")
+    t10.add_property(id=15, value="A")
+    assert empty_diff(t09, t10)
+    # ToDo: extended tests for references
+
+
 def test_compare_special_properties():
     # Test for all known special properties:
-    SPECIAL_PROPERTIES = ("description", "name",
-                          "checksum", "size", "path", "id")
     INTS = ("size", "id")
     HIDDEN = ("checksum", "size")
 
-    for key in SPECIAL_PROPERTIES:
+    for key in SPECIAL_ATTRIBUTES:
         set_key = key
         if key in HIDDEN:
             set_key = "_" + key
@@ -215,8 +522,7 @@ def test_compare_special_properties():
         assert len(diff_r1["properties"]) == 0
         assert len(diff_r2["properties"]) == 0
 
-
-def test_compare_properties():
+    # compare Property objects
     p1 = db.Property()
     p2 = db.Property()
 
@@ -467,10 +773,10 @@ def test_empty_diff():
     rec_a.remove_property("RefType")
     rec_b.remove_property("RefType")
     assert empty_diff(rec_a, rec_b)
-    rec_a.add_property(name="RefType", datatype=db.LIST(
-        "RefType"), value=[ref_rec_a, ref_rec_a])
-    rec_b.add_property(name="RefType", datatype=db.LIST(
-        "RefType"), value=[ref_rec_b, ref_rec_b])
+    rec_a.add_property(name="RefType", datatype=db.LIST("RefType"),
+                       value=[ref_rec_a, ref_rec_a])
+    rec_b.add_property(name="RefType", datatype=db.LIST("RefType"),
+                       value=[ref_rec_b, ref_rec_b])
     assert not empty_diff(rec_a, rec_b)
     assert empty_diff(rec_a, rec_b, compare_referenced_records=True)
 
@@ -568,6 +874,21 @@ B: something else"""
     # unchanged
     assert recB.get_property("propA").unit == "cm"
 
+    # test whether an id is correctly overwritten by an entity without id
+    recA = db.Record().add_parent("A").add_property(name="B", value=112)
+    newRec = db.Record().add_parent("B").add_property("c")
+    recB = db.Record().add_parent("A").add_property(name="B", value=newRec)
+
+    merge_entities(recA, recB, force=True)
+    assert recA.get_property("B").value == newRec
+
+    recA = db.Record().add_parent("A").add_property(name="B", value=[112],
+                                                    datatype=db.LIST("B"))
+    recB = db.Record().add_parent("A").add_property(name="B", value=[newRec], datatype=db.LIST(db.REFERENCE))
+
+    merge_entities(recA, recB, force=True)
+    assert recA.get_property("B").value == [newRec]
+
 
 def test_merge_missing_list_datatype_82():
     """Merging two properties, where the list-valued one has no datatype."""
@@ -601,13 +922,12 @@ def test_merge_id_with_resolved_entity():
 
     # Overwrite from right to left in both cases
     merge_entities(recA, recB, merge_id_with_resolved_entity=True)
-    assert recA.get_property(rtname).value == ref_id
-    assert recA.get_property(rtname).value == recB.get_property(rtname).value
+    assert recA.get_property(rtname).value == ref_rec
 
     recA = db.Record().add_property(name=rtname, value=ref_rec)
     merge_entities(recB, recA, merge_id_with_resolved_entity=True)
-    assert recB.get_property(rtname).value == ref_rec
-    assert recA.get_property(rtname).value == recB.get_property(rtname).value
+    assert recB.get_property(rtname).value == ref_id
+    assert recA.get_property(rtname).value == ref_rec
 
     # id mismatches
     recB = db.Record().add_property(name=rtname, value=ref_id*2)
@@ -623,7 +943,51 @@ def test_merge_id_with_resolved_entity():
     # also works in lists:
     recA = db.Record().add_property(
         name=rtname, datatype=db.LIST(rtname), value=[ref_rec, ref_id*2])
-    recB = db.Record().add_property(name=rtname, datatype=db.LIST(rtname), value=[ref_id, ref_id*2])
+    recB = db.Record().add_property(
+        name=rtname, datatype=db.LIST(rtname), value=[ref_id, ref_id*2])
     merge_entities(recA, recB, merge_id_with_resolved_entity=True)
-    assert recA.get_property(rtname).value == [ref_id, ref_id*2]
-    assert recA.get_property(rtname).value == recB.get_property(rtname).value
+    assert recA.get_property(rtname).value == [ref_rec, ref_id*2]
+    assert recB.get_property(rtname).value == [ref_id, ref_id*2]
+
+
+def test_describe_diff():
+    recA = db.Record()
+    recA.add_property(name="propA", value=2)
+    recA.add_property(name="propB", value=2)
+    recA.add_property(name="propD", value=-273, unit="K")
+
+    recB = db.Record()
+    recB.add_property(name="propA", value=2)
+    recB.add_property(name="propB", value=12)
+    recB.add_property(name="propC", value="cool 17")
+    recB.add_property(name="propD", value=-273, unit="°C")
+
+    diff = compare_entities(recA, recB)
+    diffout = describe_diff(*diff)
+
+    assert diffout.startswith("## Difference between the first version and the second version of None")
+
+    # The output of the describe_diff function is currently not ordered (e.g. by name of the property)
+    # so we cannot just compare a well-defined output string.
+
+    assert "it does not exist in the first version:" in diffout
+    assert "first version: {'value': 2}" in diffout
+    assert "second version: {'value': 12}" in diffout
+
+    assert "first version: {'unit': 'K'}" in diffout
+    assert "second version: {'unit': '°C'}" in diffout
+
+    diffout = describe_diff(*diff, name="Entity")
+    assert diffout.startswith("## Difference between the first version and the second version of Entity")
+
+    diffout = describe_diff(*diff, label_e0="recA", label_e1="recB")
+    assert "recA: {'value': 2}" in diffout
+    assert "recB: {'value': 12}" in diffout
+
+    assert "recA: {'unit': 'K'}" in diffout
+    assert "recB: {'unit': '°C'}" in diffout
+
+    assert "it does not exist in the recA:" in diffout
+
+    assert "first" not in diffout
+    assert "second" not in diffout
diff --git a/unittests/test_entity.py b/unittests/test_entity.py
index abf82f0a9b557cf9d1d2365e01fedaa4eae0c565..2127ce028f4de55b8ef0ca704c1e69959c24ba82 100644
--- a/unittests/test_entity.py
+++ b/unittests/test_entity.py
@@ -22,14 +22,17 @@
 # ** end header
 #
 """Tests for the Entity class."""
+import os
 # pylint: disable=missing-docstring
 import unittest
-from lxml import etree
 
-import os
-from linkahead import (INTEGER, Entity, Property, Record, RecordType,
+import linkahead
+from linkahead import (INTEGER, Entity, Parent, Property, Record, RecordType,
                        configure_connection)
+from linkahead.common.models import SPECIAL_ATTRIBUTES
 from linkahead.connection.mockup import MockUpServerConnection
+from lxml import etree
+from pytest import raises
 
 UNITTESTDIR = os.path.dirname(os.path.abspath(__file__))
 
@@ -82,7 +85,13 @@ class TestEntity(unittest.TestCase):
         self.assertEqual(entity.to_xml().tag, "Property")
 
     def test_instantiation(self):
-        self.assertRaises(Exception, Entity())
+        e = Entity()
+        for attr in SPECIAL_ATTRIBUTES:
+            assert hasattr(e, attr)
+
+    def test_instantiation_bad_argument(self):
+        with self.assertRaises(Exception):
+            Entity(rol="File")
 
     def test_parse_role(self):
         """During parsing, the role of an entity is set explicitely. All other
@@ -97,3 +106,179 @@ class TestEntity(unittest.TestCase):
         # test whether the __role property of this object has explicitely been
         # set.
         self.assertEqual(getattr(entity, "_Entity__role"), "Record")
+
+
+def test_parent_list():
+    p1 = RecordType(name="A")
+    pl = linkahead.common.models.ParentList([p1])
+    assert p1 in pl
+    assert pl.index(p1) == 0
+    assert RecordType(name="A") not in pl
+    assert RecordType(id=101) not in pl
+    p2 = RecordType(id=101)
+    pl.append(p2)
+    assert p2 in pl
+    assert len(pl) == 2
+    p3 = RecordType(id=103, name='B')
+    pl.append(p3)
+    assert len(pl) == 3
+
+    # test removal
+    # remove by id only, even though element in parent list has name and id
+    pl.remove(RecordType(id=103))
+    assert len(pl) == 2
+    assert p3 not in pl
+    assert p2 in pl
+    assert p1 in pl
+    # Same for removal by name
+    pl.append(p3)
+    assert len(pl) == 3
+    pl.remove(RecordType(name='B'))
+    assert len(pl) == 2
+    assert p3 not in pl
+    # And an error if no suitable element can be found
+    with raises(KeyError) as ve:
+        pl.remove(RecordType(id=105, name='B'))
+    assert "not found" in str(ve.value)
+    assert len(pl) == 2
+
+    # TODO also check pl1 == pl2
+
+
+def test_property_list():
+    # TODO: Resolve parent-list TODOs, then transfer to here.
+    # TODO: What other considerations have to be done with properties?
+    p1 = Property(name="A")
+    pl = linkahead.common.models.PropertyList()
+    pl.append(p1)
+    assert p1 in pl
+    assert Property(id=101) not in pl
+    p2 = Property(id=101)
+    pl.append(p2)
+    assert p1 in pl
+    assert p2 in pl
+    p3 = Property(id=103, name='B')
+    pl.append(p3)
+
+
+def test_filter():
+    rt1 = RecordType(id=100)
+    rt2 = RecordType(id=101, name="RT")
+    rt3 = RecordType(name="")
+    p1 = Property(id=100)
+    p2 = Property(id=100)
+    p3 = Property(id=101, name="RT")
+    p4 = Property(id=102, name="P")
+    p5 = Property(id=103, name="P")
+    p6 = Property(name="")
+    r1 = Record(id=100)
+    r2 = Record(id=100)
+    r3 = Record(id=101, name="RT")
+    r4 = Record(id=101, name="R")
+    r5 = Record(id=104, name="R")
+    r6 = Record(id=105, name="R")
+    test_ents = [rt1, rt2, rt3, p1, p2, p3, p4, p5, p6, r1, r2, r3, r4, r5, r6]
+
+    # Setup
+    for entity in [Property(name=""), Record(name=""), RecordType(name="")]:
+        for coll in [entity.properties, entity.parents]:
+            for ent in test_ents:
+                assert ent not in coll
+                assert ent not in coll.filter(ent)
+
+        # Checks with each type
+        t, t_props, t_pars = entity, entity.properties, entity.parents
+        # Properties
+        # Basic Checks
+        t.add_property(p1)
+        tp1 = t.properties[-1]
+        t.add_property(p3)
+        tp3 = t.properties[-1]
+        assert len(t_props.filter(pid=100)) == 1
+        assert tp1 in t_props.filter(pid=100)
+        assert len(t_props.filter(pid="100")) == 1
+        assert tp1 in t_props.filter(pid="100")
+        assert len(t_props.filter(pid=101, name="RT")) == 1
+        assert tp3 in t_props.filter(pid=101, name="RT")
+        for entity in [rt1, p2, r1, r2]:
+            assert entity not in t_props.filter(pid=100)
+            assert tp1 in t_props.filter(entity)
+        # Check that direct addition (not wrapped) works
+        t_props.append(p2)
+        tp2 = t_props[-1]
+        assert tp2 in t_props.filter(pid=100)
+        assert tp2 not in t_props.filter(pid=101, name="RT")
+        for entity in [rt1, r1, r2]:
+            assert entity not in t_props.filter(pid=100)
+            assert tp2 in t_props.filter(entity)
+
+        # Parents
+        # Filtering with both name and id
+        t.add_parent(r3)
+        tr3 = t.parents[-1]
+        t.add_parent(r5)
+        tr5 = t.parents[-1]
+        assert tr3 in t_pars.filter(pid=101)
+        assert tr5 not in t_pars.filter(pid=101)
+        assert tr3 not in t_pars.filter(name="R")
+        assert tr5 in t_pars.filter(name="R")
+        assert tr3 in t_pars.filter(pid=101, name="R")
+        assert tr5 not in t_pars.filter(pid=101, name="R")
+        assert tr3 not in t_pars.filter(pid=104, name="RT")
+        assert tr5 in t_pars.filter(pid=104, name="RT")
+        assert tr3 not in t_pars.filter(pid=105, name="T")
+        assert tr5 not in t_pars.filter(pid=105, name="T")
+        # Works also without id / name and with duplicate parents
+        for ent in test_ents:
+            t.add_parent(ent)
+        for ent in t_pars:
+            assert ent in t_pars.filter(ent)
+
+    # Grid-Based
+    r7 = Record()
+    r7.add_property(Property()).add_property(name="A").add_property(name="B")
+    r7.add_property(id=27).add_property(id=27, name="A").add_property(id=27, name="B")
+    r7.add_property(id=43).add_property(id=43, name="A").add_property(id=43, name="B")
+    assert len(r7.properties.filter(pid=27)) == 3
+    assert len(r7.properties.filter(pid=43)) == 3
+    assert len(r7.properties.filter(pid=43, conjunction=True)) == 3
+    assert len(r7.properties.filter(name="A")) == 3
+    assert len(r7.properties.filter(name="B")) == 3
+    assert len(r7.properties.filter(name="B", conjunction=True)) == 3
+    assert len(r7.properties.filter(pid=1, name="A")) == 1
+    assert len(r7.properties.filter(pid=1, name="A", conjunction=True)) == 0
+    assert len(r7.properties.filter(pid=27, name="B")) == 4
+    assert len(r7.properties.filter(pid=27, name="B", conjunction=True)) == 1
+    assert len(r7.properties.filter(pid=27, name="C")) == 3
+    assert len(r7.properties.filter(pid=27, name="C", conjunction=True)) == 0
+    # Entity based filtering behaves the same
+    assert (r7.properties.filter(pid=27) ==
+            r7.properties.filter(Property(id=27)))
+    assert (r7.properties.filter(pid=43, conjunction=True) ==
+            r7.properties.filter(Property(id=43), conjunction=True))
+    assert (r7.properties.filter(name="A") ==
+            r7.properties.filter(Property(name="A")))
+    assert (r7.properties.filter(name="B") ==
+            r7.properties.filter(Property(name="B")))
+    assert (r7.properties.filter(name="B", conjunction=True) ==
+            r7.properties.filter(Property(name="B"), conjunction=True))
+    assert (r7.properties.filter(pid=1, name="A") ==
+            r7.properties.filter(Property(id=1, name="A")))
+    assert (r7.properties.filter(pid=1, name="A", conjunction=True) ==
+            r7.properties.filter(Property(id=1, name="A"), conjunction=True))
+    assert (r7.properties.filter(pid=27, name="B") ==
+            r7.properties.filter(Property(id=27, name="B")))
+    assert (r7.properties.filter(pid=27, name="B", conjunction=True) ==
+            r7.properties.filter(Property(id=27, name="B"), conjunction=True))
+    assert (r7.properties.filter(pid=27, name="C") ==
+            r7.properties.filter(Property(id=27, name="C")))
+    assert (r7.properties.filter(pid=27, name="C", conjunction=True) ==
+            r7.properties.filter(Property(id=27, name="C"), conjunction=True))
+    # Name only matching and name overwrite
+    r8 = Record().add_property(name="A").add_property(name="B").add_property(name="B")
+    r8.add_property(Property(name="A"), name="B")
+    r8.add_property(Property(name="A", id=12), name="C")
+    assert len(r8.properties.filter(name="A")) == 1
+    assert len(r8.properties.filter(name="B")) == 3
+    assert len(r8.properties.filter(name="C")) == 1
+    assert len(r8.properties.filter(pid=12)) == 1