diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f83bf13c941d3d1af7072e309b46f08dee0e32c..601372161fb23b08e622f55193833d9d9652444a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,20 +5,42 @@ 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).
 
-## [Unreleased] ##
+* `_ParentList` is now called `ParentList`
+* `_Properties` is now called `PropertyList`
+
+## [0.15.1] - 2024-08-21 ##
+
+### Deprecated ###
+
+* `connection.get_username`. Use `la.Info().user_info.name` instead.
+
+### Fixed ###
+
+* [#128](https://gitlab.com/linkahead/linkahead-pylib/-/issues/128)
+  Assign `datetime.date` or `datetime.datetime` values to `DATETIME`
+  properties.
+
+### Documentation ###
+
+* Added docstrings for `linkahead.models.Info` and `linkahead.models.UserInfo`.
+
+## [0.15.0] - 2024-07-09 ##
 
 ### Added ###
 
 * Support for Python 3.12
 * The `linkahead` module now opts into type checking and supports mypy.
+* [#112](https://gitlab.com/linkahead/linkahead-pylib/-/issues/112)
+  `Entity.update_acl` now supports optional `**kwargs` that are passed to the
+  `Entity.update` method that is called internally, thus allowing, e.g.,
+  updating the ACL despite possible naming collisions with `unique=False`.
+* a `role` argument for `get_entity_by_name` and `get_entity_by_id`
 
 ### Changed ###
 * `in` operator now test whether a parent with the appropriate ID, name or both is in `ParentList`
 * `in` operator now test whether a parent with the appropriate ID, name or both is in `PropertyList`
 
-### Deprecated ###
-* `_ParentList` is now called `ParentList`
-* `_Properties` is now called `PropertyList`
+* Using environment variable PYLINKAHEADINI instead of PYCAOSDBINI.
 
 ### Removed ###
 
@@ -31,11 +53,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   of a `Container`, removing all useful methods of the `Container` class. This
   has been fixed and using a `slice` such as `[:2]` now returns a new
   `Container`.
-
-### Security ###
+* [#120](https://gitlab.com/linkahead/linkahead-pylib/-/issues/120) Unwanted
+  subproperties in reference properties.
 
 ### Documentation ###
 
+* Added documentation and a tutorial example for the usage of the `page_length`
+  argument of `execute_query`.
+
 ## [0.14.0] - 2024-02-20
 
 ### Added ###
diff --git a/CITATION.cff b/CITATION.cff
index cbcb570b27b7cd71f50645614222302bccc34805..3f51bdf839a5e0451f3d3aaf7f128f61b29927fc 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.14.0
+version: 0.15.1
 doi: 10.3390/data4020083
-date-released: 2024-02-20
+date-released: 2024-08-21
diff --git a/Makefile b/Makefile
index 9e4d30dbf8dab85892c220136466360f48d89042..21ea40ac8a6eb34032aba75c089e278fa354a6f5 100644
--- a/Makefile
+++ b/Makefile
@@ -44,7 +44,7 @@ lint:
 .PHONY: lint
 
 mypy:
-	mypy src/linkahead/common unittests --exclude high_level_api.py --exclude connection.py
+	mypy src/linkahead/common unittests
 .PHONY: mypy
 
 unittest:
diff --git a/README_SETUP.md b/README_SETUP.md
index b05eff87711b84682aa82bbd0aafd61f2e8c86eb..8a32fbfacb8fd5733c65998b35e52e1c7bbceab1 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -119,6 +119,7 @@ Build documentation in `build/` with `make doc`.
 - `sphinx`
 - `sphinx-autoapi`
 - `recommonmark`
+- `sphinx_rtd_theme`
 
 ### How to contribute ###
 
diff --git a/examples/pycaosdb_example.py b/examples/pycaosdb_example.py
deleted file mode 100755
index 9a3d766791ca7a6fd111d734d08ac4cf3b85b75a..0000000000000000000000000000000000000000
--- a/examples/pycaosdb_example.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env python3
-"""A small example to get started with caosdb-pylib.
-
-Make sure that a `pylinkahead.ini` is readable at one of the expected locations.
-"""
-
-import random
-
-import caosdb as db
-
-
-def reconfigure_connection():
-    """Change the current connection configuration."""
-    conf = db.configuration.get_config()
-    conf.set("Connection", "url", "https://demo.indiscale.com")
-    db.configure_connection()
-
-
-def main():
-    """Shows a few examples how to use the CaosDB library."""
-    conf = dict(db.configuration.get_config().items("Connection"))
-    print("##### Config:\n{}\n".format(conf))
-
-    if conf["cacert"] == "/path/to/caosdb.ca.pem":
-        print("Very likely, the path the the TLS certificate is not correct, "
-              "please fix it.")
-
-    # Query the server, the result is a Container
-    result = db.Query("FIND Record").execute()
-    print("##### First query result:\n{}\n".format(result[0]))
-
-    # Retrieve a random Record
-    rec_id = random.choice([rec.id for rec in result])
-    rec = db.Record(id=rec_id).retrieve()
-    print("##### Randomly retrieved Record:\n{}\n".format(rec))
-
-
-if __name__ == "__main__":
-    main()
diff --git a/examples/pylinkahead.ini b/examples/pylinkahead.ini
index f37e24e0e5b754ec58a07b034ba2755096f0b441..84d1eb8526201c817d6614e7eb74f35a932c5d78 100644
--- a/examples/pylinkahead.ini
+++ b/examples/pylinkahead.ini
@@ -1,7 +1,7 @@
 # To be found be the caosdb package, the INI file must be located either in
 # - $CWD/pylinkahead.ini
 # - $HOME/.pylinkahead.ini
-# - the location given in the env variable PYCAOSDBINI
+# - the location given in the env variable PYLINKAHEADINI
 
 [Connection]
 # URL of the CaosDB server
diff --git a/examples/pylinkahead_example.py b/examples/pylinkahead_example.py
new file mode 100755
index 0000000000000000000000000000000000000000..6effd57c73669c0aaa0284cb28105ae349dac608
--- /dev/null
+++ b/examples/pylinkahead_example.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+# This file is a part of the LinkAhead Project.
+#
+# Copyright (C) 2024 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Daniel Hornung <d.hornung@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+"""A small example to get started with linkahead-pylib.
+
+Make sure that a `pylinkahead.ini` is readable at one of the expected locations.
+"""
+
+import random
+
+import caosdb as db
+
+
+def reconfigure_connection():
+    """Change the current connection configuration."""
+    conf = db.configuration.get_config()
+    conf.set("Connection", "url", "https://demo.indiscale.com")
+    db.configure_connection()
+
+
+def main():
+    """Shows a few examples how to use the LinkAhead library."""
+    conf = dict(db.configuration.get_config().items("Connection"))
+    print("##### Config:\n{}\n".format(conf))
+
+    if conf["cacert"] == "/path/to/caosdb.ca.pem":
+        print("Very likely, the path to the TLS certificate is not correct, "
+              "please fix it.")
+
+    # Query the server, the result is a Container
+    result = db.Query("FIND Record").execute()
+    print("##### First query result:\n{}\n".format(result[0]))
+
+    # Retrieve a random Record
+    rec_id = random.choice([rec.id for rec in result])
+    rec = db.Record(id=rec_id).retrieve()
+    print("##### Randomly retrieved Record:\n{}\n".format(rec))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/setup.cfg b/setup.cfg
index c46089e4d24843d7d4cc4f83dad6ec1351e4cc3f..b7f1a7395a53c32c2e43c72db0b359e4a7aaadb6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,4 +1,11 @@
 [aliases]
 test=pytest
+
 [pycodestyle]
 ignore=E501,E121,E123,E126,E226,E24,E704,W503,W504
+
+[mypy]
+ignore_missing_imports = True
+
+# [mypy-linkahead.*]
+# check_untyped_defs = True
\ No newline at end of file
diff --git a/setup.py b/setup.py
index ee2a5fb6fd7212acfc9ce9bc732fc9f2d4f345b4..6ad2d0b9ef1e4c07d6519562a0c75c72c51b5b75 100755
--- a/setup.py
+++ b/setup.py
@@ -46,9 +46,9 @@ from setuptools import find_packages, setup
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ########################################################################
 
-ISRELEASED = False
+ISRELEASED = True
 MAJOR = 0
-MINOR = 14
+MINOR = 15
 MICRO = 1
 # Do not tag as pre-release until this commit
 # https://github.com/pypa/packaging/pull/515
diff --git a/src/doc/conf.py b/src/doc/conf.py
index 61a60d7c9e8d5c6b0959f4bba230cd483c06bc79..7b127420c281e37e82ee0e64768ae831e30e2798 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -29,10 +29,10 @@ copyright = '2023, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.14.1'
+version = '0.15.1'
 # The full version, including alpha/beta/rc tags
 # release = '0.5.2-rc2'
-release = '0.14.1-dev'
+release = '0.15.1'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/src/doc/configuration.md b/src/doc/configuration.md
index 54ae251b9db9ef000545e701406b979aa58043f8..427551db4e1e97d7ca5f9820df6d5916e3496020 100644
--- a/src/doc/configuration.md
+++ b/src/doc/configuration.md
@@ -1,6 +1,6 @@
 # Configuration of PyLinkAhead #
 The behavior of PyLinkAhead is defined via a configuration that is provided using configuration files.
-PyLinkAhead tries to read from the inifile specified in the environment variable `PYCAOSDBINI` or
+PyLinkAhead tries to read from the inifile specified in the environment variable `PYLINKAHEADINI` or
 alternatively in `~/.pylinkahead.ini` upon import.  After that, the ini file `pylinkahead.ini` in the
 current working directory will be read additionally, if it exists.
 
diff --git a/src/doc/tutorials/index.rst b/src/doc/tutorials/index.rst
index 706e26c2b1b4876c29d43c2bddd9a5fe357a003d..e745482ace189e10a042975869cae6310f6ad703 100644
--- a/src/doc/tutorials/index.rst
+++ b/src/doc/tutorials/index.rst
@@ -15,6 +15,7 @@ advanced usage of the Python client.
    Data-Insertion
    errors
    Entity-Getters
+   paginated_queries
    caching
    data-model-interface
    complex_data_models
diff --git a/src/doc/tutorials/paginated_queries.rst b/src/doc/tutorials/paginated_queries.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c250223f46405caa9d289ce4d8774daf06fdf366
--- /dev/null
+++ b/src/doc/tutorials/paginated_queries.rst
@@ -0,0 +1,63 @@
+Query pagination
+================
+
+When retrieving many entities, you may not want to retrieve all at once, e.g.,
+for performance reasons or to prevent connection timeouts, but rather in a
+chunked way. For that purpose, there is the ``page_length`` parameter in the
+:py:meth:`~linkahead.common.models.execute_query` function. If this is set to a
+non-zero integer, the behavior of the function changes in that it returns a
+Python `generator <https://docs.python.org/3/glossary.html#term-generator>`_
+which can be used, e.g., in loops or in list comprehension. The generator yields
+a :py:class:`~linkahead.common.models.Container` containing the next
+``page_length`` many entities from the query result.
+
+The following example illustrates this on the demo server.
+
+.. code-block:: python
+
+   import linkahead as db
+
+   # 10 at the time of writing of this example
+   print(db.execute_query("FIND MusicalInstrument"))
+
+   # Retrieve in pages of length 5 and iterate over the pages
+   for page in db.execute_query("FIND MusicalInstrument", page_length=5):
+       # each page is a container
+       print(type(page))
+       # exactly page_length=5 for the first N-1 pages,
+       # and possibly less for the last page
+       print(len(page))
+       # the items on each page are subclasses of Entity
+       print(type(page[0]))
+       # The id of the first entity on the page is different for all pages
+       print(page[0].id)
+
+   # You can use this in a list comprehension to fill a container
+   container_paginated = db.Container().extend(
+       [ent for page in db.execute_query("FIND MusicalInstrument", page_length=5) for ent in page]
+   )
+   # The result is the same as in the unpaginated case, but the
+   # following can cause connection timeouts in case of very large
+   # retrievals
+   container_at_once = db.execute_query("FIND MusicalInstrument")
+   for ent1, ent2 in zip(container_paginated, container_at_once):
+      print(ent1.id == ent2.id)  # always true
+
+As you can see, you can iterate over a paginated query and then access the
+entities on each page during the iteration.
+
+.. note::
+
+   The ``page_length`` keyword is ignored for ``COUNT`` queries where
+   :py:meth:`~linkahead.common.models.execute_query` always returns the integer
+   result and in case of ``unique=True`` where always exactly one
+   :py:class:`~linkahead.common.models.Entity` is returned.
+
+
+.. warning::
+
+   Be careful when combining query pagination with insert, update, or delete
+   operations. If your database changes while iterating over a paginated query,
+   the client will raise a
+   :py:exc:`~linkahead.exceptions.PagingConsistencyError` since the server
+   can't guarantee that the query results haven't changed in the meantime.
diff --git a/src/linkahead/__init__.py b/src/linkahead/__init__.py
index 3a8c5ba39c88deaa5dc945135e3828945fd39d58..cd54f8f4e05326579521fbbf226f027d32fa616e 100644
--- a/src/linkahead/__init__.py
+++ b/src/linkahead/__init__.py
@@ -24,7 +24,7 @@
 
 """LinkAhead Python bindings.
 
-Tries to read from the inifile specified in the environment variable `PYCAOSDBINI` or
+Tries to read from the inifile specified in the environment variable `PYLINKAHEADINI` or
 alternatively in `~/.pylinkahead.ini` upon import.  After that, the ini file `pylinkahead.ini` in
 the current working directory will be read additionally, if it exists.
 
diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py
index e2ed0facea84e6056b1ac877b4417ce6ad8ef504..4ae8edd16f1fdc00eb7ba2c17661eea6e114885e 100644
--- a/src/linkahead/apiutils.py
+++ b/src/linkahead/apiutils.py
@@ -6,6 +6,8 @@
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
 # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
 # Copyright (C) 2020-2022 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Joscha Schmiedt <joscha@schmiedt.dev>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -25,11 +27,11 @@
 """API-Utils: Some simplified functions for generation of records etc.
 
 """
-
+from __future__ import annotations
 import logging
 import warnings
 from collections.abc import Iterable
-from typing import Any, Dict, List
+from typing import Any, Union, Optional
 
 from .common.datatype import is_reference
 from .common.models import (SPECIAL_ATTRIBUTES, Container, Entity, File,
@@ -47,12 +49,14 @@ class EntityMergeConflictError(LinkAheadException):
     """
 
 
-def new_record(record_type, name=None, description=None,
-               tempid=None, insert=False, **kwargs):
+def new_record(record_type: Union[str],
+               name: Optional[str] = None,
+               description: Optional[str] = None,
+               tempid: Optional[int] = None,
+               insert: bool = False, **kwargs) -> Record:
     """Function to simplify the creation of Records.
 
     record_type: The name of the RecordType to use for this record.
-                 (ids should also work.)
     name: Name of the new Record.
     kwargs: Key-value-pairs for the properties of this Record.
 
@@ -92,19 +96,19 @@ def new_record(record_type, name=None, description=None,
     return r
 
 
-def id_query(ids):
+def id_query(ids: list[int]) -> Container:
     warnings.warn("Please use 'create_id_query', which only creates"
                   "the string.", DeprecationWarning)
 
-    return execute_query(create_id_query(ids))
+    return execute_query(create_id_query(ids))  # type: ignore
 
 
-def create_id_query(ids):
+def create_id_query(ids: list[int]) -> str:
     return "FIND ENTITY WITH " + " OR ".join(
         ["ID={}".format(id) for id in ids])
 
 
-def get_type_of_entity_with(id_):
+def get_type_of_entity_with(id_: int):
     objs = retrieve_entities_with_ids([id_])
 
     if len(objs) == 0:
@@ -127,11 +131,11 @@ def get_type_of_entity_with(id_):
         return Entity
 
 
-def retrieve_entity_with_id(eid):
+def retrieve_entity_with_id(eid: int):
     return execute_query("FIND ENTITY WITH ID={}".format(eid), unique=True)
 
 
-def retrieve_entities_with_ids(entities):
+def retrieve_entities_with_ids(entities: list) -> Container:
     collection = Container()
     step = 20
 
@@ -175,7 +179,10 @@ 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(old_entity: Entity,
+                     new_entity: Entity,
+                     compare_referenced_records: bool = False
+                     ) -> tuple[dict[str, Any], dict[str, Any]]:
     """Compare two entites.
 
     Return a tuple of dictionaries, the first index belongs to additional information for old
@@ -209,8 +216,8 @@ def compare_entities(old_entity: Entity, new_entity: Entity, compare_referenced_
         identical records are stored in different objects. Default is False.
 
     """
-    olddiff: Dict[str, Any] = {"properties": {}, "parents": []}
-    newdiff: Dict[str, Any] = {"properties": {}, "parents": []}
+    olddiff: dict[str, Any] = {"properties": {}, "parents": []}
+    newdiff: dict[str, Any] = {"properties": {}, "parents": []}
 
     if old_entity is new_entity:
         return (olddiff, newdiff)
@@ -290,12 +297,15 @@ def compare_entities(old_entity: Entity, new_entity: Entity, compare_referenced_
                     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]):
+                        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
+                                # 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)])
+                                    [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
@@ -328,7 +338,8 @@ def compare_entities(old_entity: Entity, new_entity: Entity, compare_referenced_
     return (olddiff, newdiff)
 
 
-def empty_diff(old_entity: Entity, new_entity: Entity, compare_referenced_records: bool = False):
+def empty_diff(old_entity: Entity, new_entity: Entity,
+               compare_referenced_records: bool = False) -> bool:
     """Check whether the `compare_entities` found any differences between
     old_entity and new_entity.
 
@@ -357,8 +368,12 @@ def empty_diff(old_entity: Entity, new_entity: Entity, compare_referenced_record
     return True
 
 
-def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_empty_diffs=True,
-                   force=False, merge_id_with_resolved_entity: bool = False):
+def merge_entities(entity_a: Entity,
+                   entity_b: Entity,
+                   merge_references_with_empty_diffs=True,
+                   force=False,
+                   merge_id_with_resolved_entity: bool = False
+                   ) -> 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
@@ -441,8 +456,12 @@ def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_emp
                         if merge_id_with_resolved_entity is True and attribute == "value":
                             # Do a special check for the case of an id value on the
                             # one hand, and a resolved entity on the other side.
-                            this = entity_a.get_property(key).value
-                            that = entity_b.get_property(key).value
+                            prop_a = entity_a.get_property(key)
+                            assert prop_a is not None, f"Property {key} not found in entity_a"
+                            prop_b = entity_b.get_property(key)
+                            assert prop_b is not None, f"Property {key} not found in entity_b"
+                            this = prop_a.value
+                            that = prop_b.value
                             same = False
                             if isinstance(this, list) and isinstance(that, list):
                                 if len(this) == len(that):
@@ -465,11 +484,13 @@ def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_emp
         else:
             # TODO: This is a temporary FIX for
             #       https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/105
-            entity_a.add_property(id=entity_b.get_property(key).id,
-                                  name=entity_b.get_property(key).name,
-                                  datatype=entity_b.get_property(key).datatype,
-                                  value=entity_b.get_property(key).value,
-                                  unit=entity_b.get_property(key).unit,
+            prop_b = entity_b.get_property(key)
+            assert prop_b is not None, f"Property {key} not found in entity_b"
+            entity_a.add_property(id=prop_b.id,
+                                  name=prop_b.name,
+                                  datatype=prop_b.datatype,
+                                  value=prop_b.value,
+                                  unit=prop_b.unit,
                                   importance=entity_b.get_importance(key))
             # entity_a.add_property(
             #     entity_b.get_property(key),
@@ -591,7 +612,7 @@ def resolve_reference(prop: Property):
             prop.value = retrieve_entity_with_id(prop.value)
 
 
-def create_flat_list(ent_list: List[Entity], flat: List[Entity]):
+def create_flat_list(ent_list: list[Entity], flat: list[Entity]):
     """
     Recursively adds all properties contained in entities from ent_list to
     the output list flat. Each element will only be added once to the list.
diff --git a/src/linkahead/cached.py b/src/linkahead/cached.py
index b27afe0469bcaac733ece4c0be3d8d124f6305c0..cf1d1d34362335f87c5eca094b5aa9d6b750f68d 100644
--- a/src/linkahead/cached.py
+++ b/src/linkahead/cached.py
@@ -5,6 +5,8 @@
 # Copyright (C) 2023 IndiScale GmbH <info@indiscale.com>
 # Copyright (C) 2023 Henrik tom Wörden <h.tomwoerden@indiscale.com>
 # Copyright (C) 2023 Daniel Hornung <d.hornung@indiscale.com>
+# Copyright (C) 2024 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Joscha Schmiedt <joscha@schmiedt.dev>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -32,9 +34,10 @@ See also
 - ``cached_get_entity_by(...)`` : Get an Entity by name, id, ...
 """
 
+from __future__ import annotations
 from enum import Enum
 from functools import lru_cache
-from typing import Union
+from typing import Any, Optional, Union
 
 from .exceptions import EmptyUniqueQueryError, QueryNotUniqueError
 from .utils import get_entity
@@ -45,7 +48,7 @@ from .common.models import execute_query, Entity, Container
 DEFAULT_SIZE = 33333
 
 # This dict cache is solely for filling the real cache manually (e.g. to reuse older query results)
-_DUMMY_CACHE = {}
+_DUMMY_CACHE: dict[Union[str, int], Any] = {}
 
 
 class AccessType(Enum):
@@ -59,8 +62,10 @@ class AccessType(Enum):
     NAME = 4
 
 
-def cached_get_entity_by(eid: Union[str, int] = None, name: str = None, path: str = None, query:
-                         str = None) -> Entity:
+def cached_get_entity_by(eid: Union[str, int, None] = None,
+                         name: Optional[str] = None,
+                         path: Optional[str] = None,
+                         query: Optional[str] = None) -> Union[Entity, tuple[None]]:
     """Return a single entity that is identified uniquely by one argument.
 
 You must supply exactly one argument.
@@ -99,7 +104,7 @@ If a query phrase is given, the result must be unique.  If this is not what you
     raise RuntimeError("This line should never be reached.")
 
 
-def cached_query(query_string) -> Container:
+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.
@@ -111,8 +116,8 @@ All additional arguments are at their default values.
     return result
 
 
-@lru_cache(maxsize=DEFAULT_SIZE)
-def _cached_access(kind: AccessType, value: Union[str, int], unique=True):
+@ 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
     # cached_get_entity_by with the different AccessTypes. However, there is only one cache size.
@@ -123,12 +128,24 @@ def _cached_access(kind: AccessType, value: Union[str, int], unique=True):
 
     try:
         if kind == AccessType.QUERY:
+            if not isinstance(value, str):
+                raise TypeError(
+                    f"If AccessType is QUERY, value must be a string, not {type(value)}.")
             return execute_query(value, unique=unique)
         if kind == AccessType.NAME:
+            if not isinstance(value, str):
+                raise TypeError(
+                    f"If AccessType is NAME, value must be a string, not {type(value)}.")
             return get_entity.get_entity_by_name(value)
         if kind == AccessType.EID:
+            if not isinstance(value, (str, int)):
+                raise TypeError(
+                    f"If AccessType is EID, value must be a string or int, not {type(value)}.")
             return get_entity.get_entity_by_id(value)
         if kind == AccessType.PATH:
+            if not isinstance(value, str):
+                raise TypeError(
+                    f"If AccessType is PATH, value must be a string, not {type(value)}.")
             return get_entity.get_entity_by_path(value)
     except (QueryNotUniqueError, EmptyUniqueQueryError) as exc:
         return exc
@@ -152,7 +169,7 @@ out: named tuple
     return _cached_access.cache_info()
 
 
-def cache_initialize(maxsize=DEFAULT_SIZE) -> None:
+def cache_initialize(maxsize: int = DEFAULT_SIZE) -> None:
     """Create a new cache with the given size for `cached_query` and `cached_get_entity_by`.
 
     This implies a call of :func:`cache_clear`, the old cache is emptied.
@@ -163,7 +180,9 @@ def cache_initialize(maxsize=DEFAULT_SIZE) -> None:
     _cached_access = lru_cache(maxsize=maxsize)(_cached_access.__wrapped__)
 
 
-def cache_fill(items: dict, kind: AccessType = AccessType.EID, unique: bool = True) -> None:
+def cache_fill(items: dict[Union[str, int], Any],
+               kind: AccessType = AccessType.EID,
+               unique: bool = True) -> None:
     """Add entries to the cache manually.
 
     This allows to fill the cache without actually submitting queries.  Note that this does not
@@ -186,6 +205,19 @@ unique: bool, optional
   :func:`cached_query`.
 
     """
+
+    if kind == AccessType.QUERY:
+        assert all(isinstance(key, str) for key in items.keys()), "Keys must be strings."
+    elif kind == AccessType.NAME:
+        assert all(isinstance(key, str) for key in items.keys()), "Keys must be strings."
+    elif kind == AccessType.EID:
+        assert all(isinstance(key, (str, int))
+                   for key in items.keys()), "Keys must be strings or integers."
+    elif kind == AccessType.PATH:
+        assert all(isinstance(key, str) for key in items.keys()), "Keys must be strings."
+    else:
+        raise ValueError(f"Unknown AccessType: {kind}")
+
     # 1. add the given items to the corresponding dummy dict cache
     _DUMMY_CACHE.update(items)
 
diff --git a/src/linkahead/common/administration.py b/src/linkahead/common/administration.py
index 417081b0dad19ce15049b8ce05aeef8cc86607f7..dee341fa84dd85cbd41a77c0e2d510a96f2c4824 100644
--- a/src/linkahead/common/administration.py
+++ b/src/linkahead/common/administration.py
@@ -23,8 +23,8 @@
 #
 # ** end header
 #
-
-"""missing docstring."""
+from __future__ import annotations
+"""Utility functions for server and user administration."""
 
 import random
 import re
@@ -38,8 +38,12 @@ from ..exceptions import (EntityDoesNotExistError, HTTPClientError,
                           ServerConfigurationException)
 from .utils import xml2str
 
+from typing import Optional, TYPE_CHECKING, Union
+if TYPE_CHECKING:
+    from ..common.models import Entity
+
 
-def set_server_property(key, value):
+def set_server_property(key: str, value: str):
     """set_server_property.
 
     Set a server property.
@@ -65,7 +69,7 @@ def set_server_property(key, value):
             "Debug mode in server is probably disabled.") from None
 
 
-def get_server_properties():
+def get_server_properties() -> dict[str, Optional[str]]:
     """get_server_properties.
 
     Get all server properties as a dict.
@@ -84,7 +88,7 @@ def get_server_properties():
             "Debug mode in server is probably disabled.") from None
 
     xml = etree.parse(body)
-    props = dict()
+    props: dict[str, Optional[str]] = dict()
 
     for elem in xml.getroot():
         props[elem.tag] = elem.text
@@ -92,7 +96,7 @@ def get_server_properties():
     return props
 
 
-def get_server_property(key):
+def get_server_property(key: str) -> Optional[str]:
     """get_server_property.
 
     Get a server property.
@@ -149,7 +153,7 @@ def generate_password(length: int):
     return password
 
 
-def _retrieve_user(name, realm=None, **kwargs):
+def _retrieve_user(name: str, realm: Optional[str] = None, **kwargs):
     con = get_connection()
     try:
         return con._http_request(method="GET", path="User/" + (realm + "/" + name if realm is not None else name), **kwargs).read()
@@ -161,7 +165,7 @@ def _retrieve_user(name, realm=None, **kwargs):
         raise
 
 
-def _delete_user(name, **kwargs):
+def _delete_user(name: str, **kwargs):
     con = get_connection()
     try:
         return con._http_request(method="DELETE", path="User/" + name, **kwargs).read()
@@ -173,10 +177,14 @@ def _delete_user(name, **kwargs):
         raise
 
 
-def _update_user(name, realm=None, password=None, status=None,
-                 email=None, entity=None, **kwargs):
+def _update_user(name: str,
+                 realm: Optional[str] = None,
+                 password: Optional[str] = None,
+                 status: Optional[str] = None,
+                 email: Optional[str] = None,
+                 entity: Optional[Entity] = None, **kwargs):
     con = get_connection()
-    params = {}
+    params: dict[str, Optional[str]] = {}
 
     if password is not None:
         params["password"] = password
@@ -204,9 +212,13 @@ def _update_user(name, realm=None, password=None, status=None,
         raise
 
 
-def _insert_user(name, password=None, status=None, email=None, entity=None, **kwargs):
+def _insert_user(name: str,
+                 password: Optional[str] = None,
+                 status: Optional[str] = None,
+                 email: Optional[str] = None,
+                 entity: Optional[Entity] = None, **kwargs):
     con = get_connection()
-    params = {"username": name}
+    params: dict[str, Union[str, Entity]] = {"username": name}
 
     if password is not None:
         params["password"] = password
@@ -394,15 +406,15 @@ priority : bool, optional
     """
 
     @staticmethod
-    def _parse_boolean(bstr):
+    def _parse_boolean(bstr) -> bool:
         return str(bstr) in ["True", "true", "TRUE", "yes"]
 
-    def __init__(self, action, permission, priority=False):
+    def __init__(self, action: str, permission: str, priority: bool = False):
         self._action = action
         self._permission = permission
         self._priority = PermissionRule._parse_boolean(priority)
 
-    def _to_xml(self):
+    def _to_xml(self) -> etree._Element:
         xml = etree.Element(self._action)
         xml.set("permission", self._permission)
 
@@ -412,12 +424,15 @@ priority : bool, optional
         return xml
 
     @staticmethod
-    def _parse_element(elem):
-        return PermissionRule(elem.tag, elem.get(
-            "permission"), elem.get("priority"))
+    def _parse_element(elem: etree._Element):
+        permission = elem.get("permission")
+        if permission is None:
+            raise ValueError(f"Permission is missing in PermissionRule xml: {elem}")
+        priority = PermissionRule._parse_boolean(elem.get("priority"))
+        return PermissionRule(elem.tag, permission, priority if priority is not None else False)
 
     @staticmethod
-    def _parse_body(body):
+    def _parse_body(body: str):
         xml = etree.fromstring(body)
         ret = set()
 
diff --git a/src/linkahead/common/datatype.py b/src/linkahead/common/datatype.py
index 65e6246c0287f0af07aa604f4bc18ce54615cae2..7afcb7a5beee26a99934640ac41ccf403f9325fe 100644
--- a/src/linkahead/common/datatype.py
+++ b/src/linkahead/common/datatype.py
@@ -22,12 +22,15 @@
 #
 # ** end header
 #
-
+from __future__ import annotations
 import re
-import sys
 
-if sys.version_info >= (3, 8):
-    from typing import Literal
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+    from typing import Literal, Union
+    from linkahead.common.models import Entity, Container
+    DATATYPE = Literal["DOUBLE", "REFERENCE", "TEXT", "DATETIME", "INTEGER", "FILE", "BOOLEAN"]
+
 
 from ..exceptions import EmptyUniqueQueryError, QueryNotUniqueError
 
@@ -38,19 +41,16 @@ DATETIME = "DATETIME"
 INTEGER = "INTEGER"
 FILE = "FILE"
 BOOLEAN = "BOOLEAN"
-if sys.version_info >= (3, 8):
-    DATATYPE = Literal["DOUBLE", "REFERENCE", "TEXT", "DATETIME", "INTEGER", "FILE", "BOOLEAN"]
 
 
-def LIST(datatype):
+def LIST(datatype: Union[str, Entity, DATATYPE]) -> str:
     # FIXME May be ambiguous (if name duplicate) or insufficient (if only ID exists).
-    if hasattr(datatype, "name"):
-        datatype = datatype.name
+    datatype = getattr(datatype, "name", datatype)
 
     return "LIST<" + str(datatype) + ">"
 
 
-def get_list_datatype(datatype: str, strict: bool = False):
+def get_list_datatype(datatype: str, strict: bool = False) -> Union[str, None]:
     """Returns the datatype of the elements in the list.  If it not a list, return None."""
     # TODO Union[str, Entity]
     if not isinstance(datatype, str) or not datatype.lower().startswith("list"):
@@ -74,13 +74,13 @@ def get_list_datatype(datatype: str, strict: bool = False):
             return None
 
 
-def is_list_datatype(datatype):
+def is_list_datatype(datatype: str) -> bool:
     """ returns whether the datatype is a list """
 
     return get_list_datatype(datatype) is not None
 
 
-def is_reference(datatype):
+def is_reference(datatype: str) -> bool:
     """Returns whether the value is a reference
 
     FILE and REFERENCE properties are examples, but also datatypes that are
@@ -105,12 +105,12 @@ def is_reference(datatype):
     if datatype in [DOUBLE, BOOLEAN, INTEGER, TEXT, DATETIME]:
         return False
     elif is_list_datatype(datatype):
-        return is_reference(get_list_datatype(datatype))
+        return is_reference(get_list_datatype(datatype))  # type: ignore
     else:
         return True
 
 
-def get_referenced_recordtype(datatype):
+def get_referenced_recordtype(datatype: str) -> str:
     """Return the record type of the referenced datatype.
 
     Raises
@@ -134,7 +134,7 @@ def get_referenced_recordtype(datatype):
         raise ValueError("datatype must be a reference")
 
     if is_list_datatype(datatype):
-        datatype = get_list_datatype(datatype)
+        datatype = get_list_datatype(datatype)  # type: ignore
         if datatype is None:
             raise ValueError("list does not have a list datatype")
 
@@ -145,7 +145,7 @@ def get_referenced_recordtype(datatype):
     return datatype
 
 
-def get_id_of_datatype(datatype):
+def get_id_of_datatype(datatype: str) -> int:
     """ returns the id of a Record Type
 
     This is not trivial, as queries may also return children. A check comparing
@@ -170,12 +170,14 @@ def get_id_of_datatype(datatype):
 
     from .models import execute_query
     if is_list_datatype(datatype):
-        datatype = get_list_datatype(datatype)
+        datatype = get_list_datatype(datatype)  # type: ignore
     q = "FIND RECORDTYPE {}".format(datatype)
 
     # we cannot use unique=True here, because there might be subtypes
-    res = execute_query(q)
-    res = [el for el in res if el.name.lower() == datatype.lower()]
+    res: Container = execute_query(q)  # type: ignore
+    if isinstance(res, int):
+        raise ValueError("FIND RECORDTYPE query returned an `int`")
+    res: list[Entity] = [el for el in res if el.name.lower() == datatype.lower()]  # type: ignore
 
     if len(res) > 1:
         raise QueryNotUniqueError(
diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 304203107091f7813f206b9b236ff76c41ca4844..f537a34bfe902fc8fa4991a0bb26a6d2d7e7cb9f 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -34,7 +34,6 @@ transactions.
 """
 
 from __future__ import annotations  # Can be removed with 3.10.
-from __future__ import print_function, unicode_literals
 
 import re
 import sys
@@ -42,6 +41,7 @@ import warnings
 from builtins import str
 from copy import deepcopy
 from enum import Enum
+from datetime import date, datetime
 from functools import cmp_to_key
 from hashlib import sha512
 from os import listdir
@@ -49,13 +49,14 @@ 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
 
-if TYPE_CHECKING and sys.version_info > (3, 7):
-    from datetime import datetime
-    from typing import Any, Dict, Optional, Type, Union, List, TextIO, Tuple, Literal
+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 warnings import warn
 
@@ -101,15 +102,17 @@ from .versioning import Version
 
 _ENTITY_URI_SEGMENT = "Entity"
 
-OBLIGATORY = "OBLIGATORY"
-SUGGESTED = "SUGGESTED"
-RECOMMENDED = "RECOMMENDED"
-FIX = "FIX"
-ALL = "ALL"
-NONE = "NONE"
+OBLIGATORY: Final = "OBLIGATORY"
+SUGGESTED: Final = "SUGGESTED"
+RECOMMENDED: Final = "RECOMMENDED"
+FIX: Final = "FIX"
+ALL: Final = "ALL"
+NONE: Final = "NONE"
+
 if TYPE_CHECKING:
-    INHERITANCE = Literal["OBLIGATORY", "SUGGESTED", "RECOMMENDED", "ALL", "NONE"]
+    INHERITANCE = Literal["OBLIGATORY", "SUGGESTED", "RECOMMENDED", "ALL", "NONE", "FIX"]
     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"]
@@ -138,7 +141,7 @@ class Entity:
         **kwargs,
     ):
 
-        self.__role = kwargs["role"] if "role" in kwargs else None
+        self.__role: Optional[ROLE] = kwargs["role"] if "role" in kwargs else None
         self._checksum: Optional[str] = None
         self._size = None
         self._upload = None
@@ -147,7 +150,7 @@ class Entity:
         self._wrapped_entity: Optional[Entity] = None
         self._version: Optional[Version] = None
         self._cuid: Optional[str] = None
-        self._flags: Dict[str, str] = dict()
+        self._flags: dict[str, str] = dict()
         self.__value = None
         self.__datatype: Optional[DATATYPE] = None
         self.datatype: Optional[DATATYPE] = datatype
@@ -167,7 +170,7 @@ class Entity:
         self.id: Optional[int] = id
         self.state: Optional[State] = None
 
-    def copy(self):
+    def copy(self) -> Entity:
         """
         Return a copy of entity.
 
@@ -178,6 +181,7 @@ class Entity:
         Special attributes, as defined by the global variable SPECIAL_ATTRIBUTES and additionaly
         the "value" are copied using setattr.
         """
+        new: Union[File, Property, RecordType, Record, Entity]
         if self.role == "File":
             new = File()
         elif self.role == "Property":
@@ -242,7 +246,7 @@ class Entity:
         return self._wrapped_entity.size
 
     @property
-    def id(self):
+    def id(self) -> Any:
         if self.__id is not None:
             return self.__id
 
@@ -252,9 +256,11 @@ class Entity:
         return self._wrapped_entity.id
 
     @id.setter
-    def id(self, new_id):
+    def id(self, new_id) -> None:
         if new_id is not None:
-            self.__id = int(new_id)
+            if not isinstance(new_id, int):
+                new_id = int(new_id)
+            self.__id: Optional[int] = new_id
         else:
             self.__id = None
 
@@ -448,7 +454,8 @@ class Entity:
         """
         # @review Florian Spreckelsen 2022-03-17
         if self.acl is None:
-            raise EntityHasNoAclError("This entity does not have an ACL (yet).")
+            raise EntityHasNoAclError(
+                "This entity does not have an ACL (yet).")
 
         self.acl.deny(realm=realm, username=username, role=role,
                       permission=permission, priority=priority,
@@ -456,6 +463,8 @@ class Entity:
 
     def revoke_denial(self, realm=None, username=None,
                       role=None, permission=None, priority=False):
+        if self.acl is None:
+            raise EntityHasNoAclError("This entity does not have an ACL (yet).")
         self.acl.revoke_denial(
             realm=realm,
             username=username,
@@ -465,6 +474,8 @@ class Entity:
 
     def revoke_grant(self, realm=None, username=None,
                      role=None, permission=None, priority=False):
+        if self.acl is None:
+            raise EntityHasNoAclError("This entity does not have an ACL (yet).")
         self.acl.revoke_grant(
             realm=realm,
             username=username,
@@ -478,7 +489,8 @@ class Entity:
             return permission in self.permissions
 
         if self.acl is None:
-            raise EntityHasNoAclError("This entity does not have an ACL (yet).")
+            raise EntityHasNoAclError(
+                "This entity does not have an ACL (yet).")
         return self.acl.is_permitted(role=role, permission=permission)
 
     def get_all_messages(self) -> Messages:
@@ -593,10 +605,10 @@ class Entity:
             bool,
             datetime,
             Entity,
-            List[int],
-            List[str],
-            List[bool],
-            List[Entity],
+            list[int],
+            list[str],
+            list[bool],
+            list[Entity],
             None,
         ] = None,
         id: Optional[int] = None,
@@ -661,8 +673,8 @@ class Entity:
             If the first parameter is an integer then it is interpreted as the id and id must be
             undefined or None.
         UserWarning
-            If the first parameter is not None and neither an instance of Entity nor an integer it is
-            interpreted as the name and name must be undefined or None.
+            If the first parameter is not None and neither an instance of Entity nor an integer it
+            is interpreted as the name and name must be undefined or None.
 
         Raises
         ------
@@ -678,7 +690,8 @@ class Entity:
 
         >>> import linkahead as db
         >>> rec = db.Record(name="TestRec").add_parent(name="TestType")
-        >>> rec.add_property("TestProp", value=27)  # specified by name, you could equally use the property's id if it is known
+        >>> rec.add_property("TestProp", value=27)  # specified by name, you could equally use the
+        >>>                                         # property's id if it is known
 
         You can also use the Python object:
 
@@ -701,10 +714,12 @@ class Entity:
         Note that since `TestProp` is a scalar integer Property, the datatype
         `LIST<INTEGER>` has to be specified explicitly.
 
-        Finally, we can also add reference properties, specified by the RecordType of the referenced entity.
+        Finally, we can also add reference properties, specified by the RecordType of the referenced
+        entity.
 
         >>> ref_rec = db.Record(name="ReferencedRecord").add_parent(name="OtherRT")
-        >>> rec.add_property(name="OtherRT", value=ref_rec)  # or value=ref_rec.id if ref_rec has one set by the server
+        >>> rec.add_property(name="OtherRT", value=ref_rec)  # or value=ref_rec.id if ref_rec has
+        >>>                                                  # one set by the server
 
         See more on adding properties and inserting data in
         https://docs.indiscale.com/caosdb-pylib/tutorials/Data-Insertion.html.
@@ -724,12 +739,21 @@ class Entity:
             abstract_property = property
         elif isinstance(property, int):
             if pid is not None:
-                raise UserWarning("The first parameter was an integer which would normally be interpreted as the id of the property which is to be added. But you have also specified a parameter 'id' in the method call. This is ambiguous and cannot be processed.")
+                raise UserWarning(
+                    "The first parameter was an integer which would normally be interpreted as the"
+                    " id of the property which is to be added. But you have also specified a"
+                    " parameter 'id' in the method call. This is ambiguous and cannot be processed."
+                )
             pid = property
             id = pid
         elif property is not None:
             if name is not None:
-                raise UserWarning("The first parameter was neither an instance of Entity nor an integer. Therefore the string representation of your first parameter would normally be interpreted name of the property which is to be added. But you have also specified a parameter 'name' in the method call. This is ambiguous and cannot be processed.")
+                raise UserWarning(
+                    "The first parameter was neither an instance of Entity nor an integer."
+                    " Therefore the string representation of your first parameter would normally be"
+                    " interpreted name of the property which is to be added. But you have also"
+                    " specified a parameter 'name' in the method call. This is ambiguous and cannot"
+                    " be processed.")
             name = str(property)
 
         if property is None and name is None and pid is None:
@@ -903,7 +927,7 @@ out: bool
 
         return self.parents
 
-    def get_parents_recursively(self, retrieve: bool = True) -> List[Entity]:
+    def get_parents_recursively(self, retrieve: bool = True) -> list[Entity]:
         """Get all ancestors of this entity.
 
 Parameters
@@ -914,16 +938,16 @@ retrieve: bool, optional
 
 Returns
 -------
-out: List[Entity]
+out: list[Entity]
   The parents of this Entity
 """
 
-        all_parents: List[Entity] = []
+        all_parents: list[Entity] = []
         self._get_parent_recursively(all_parents, retrieve=retrieve)
 
         return all_parents
 
-    def _get_parent_recursively(self, all_parents: List[Entity], retrieve: bool = True):
+    def _get_parent_recursively(self, all_parents: list[Entity], retrieve: bool = True):
         """Get all ancestors with a little helper.
 
         As a side effect of this method, the ancestors are added to
@@ -1044,12 +1068,13 @@ out: List[Entity]
 
                     return p
         else:
-            raise ValueError("`pattern` argument should be an Entity, int or str.")
+            raise ValueError(
+                "`pattern` argument should be an Entity, int or str.")
 
         return None
 
     def _get_value_for_selector(
-        self, selector: Union[str, List[str], Tuple[str]]
+        self, selector: Union[str, list[str], tuple[str]]
     ) -> Any:
         """return the value described by the selector
 
@@ -1143,7 +1168,7 @@ out: List[Entity]
         row : tuple
             A row-like representation of the entity's properties.
         """
-        row = tuple()
+        row: tuple = tuple()
 
         for selector in selectors:
             val = self._get_value_for_selector(selector)
@@ -1191,7 +1216,7 @@ out: List[Entity]
 
         return ret
 
-    def get_errors_deep(self, roots=None) -> List[Tuple[str, List[Entity]]]:
+    def get_errors_deep(self, roots=None) -> list[tuple[str, list[Entity]]]:
         """Get all error messages of this entity and all sub-entities /
         parents / properties.
 
@@ -1436,7 +1461,30 @@ out: List[Entity]
         self.acl = Entity(name=self.name, id=self.id).retrieve(
             flags={"ACL": None}).acl
 
-    def update_acl(self):
+    def update_acl(self, **kwargs):
+        """Update this entity's ACL on the server.
+
+        A typical workflow is to first edit ``self.acl`` and then call this
+        method.
+
+        Note
+        ----
+        This overwrites any existing ACL, so you may want to run
+        ``retrieve_acl`` before updating the ACL in this entity.
+
+        Parameters
+        ----------
+        **kwargs : dict
+            Keyword arguments that are passed through to the
+            ``Entity.update`` method.  Useful for e.g. ``unique=False`` in the
+            case of naming collisions.
+
+        Returns
+        -------
+        e : Entity
+            This entity after the update of the ACL.
+
+        """
         if self.id is None:
             c = Container().retrieve(query=self.name, sync=False)
 
@@ -1456,8 +1504,10 @@ out: List[Entity]
                 raise TransactionError(ae)
         else:
             e = Container().retrieve(query=self.id, sync=False)[0]
+        if self.acl is None:
+            raise EntityHasNoAclError("This entity does not have an ACL yet. Please set one first.")
         e.acl = ACL(self.acl.to_xml())
-        e.update()
+        e.update(**kwargs)
 
         return e
 
@@ -1513,13 +1563,14 @@ out: List[Entity]
         identified, retrieved, updated, and deleted via this ID until it has
         been deleted.
 
-        If the insertion fails, a LinkAheadException will be raised. The server will have returned at
-        least one error-message describing the reason why it failed in that case (call
+        If the insertion fails, a LinkAheadException will be raised. The server will have returned
+        at least one error-message describing the reason why it failed in that case (call
         <this_entity>.get_all_messages() in order to get these error-messages).
 
-        Some insertions might cause warning-messages on the server-side, but the entities are inserted
-        anyway. Set the flag 'strict' to True in order to force the server to take all warnings as errors.
-        This prevents the server from inserting this entity if any warning occurs.
+        Some insertions might cause warning-messages on the server-side, but the entities are
+        inserted anyway. Set the flag 'strict' to True in order to force the server to take all
+        warnings as errors.  This prevents the server from inserting this entity if any warning
+        occurs.
 
         Parameters
         ----------
@@ -1557,21 +1608,22 @@ 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,
-        and value of an entity may be changed. Additionally, properties, parents and messages may be added.
+        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,
+        and value of an entity may be changed. Additionally, properties, parents and messages may be
+        added.
 
-        However, the first one is more powerful: It is possible to delete and change properties, parents
-        and attributes, which is not possible via the second one for internal reasons (which are reasons
-        of definiteness).
+        However, the first one is more powerful: It is possible to delete and change properties,
+        parents and attributes, which is not possible via the second one for internal reasons (which
+        are reasons of definiteness).
 
         If the update fails, a LinkAheadException will be raised. The server will have returned at
         least one error message describing the reason why it failed in that case (call
         <this_entity>.get_all_messages() in order to get these error-messages).
 
         Some updates might cause warning messages on the server-side, but the updates are performed
-        anyway. Set flag 'strict' to True in order to force the server to take all warnings as errors.
-        This prevents the server from updating this entity if any warnings occur.
+        anyway. Set flag 'strict' to True in order to force the server to take all warnings as
+        errors.  This prevents the server from updating this entity if any warnings occur.
 
         @param strict=False: Flag for strict mode.
         """
@@ -1635,6 +1687,9 @@ def _parse_value(datatype, value):
         if isinstance(value, str):
             return value
 
+    if datatype == DATETIME and (isinstance(value, date) or isinstance(value, datetime)):
+        return value
+
     # deal with collections
     if isinstance(datatype, str):
         matcher = re.compile(r"^(?P<col>[^<]+)<(?P<dt>[^>]+)>$")
@@ -1746,8 +1801,8 @@ class QueryTemplate():
         raise_exception_on_error: bool = True,
         unique: bool = True,
         sync: bool = True,
-        flags: Optional[Dict[str, Optional[str]]] = None,
-    ):
+        flags: Optional[QueryDict] = None,
+    ) -> Container:
 
         return Container().append(self).retrieve(
             raise_exception_on_error=raise_exception_on_error,
@@ -1761,8 +1816,8 @@ class QueryTemplate():
         raise_exception_on_error: bool = True,
         unique: bool = True,
         sync: bool = True,
-        flags: Optional[Dict[str, Optional[str]]] = None,
-    ):
+        flags: Optional[QueryDict] = None,
+    ) -> Container:
 
         return Container().append(self).insert(
             strict=strict,
@@ -1777,8 +1832,8 @@ class QueryTemplate():
         raise_exception_on_error: bool = True,
         unique: bool = True,
         sync: bool = True,
-        flags: Optional[Dict[str, Optional[str]]] = None,
-    ):
+        flags: Optional[QueryDict] = None,
+    ) -> Container:
 
         return Container().append(self).update(
             strict=strict,
@@ -1794,7 +1849,7 @@ class QueryTemplate():
     def __repr__(self):
         return xml2str(self.to_xml())
 
-    def to_xml(self, xml: Optional[etree._Element] = None):
+    def to_xml(self, xml: Optional[etree._Element] = None) -> etree._Element:
         if xml is None:
             xml = etree.Element("QueryTemplate")
 
@@ -1932,7 +1987,8 @@ class Property(Entity):
 
     """LinkAhead's Property object."""
 
-    def add_property(self, property=None, value=None, id=None, name=None, description=None, datatype=None,
+    def add_property(self, property=None, value=None, id=None, name=None, description=None,
+                     datatype=None,
                      unit=None, importance=FIX, inheritance=FIX):  # @ReservedAssignment
         """See ``Entity.add_property``."""
 
@@ -1945,8 +2001,6 @@ class Property(Entity):
 
         Parameters
         ----------
-       Parameters
-        ----------
         parent : Entity or int or str or None
             The parent entity, either specified by the Entity object
             itself, or its id or its name. Default is None.
@@ -1968,7 +2022,8 @@ class Property(Entity):
 
         """
 
-        return super(Property, self).add_parent(parent=parent, id=id, name=name, inheritance=inheritance)
+        return super(Property, self).add_parent(parent=parent, id=id, name=name,
+                                                inheritance=inheritance)
 
     def __init__(
         self,
@@ -1998,13 +2053,14 @@ class Property(Entity):
             local_serialization=local_serialization,
         )
 
-    def is_reference(self, server_retrieval=False):
+    def is_reference(self, server_retrieval: bool = False) -> Optional[bool]:
         """Returns whether this Property is a reference
 
         Parameters
         ----------
         server_retrieval : bool, optional
-            If True and the datatype is not set, the Property is retrieved from the server, by default False
+            If True and the datatype is not set, the Property is retrieved from the server, by
+            default False
 
         Returns
         -------
@@ -2054,7 +2110,7 @@ class Message(object):
         self.code = int(code) if code is not None else None
         self.body = body
 
-    def to_xml(self, xml: Optional[etree._Element] = None):
+    def to_xml(self, xml: Optional[etree._Element] = None) -> etree._Element:
         if xml is None:
             xml = etree.Element(str(self.type))
 
@@ -2074,21 +2130,23 @@ class Message(object):
 
     def __eq__(self, obj):
         if isinstance(obj, Message):
-            return self.type == obj.type and self.code == obj.code and self.description == obj.description
+            return (self.type == obj.type and self.code == obj.code
+                    and self.description == obj.description)
 
         return False
 
-    def get_code(self):
+    def get_code(self) -> Optional[int]:
         warn(("get_code is deprecated and will be removed in future. "
               "Use self.code instead."), DeprecationWarning)
-        return int(self.code)
+        return int(self.code) if self.code is not None else None
 
 
 class RecordType(Entity):
 
     """This class represents LinkAhead's RecordType entities."""
 
-    def add_property(self, property=None, value=None, id=None, name=None, description=None, datatype=None,
+    def add_property(self, property=None, value=None, id=None, name=None, description=None,
+                     datatype=None,
                      unit=None, importance=RECOMMENDED, inheritance=FIX):  # @ReservedAssignment
         """See ``Entity.add_property``."""
 
@@ -2166,7 +2224,8 @@ class Record(Entity):
 
     """This class represents LinkAhead's Record entities."""
 
-    def add_property(self, property=None, value=None, id=None, name=None, description=None, datatype=None,
+    def add_property(self, property=None, value=None, id=None, name=None, description=None,
+                     datatype=None,
                      unit=None, importance=FIX, inheritance=FIX):  # @ReservedAssignment
         """See ``Entity.add_property``."""
 
@@ -2265,7 +2324,7 @@ class File(Record):
         xml: Optional[etree._Element] = None,
         add_properties: INHERITANCE = "ALL",
         local_serialization: bool = False,
-    ):
+    ) -> etree._Element:
         """Convert this file to an xml element.
 
         @return: xml element
@@ -2277,7 +2336,7 @@ class File(Record):
         return Entity.to_xml(self, xml=xml, add_properties=add_properties,
                              local_serialization=local_serialization)
 
-    def download(self, target: Optional[str] = None):
+    def download(self, target: Optional[str] = None) -> str:
         """Download this file-entity's actual file from the file server. It
         will be stored to the target or will be hold as a temporary file.
 
@@ -2287,7 +2346,8 @@ class File(Record):
         self.clear_server_messages()
 
         if target:
-            file_: Union[BufferedWriter, _TemporaryFileWrapper] = open(target, "wb")
+            file_: Union[BufferedWriter,
+                         _TemporaryFileWrapper] = open(target, "wb")
         else:
             file_ = NamedTemporaryFile(mode='wb', delete=False)
         checksum = File.download_from_path(file_, self.path)
@@ -2341,7 +2401,7 @@ class File(Record):
                 return File._get_checksum_single_file(files)
 
     @staticmethod
-    def _get_checksum_single_file(single_file):
+    def _get_checksum_single_file(single_file:  Union[str, bytes, PathLike[str], PathLike[bytes]]):
         _file = open(single_file, 'rb')
         data = _file.read(1000)
         checksum = sha512()
@@ -2370,10 +2430,10 @@ class PropertyList(list):
 
     def __init__(self):
         super().__init__()
-        self._importance: Dict[Entity, IMPORTANCE] = dict()
-        self._inheritance: Dict[Entity, INHERITANCE] = dict()
-        self._element_by_name: Dict[str, Entity] = dict()
-        self._element_by_id: Dict[str, Entity] = dict()
+        self._importance: dict[Entity, IMPORTANCE] = dict()
+        self._inheritance: dict[Entity, INHERITANCE] = dict()
+        self._element_by_name: dict[str, Entity] = dict()
+        self._element_by_id: dict[str, Entity] = dict()
 
     def get_importance(
         self, property: Union[Property, Entity, str, None]
@@ -2384,7 +2444,8 @@ class PropertyList(list):
 
             return self._importance.get(property)
 
-    def set_importance(self, property: Optional[Property], importance: IMPORTANCE):  # @ReservedAssignment
+    # @ReservedAssignment
+    def set_importance(self, property: Optional[Property], importance: IMPORTANCE):
         if property is not None:
             self._importance[property] = importance
 
@@ -2405,7 +2466,7 @@ class PropertyList(list):
 
     def append(
         self,
-        property: Union[List[Entity], Entity, Property],
+        property: Union[list[Entity], Entity, Property],
         importance: Optional[IMPORTANCE] = None,
         inheritance: Optional[INHERITANCE] = None,
     ):  # @ReservedAssignment
@@ -2422,7 +2483,7 @@ class PropertyList(list):
             if inheritance is not None:
                 self._inheritance[property] = inheritance
             else:
-                self._inheritance[property] = "ALL"
+                self._inheritance[property] = "FIX"
 
             if property.id is not None:
                 self._element_by_id[str(property.id)] = property
@@ -2436,6 +2497,7 @@ class PropertyList(list):
         return self
 
     def to_xml(self, add_to_element: etree._Element, add_properties: INHERITANCE):
+        p: Property
         for p in self:
             importance = self._importance.get(p)
 
@@ -2578,7 +2640,7 @@ class ParentList(list):
 
         return self
 
-    def to_xml(self, add_to_element):
+    def to_xml(self, add_to_element: etree._Element):
         for p in self:
             pelem = etree.Element("Parent")
 
@@ -2696,7 +2758,8 @@ class Messages(list):
     <<< msgs = Messages()
 
     <<< # create Message
-    <<< msg = Message(type="HelloWorld", code=1, description="Greeting the world", body="Hello, world!")
+    <<< msg = Message(type="HelloWorld", code=1, description="Greeting the world",
+    ...               body="Hello, world!")
 
     <<< # append it to the Messages
     <<< msgs.append(msg)
@@ -2770,11 +2833,12 @@ class Messages(list):
         if isinstance(value, Message):
             body = value.body
             description = value.description
-            m = Message
+            m = Message()
         else:
             body = value
             description = None
-            m = Message(type=type, code=code, description=description, body=body)
+            m = Message(type=type, code=code,
+                        description=description, body=body)
         if isinstance(key, int):
             super().__setitem__(key, m)
         else:
@@ -2782,7 +2846,8 @@ class Messages(list):
 
     def __getitem__(self, key):
         if not isinstance(key, int):
-            warn("__getitem__ only supports integer keys in future.", DeprecationWarning)
+            warn("__getitem__ only supports integer keys in future.",
+                 DeprecationWarning)
         if isinstance(key, tuple):
             if len(key) == 2:
                 type = key[0]  # @ReservedAssignment
@@ -2808,7 +2873,8 @@ class Messages(list):
 
     def __delitem__(self, key):
         if isinstance(key, tuple):
-            warn("__delitem__ only supports integer keys in future.", DeprecationWarning)
+            warn("__delitem__ only supports integer keys in future.",
+                 DeprecationWarning)
             if self.get(key[0], key[1]) is not None:
                 self.remove(self.get(key[0], key[1]))
         else:
@@ -2861,7 +2927,7 @@ class Messages(list):
 
         return default
 
-    def to_xml(self, add_to_element):
+    def to_xml(self, add_to_element: etree._Element):
         for m in self:
             melem = m.to_xml()
             add_to_element.append(melem)
@@ -3013,7 +3079,7 @@ class Container(list):
     def __hash__(self):
         return object.__hash__(self)
 
-    def remove(self, entity):
+    def remove(self, entity: Entity):
         """Remove the first entity from this container which is equal to the
         given entity. Raise a ValueError if there is no such entity.
 
@@ -3050,7 +3116,8 @@ class Container(list):
                     return e
         raise KeyError("No entity with such cuid (" + str(cuid) + ")!")
 
-    def get_entity_by_id(self, id):  # @ReservedAssignment
+    # @ReservedAssignment
+    def get_entity_by_id(self, id: Union[int, str]) -> Entity:
         """Get the first entity which has the given id. Note: If several
         entities are in this list which have the same id, this method will only
         return the first and ignore the others.
@@ -3083,7 +3150,7 @@ class Container(list):
 
         return error_list
 
-    def get_entity_by_name(self, name: str, case_sensitive: bool = True):
+    def get_entity_by_name(self, name: str, case_sensitive: bool = True) -> Entity:
         """Get the first entity which has the given name. Note: If several
         entities are in this list which have the same name, this method will
         only return the first and ignore the others.
@@ -3162,12 +3229,20 @@ class Container(list):
 
         return self
 
-    def to_xml(self, add_to_element=None, local_serialization=False):
+    def to_xml(self, add_to_element: Optional[etree._Element] = None,
+               local_serialization: bool = False) -> etree._Element:
         """Get an xml tree representing this Container or append all entities
         to the given xml element.
 
-        @param add_to_element=None: optional element to which all entities of this container is to be appended.
-        @return xml element
+        Parameters
+        ----------
+        add_to_element : etree._Element, optional
+            optional element to which all entities of this container is to
+            be appended. Default is None
+
+        Returns
+        -------
+        xml_element : etree._Element
         """
         tmpid = 0
 
@@ -3316,7 +3391,7 @@ class Container(list):
                 if isinstance(e, Message):
                     c.messages.append(e)
                 elif isinstance(e, Query):
-                    c.query = e
+                    c.query = e  # type: ignore
 
                     if e.messages is not None:
                         c.messages.extend(e.messages)
@@ -3339,7 +3414,8 @@ class Container(list):
             return c
         else:
             raise LinkAheadException(
-                "The server's response didn't contain the expected elements. The configuration of this client might be invalid (especially the url).")
+                "The server's response didn't contain the expected elements. The configuration of"
+                " this client might be invalid (especially the url).")
 
     def _sync(
         self,
@@ -3405,7 +3481,8 @@ class Container(list):
 
         # which is to be synced with which:
         # sync_dict[local_entity]=sync_remote_enities
-        sync_dict: Dict[Union[Container, Entity], Optional[List[Entity]]] = dict()
+        sync_dict: dict[Union[Container, Entity],
+                        Optional[list[Entity]]] = dict()
 
         # list of remote entities which already have a local equivalent
         used_remote_entities = []
@@ -3434,7 +3511,8 @@ class Container(list):
                     msg = "Request was not unique. CUID " + \
                         str(local_entity._cuid) + " was found " + \
                         str(len(sync_remote_entities)) + " times."
-                    local_entity.add_message(Message(description=msg, type="Error"))
+                    local_entity.add_message(
+                        Message(description=msg, type="Error"))
 
                     if raise_exception_on_error:
                         raise MismatchingEntitiesError(msg)
@@ -3459,7 +3537,8 @@ class Container(list):
                     msg = "Request was not unique. ID " + \
                         str(local_entity.id) + " was found " + \
                         str(len(sync_remote_entities)) + " times."
-                    local_entity.add_message(Message(description=msg, type="Error"))
+                    local_entity.add_message(
+                        Message(description=msg, type="Error"))
 
                     if raise_exception_on_error:
                         raise MismatchingEntitiesError(msg)
@@ -3489,7 +3568,8 @@ class Container(list):
                     msg = "Request was not unique. Path " + \
                         str(local_entity.path) + " was found " + \
                         str(len(sync_remote_entities)) + " times."
-                    local_entity.add_message(Message(description=msg, type="Error"))
+                    local_entity.add_message(
+                        Message(description=msg, type="Error"))
 
                     if raise_exception_on_error:
                         raise MismatchingEntitiesError(msg)
@@ -3519,7 +3599,8 @@ class Container(list):
                     msg = "Request was not unique. Name " + \
                         str(local_entity.name) + " was found " + \
                         str(len(sync_remote_entities)) + " times."
-                    local_entity.add_message(Message(description=msg, type="Error"))
+                    local_entity.add_message(
+                        Message(description=msg, type="Error"))
 
                     if raise_exception_on_error:
                         raise MismatchingEntitiesError(msg)
@@ -3532,13 +3613,15 @@ class Container(list):
                 sync_remote_entities.append(remote_entity)
 
         if len(sync_remote_entities) > 0:
-            sync_dict[self] = sync_remote_entities  # FIXME: How is this supposed to work?
+            # FIXME: How is this supposed to work?
+            sync_dict[self] = sync_remote_entities
 
         if unique and len(sync_remote_entities) != 0:
             msg = "Request was not unique. There are " + \
                 str(len(sync_remote_entities)) + \
                 " entities which could not be matched to one of the requested ones."
-            remote_container.add_message(Message(description=msg, type="Error"))
+            remote_container.add_message(
+                Message(description=msg, type="Error"))
 
             if raise_exception_on_error:
                 raise MismatchingEntitiesError(msg)
@@ -3568,14 +3651,16 @@ class Container(list):
         dependent_references = set()
         dependencies = set()
 
+        container_item: Entity
         for container_item in container:
             item_id.add(container_item.id)
 
             for parents in container_item.get_parents():
                 is_parent.add(parents.id)
 
+            prop: Property
             for prop in container_item.get_properties():
-                prop_dt = prop.datatype
+                prop_dt: Union[DATATYPE, str, None] = prop.datatype
                 if prop_dt is not None and is_reference(prop_dt):
                     # add only if it is a reference, not a simple property
                     # Step 1: look for prop.value
@@ -3598,7 +3683,8 @@ class Container(list):
                         if is_list_datatype(prop_dt):
                             ref_name = get_list_datatype(prop_dt)
                             try:
-                                is_being_referenced.add(container.get_entity_by_name(ref_name).id)
+                                is_being_referenced.add(
+                                    container.get_entity_by_name(ref_name).id)  # type: ignore
                             except KeyError:
                                 pass
                         elif isinstance(prop_dt, str):
@@ -3631,7 +3717,8 @@ class Container(list):
 
         return dependencies
 
-    def delete(self, raise_exception_on_error=True, flags=None, chunk_size=100):
+    def delete(self, raise_exception_on_error: bool = True,
+               flags: Optional[QueryDict] = None, chunk_size: int = 100):
         """Delete all entities in this container.
 
         Entities are identified via their id if present and via their
@@ -3643,7 +3730,8 @@ class Container(list):
 
         """
         item_count = len(self)
-        # Split Container in 'chunk_size'-sized containers (if necessary) to avoid error 414 Request-URI Too Long
+        # Split Container in 'chunk_size'-sized containers (if necessary) to avoid error 414
+        # Request-URI Too Long
 
         if item_count > chunk_size:
             dependencies = Container._find_dependencies_in_container(self)
@@ -3654,7 +3742,8 @@ class Container(list):
             if len(dependencies) == item_count:
                 if raise_exception_on_error:
                     te = TransactionError(
-                        msg="The container is too large and with too many dependencies within to be deleted.",
+                        msg=("The container is too large and with too many dependencies within to"
+                             " be deleted."),
                         container=self)
                     raise te
 
@@ -3745,7 +3834,7 @@ class Container(list):
         unique: bool = True,
         raise_exception_on_error: bool = True,
         sync: bool = True,
-        flags=None,
+        flags: Optional[QueryDict] = None,
     ):
         """Retrieve all entities in this container identified via their id if
         present and via their name otherwise. Any locally already existing
@@ -3755,9 +3844,9 @@ class Container(list):
 
         If any entity has no id and no name a LinkAheadException will be raised.
 
-        Note: If only a name is given this could lead to ambiguities. All entities with the name in question
-        will be returned. Therefore, the container could contain more elements after the retrieval than
-        before.
+        Note: If only a name is given this could lead to ambiguities. All entities with the name in
+        question will be returned. Therefore, the container could contain more elements after the
+        retrieval than before.
         """
 
         if isinstance(query, list):
@@ -3821,7 +3910,7 @@ class Container(list):
 
         return (entities[0:hl], entities[hl:len(entities)])
 
-    def _retrieve(self, entities, flags: Dict[str, Optional[str]]):
+    def _retrieve(self, entities, flags: Optional[QueryDict]):
         c = get_connection()
         try:
             _log_request("GET: " + _ENTITY_URI_SEGMENT + str(entities) +
@@ -3854,7 +3943,8 @@ class Container(list):
         return self
 
     @staticmethod
-    def _dir_to_http_parts(root, d, upload):  # @ReservedAssignment
+    # @ReservedAssignment
+    def _dir_to_http_parts(root: str, d: Optional[str], upload: str):
         ret = []
         x = (root + '/' + d if d is not None else root)
 
@@ -3881,7 +3971,7 @@ class Container(list):
         raise_exception_on_error: bool = True,
         unique: bool = True,
         sync: bool = True,
-        flags: Optional[Dict[str, Any]] = None,
+        flags: Optional[dict[str, Any]] = None,
     ):
         """Update these entites."""
 
@@ -3893,7 +3983,7 @@ class Container(list):
 
         self.clear_server_messages()
         insert_xml = etree.Element("Update")
-        http_parts: List[MultipartParam] = []
+        http_parts: list[MultipartParam] = []
 
         if flags is None:
             flags = {}
@@ -3964,7 +4054,7 @@ class Container(list):
 
     @staticmethod
     def _process_file_if_present_and_add_to_http_parts(
-        http_parts: List[MultipartParam], entity: Union[File, Entity]
+        http_parts: list[MultipartParam], entity: Union[File, Entity]
     ):
         if isinstance(entity, File) and hasattr(
                 entity, 'file') and entity.file is not None:
@@ -4014,7 +4104,7 @@ class Container(list):
         raise_exception_on_error: bool = True,
         unique: bool = True,
         sync: bool = True,
-        flags: Optional[Dict[str, Optional[str]]] = None,
+        flags: Optional[QueryDict] = None,
     ):
         """Insert this file entity into LinkAhead. A successful insertion will
         generate a new persistent ID for this entity. This entity can be
@@ -4030,19 +4120,27 @@ class Container(list):
         warnings as errors.  This prevents the server from inserting this entity if any warning
         occurs.
 
-        @param strict=False: Flag for strict mode.
-        @param sync=True: synchronize this container with the response from the server. Otherwise,
-                          this method returns a new container with the inserted entities and leaves
-                          this container untouched.
-        @param unique=True: Flag for unique mode. If set to True, the server will check if the name
-                            of the entity is unique. If not, the server will return an error.
-        @param flags=None: Additional flags for the server.
+        Parameters
+        ----------
+        strict : bool, optional
+            Flag for strict mode. Default is False.
+        sync : bool, optional
+            synchronize this container with the response from the
+            server. Otherwise, this method returns a new container with the
+            inserted entities and leaves this container untouched. Default is
+            True.
+        unique : bool, optional
+            Flag for unique mode. If set to True, the server will check if the
+            name of the entity is unique. If not, the server will return an
+            error. Default is True.
+        flags : dict, optional
+            Additional flags for the server. Default is None.
 
         """
 
         self.clear_server_messages()
         insert_xml = etree.Element("Insert")
-        http_parts: List[MultipartParam] = []
+        http_parts: list[MultipartParam] = []
 
         if flags is None:
             flags = {}
@@ -4096,7 +4194,8 @@ class Container(list):
 
         if len(self) > 0 and len(insert_xml) < 1:
             te = TransactionError(
-                msg="There are no entities to be inserted. This container contains existent entities only.",
+                msg=("There are no entities to be inserted. This container contains existent"
+                     " entities only."),
                 container=self)
             raise te
         _log_request("POST: " + _ENTITY_URI_SEGMENT +
@@ -4203,8 +4302,8 @@ class Container(list):
         return self
 
     def get_property_values(
-        self, *selectors: Union[str, Tuple[str]]
-    ) -> List[Tuple[str]]:
+        self, *selectors: Union[str, tuple[str]]
+    ) -> list[tuple[str]]:
         """ Return a list of tuples with values of the given selectors.
 
         I.e. a tabular representation of the container's content.
@@ -4260,7 +4359,8 @@ def sync_global_acl():
                         ACL.global_acl = ACL(xml=pelem)
     else:
         raise LinkAheadException(
-            "The server's response didn't contain the expected elements. The configuration of this client might be invalid (especially the url).")
+            "The server's response didn't contain the expected elements. The configuration of this"
+            " client might be invalid (especially the url).")
 
 
 def get_known_permissions():
@@ -4296,11 +4396,13 @@ class ACI():
         return hash(self.__repr__())
 
     def __eq__(self, other):
-        return isinstance(other, ACI) and (self.role is None and self.username == other.username and self.realm ==
-                                           other.realm) or self.role == other.role and self.permission == other.permission
+        return (isinstance(other, ACI) and
+                (self.role is None and self.username == other.username
+                 and self.realm == other.realm)
+                or self.role == other.role and self.permission == other.permission)
 
     def __repr__(self):
-        return str(self.realm) + ":" + str(self.username) + ":" + str(self.role) + ":" + str(self.permission)
+        return ":".join([str(self.realm), str(self.username), str(self.role), str(self.permission)])
 
     def add_to_element(self, e: etree._Element):
         if self.role is not None:
@@ -4379,7 +4481,7 @@ class ACL():
                                   permission=permission, priority=priority,
                                   revoke_grant=False)
 
-    def combine(self, other: ACL):
+    def combine(self, other: ACL) -> ACL:
         """ Combine and return new instance."""
         result = ACL()
         result._grants.update(other._grants)
@@ -4394,7 +4496,11 @@ class ACL():
         return result
 
     def __eq__(self, other):
-        return isinstance(other, ACL) and other._grants == self._grants and self._denials == other._denials and self._priority_grants == other._priority_grants and self._priority_denials == other._priority_denials
+        return (isinstance(other, ACL)
+                and other._grants == self._grants
+                and self._denials == other._denials
+                and self._priority_grants == other._priority_grants
+                and self._priority_denials == other._priority_denials)
 
     def is_empty(self):
         return len(self._grants) + len(self._priority_grants) + \
@@ -4721,7 +4827,7 @@ class Query():
         return self.flags.get(key)
 
     def __init__(self, q: Union[str, etree._Element]):
-        self.flags: Dict[str, Optional[str]] = dict()
+        self.flags: QueryDict = dict()
         self.messages = Messages()
         self.cached: Optional[bool] = None
         self.etag = None
@@ -4747,7 +4853,7 @@ class Query():
         else:
             self.q = q
 
-    def _query_request(self, query_dict: Dict[str, Optional[str]]):
+    def _query_request(self, query_dict: QueryDict):
         """Used internally to execute the query request..."""
         _log_request("GET Entity?" + str(query_dict), None)
         connection = get_connection()
@@ -4760,7 +4866,7 @@ class Query():
     def _paging_generator(
         self,
         first_page: Container,
-        query_dict: Dict[str, Optional[str]],
+        query_dict: QueryDict,
         page_length: int,
     ):
         """Used internally to create a generator of pages instead instead of a
@@ -4774,7 +4880,8 @@ class Query():
             next_page = self._query_request(query_dict)
             etag = next_page.query.etag
             if etag is not None and etag != self.etag:
-                raise PagingConsistencyError("The database state changed while retrieving the pages")
+                raise PagingConsistencyError(
+                    "The database state changed while retrieving the pages")
             yield next_page
             index += page_length
 
@@ -4876,9 +4983,9 @@ def execute_query(
     unique: bool = False,
     raise_exception_on_error: bool = True,
     cache: bool = True,
-    flags: Optional[Dict[str, Optional[str]]] = None,
+    flags: Optional[QueryDict] = None,
     page_length: Optional[int] = None,
-) -> Union[Container, int]:
+) -> Union[Container, Entity, int]:
     """Execute a query (via a server-requests) and return the results.
 
     Parameters
@@ -4905,8 +5012,8 @@ def execute_query(
         Otherwise, paging is disabled, as well as for count queries and
         when unique is True. Defaults to None.
 
-    Raises:
-    -------
+    Raises
+    ------
     PagingConsistencyError
         If the database state changed between paged requests.
 
@@ -4917,7 +5024,7 @@ def execute_query(
 
     Returns
     -------
-    results : Container or integer
+    results : Container or Entity or integer
         Returns an integer when it was a `COUNT` query. Otherwise, returns a
         Container with the resulting entities.
     """
@@ -4933,8 +5040,7 @@ def execute_query(
 
 class DropOffBox(list):
     def __init__(self, *args, **kwargs):
-        warn(DeprecationWarning(
-                "The DropOffBox is deprecated and will be removed in future."))
+        warn(DeprecationWarning("The DropOffBox is deprecated and will be removed in future."))
         super().__init__(*args, **kwargs)
 
     path = None
@@ -4969,6 +5075,17 @@ class DropOffBox(list):
 
 
 class UserInfo():
+    """User information from a server response.
+
+    Attributes
+    ----------
+    name : str
+        Username
+    realm : str
+        Realm in which this user lives, e.g., CaosDB or LDAP.
+    roles : list[str]
+        List of roles assigned to this user.
+    """
 
     def __init__(self, xml: etree._Element):
         self.roles = [role.text for role in xml.findall("Roles/Role")]
@@ -4977,6 +5094,21 @@ class UserInfo():
 
 
 class Info():
+    """Info about the LinkAhead instance that you are connected to. It has a
+    simple string representation in the form of "Connected to a LinkAhead with N
+    Records".
+
+    Attributes
+    ----------
+    messages : Messages
+        Collection of messages that the server's ``Info`` response contained.
+    user_info : UserInfo
+        Information about the user that is connected to the server, such as
+        name, realm or roles.
+    time_zone : TimeZone
+        The timezone information returned by the server.
+
+    """
 
     def __init__(self):
         self.messages = Messages()
@@ -4985,6 +5117,7 @@ class Info():
         self.sync()
 
     def sync(self):
+        """Retrieve server information from the server's ``Info`` response."""
         c = get_connection()
         try:
             http_response = c.retrieve(["Info"])
@@ -5044,7 +5177,7 @@ class Permission():
 
 class Permissions():
 
-    known_permissions: Optional[List[Permissions]] = None
+    known_permissions: Optional[Permissions] = None
 
     def __init__(self, xml: etree._Element):
         self.parse_xml(xml)
@@ -5159,7 +5292,8 @@ def _parse_single_xml_element(elem: etree._Element):
         )
 
 
-def _evaluate_and_add_error(parent_error: TransactionError, ent: Union[Entity, QueryTemplate, Container]):
+def _evaluate_and_add_error(parent_error: TransactionError,
+                            ent: Union[Entity, QueryTemplate, Container]):
     """Evaluate the error message(s) attached to entity and add a
     corresponding exception to parent_error.
 
@@ -5289,7 +5423,7 @@ def raise_errors(arg0: Union[Entity, QueryTemplate, Container]):
         raise transaction_error
 
 
-def delete(ids: Union[List[int], range], raise_exception_on_error: bool = True):
+def delete(ids: Union[list[int], range], raise_exception_on_error: bool = True):
     c = Container()
 
     if isinstance(ids, list) or isinstance(ids, range):
diff --git a/src/linkahead/common/state.py b/src/linkahead/common/state.py
index 82f314e80191163f14a5c4babdd749f977f2901b..e352f82d9820620d1692cb6337eb218210e799e6 100644
--- a/src/linkahead/common/state.py
+++ b/src/linkahead/common/state.py
@@ -19,11 +19,19 @@
 #
 # ** end header
 
+from __future__ import annotations  # Can be removed with 3.10.
 import copy
 from lxml import etree
 
+from typing import TYPE_CHECKING
+import sys
 
-def _translate_to_state_acis(acis):
+if TYPE_CHECKING:
+    from typing import Optional
+    from linkahead.common.models import ACL, ACI
+
+
+def _translate_to_state_acis(acis: set[ACI]) -> set[ACI]:
     result = set()
     for aci in acis:
         aci = copy.copy(aci)
@@ -50,7 +58,13 @@ class Transition:
         A state name
     """
 
-    def __init__(self, name, from_state, to_state, description=None):
+    def __init__(
+        self,
+        name: Optional[str],
+        from_state: Optional[str],
+        to_state: Optional[str],
+        description: Optional[str] = None,
+    ):
         self._name = name
         self._from_state = from_state
         self._to_state = to_state
@@ -76,25 +90,29 @@ class Transition:
         return f'Transition(name="{self.name}", from_state="{self.from_state}", to_state="{self.to_state}", description="{self.description}")'
 
     def __eq__(self, other):
-        return (isinstance(other, Transition)
-                and other.name == self.name
-                and other.to_state == self.to_state
-                and other.from_state == self.from_state)
+        return (
+            isinstance(other, Transition)
+            and other.name == self.name
+            and other.to_state == self.to_state
+            and other.from_state == self.from_state
+        )
 
     def __hash__(self):
         return 23472 + hash(self.name) + hash(self.from_state) + hash(self.to_state)
 
     @staticmethod
-    def from_xml(xml):
-        to_state = [to.get("name") for to in xml
-                    if to.tag.lower() == "tostate"]
-        from_state = [from_.get("name") for from_ in xml
-                      if from_.tag.lower() == "fromstate"]
-        result = Transition(name=xml.get("name"),
-                            description=xml.get("description"),
-                            from_state=from_state[0] if from_state else None,
-                            to_state=to_state[0] if to_state else None)
-        return result
+    def from_xml(xml: etree._Element) -> "Transition":
+        to_state = [to.get("name")
+                    for to in xml if to.tag.lower() == "tostate"]
+        from_state = [
+            from_.get("name") for from_ in xml if from_.tag.lower() == "fromstate"
+        ]
+        return Transition(
+            name=xml.get("name"),
+            description=xml.get("description"),
+            from_state=from_state[0] if from_state else None,
+            to_state=to_state[0] if to_state else None,
+        )
 
 
 class State:
@@ -119,12 +137,12 @@ class State:
         All transitions which are available from this state (read-only)
     """
 
-    def __init__(self, model, name):
+    def __init__(self, model: Optional[str], name: Optional[str]):
         self.name = name
         self.model = model
-        self._id = None
-        self._description = None
-        self._transitions = None
+        self._id: Optional[str] = None
+        self._description: Optional[str] = None
+        self._transitions: Optional[set[Transition]] = None
 
     @property
     def id(self):
@@ -139,9 +157,11 @@ class State:
         return self._transitions
 
     def __eq__(self, other):
-        return (isinstance(other, State)
-                and self.name == other.name
-                and self.model == other.model)
+        return (
+            isinstance(other, State)
+            and self.name == other.name
+            and self.model == other.model
+        )
 
     def __hash__(self):
         return hash(self.name) + hash(self.model)
@@ -164,7 +184,7 @@ class State:
         return xml
 
     @staticmethod
-    def from_xml(xml):
+    def from_xml(xml: etree._Element):
         """Create a new State instance from an xml Element.
 
         Parameters
@@ -175,24 +195,26 @@ class State:
         -------
         state : State
         """
-        name = xml.get("name")
-        model = xml.get("model")
-        result = State(name=name, model=model)
+        result = State(name=xml.get("name"), model=xml.get("model"))
         result._id = xml.get("id")
         result._description = xml.get("description")
-        transitions = [Transition.from_xml(t) for t in xml if t.tag.lower() ==
-                       "transition"]
+        transitions = [
+            Transition.from_xml(t) for t in xml if t.tag.lower() == "transition"
+        ]
         if transitions:
             result._transitions = set(transitions)
 
         return result
 
     @staticmethod
-    def create_state_acl(acl):
+    def create_state_acl(acl: ACL):
         from .models import ACL
+
         state_acl = ACL()
         state_acl._grants = _translate_to_state_acis(acl._grants)
         state_acl._denials = _translate_to_state_acis(acl._denials)
-        state_acl._priority_grants = _translate_to_state_acis(acl._priority_grants)
-        state_acl._priority_denials = _translate_to_state_acis(acl._priority_denials)
+        state_acl._priority_grants = _translate_to_state_acis(
+            acl._priority_grants)
+        state_acl._priority_denials = _translate_to_state_acis(
+            acl._priority_denials)
         return state_acl
diff --git a/src/linkahead/common/timezone.py b/src/linkahead/common/timezone.py
index 8fc5e710d3cbf6f20cf81397573f972db3b22f12..9ccb433c49b07e73a90bdad3ae6caaf2017a9c1d 100644
--- a/src/linkahead/common/timezone.py
+++ b/src/linkahead/common/timezone.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+from typing import Optional
+
+
 class TimeZone():
     """
     TimeZone, e.g. CEST, Europe/Berlin, UTC+4.
@@ -7,13 +11,13 @@ class TimeZone():
     ----------
     zone_id : string
         ID of the time zone.
-    offset : int
-        Offset to UTC in seconds.
+    offset : str
+        Offset to UTC, e.g. "+1400"
     display_name : string
         A human-friendly name of the time zone:
     """
 
-    def __init__(self, zone_id, offset, display_name):
+    def __init__(self, zone_id: Optional[str], offset: Optional[str], display_name: str):
         self.zone_id = zone_id
         self.offset = offset
         self.display_name = display_name
diff --git a/src/linkahead/common/versioning.py b/src/linkahead/common/versioning.py
index facfbc488e413e090ea1a856501ccd96334f8354..2e292e6bb031725fbd6da618c4b888c05072c46b 100644
--- a/src/linkahead/common/versioning.py
+++ b/src/linkahead/common/versioning.py
@@ -26,10 +26,14 @@
 Currently this module defines nothing but a single class, `Version`.
 """
 
-from __future__ import absolute_import
+from __future__ import annotations
 from .utils import xml2str
 from lxml import etree
 
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+    from typing import Optional, List, Union
+
 
 class Version():
     """The version of an entity.
@@ -95,9 +99,11 @@ class Version():
     """
 
     # pylint: disable=redefined-builtin
-    def __init__(self, id=None, date=None, username=None, realm=None,
-                 predecessors=None, successors=None, is_head=False,
-                 is_complete_history=False):
+    def __init__(self, id: Optional[str] = None, date: Optional[str] = None,
+                 username: Optional[str] = None, realm: Optional[str] = None,
+                 predecessors: Optional[List[Version]] = None, successors: Optional[List[Version]] = None,
+                 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."""
         self.id = id
@@ -109,7 +115,7 @@ object."""
         self.is_head = str(is_head).lower() == "true"
         self.is_complete_history = str(is_complete_history).lower() == "true"
 
-    def get_history(self):
+    def get_history(self) -> List[Version]:
         """ Returns a flat list of Version instances representing the history
         of the entity.
 
@@ -126,7 +132,7 @@ object."""
         -------
         list of Version
         """
-        versions = []
+        versions: List[Version] = []
         for p in self.predecessors:
             # assuming that predecessors don't have any successors
             versions = p.get_history()
@@ -137,7 +143,7 @@ object."""
             versions.extend(s.get_history())
         return versions
 
-    def to_xml(self, tag="Version"):
+    def to_xml(self, tag: str = "Version") -> etree._Element:
         """Serialize this version to xml.
 
         The tag name is 'Version' per default. But since this method is called
@@ -184,7 +190,7 @@ object."""
         return xml2str(self.to_xml())
 
     @staticmethod
-    def from_xml(xml):
+    def from_xml(xml: etree._Element) -> Version:
         """Parse a version object from a 'Version' xml element.
 
         Parameters
diff --git a/src/linkahead/configuration.py b/src/linkahead/configuration.py
index 55a400459b1f3c9ea93d10ddf8c78765aa323eaa..f57289d7dcb6d7ab062024dc697dbda557670d7a 100644
--- a/src/linkahead/configuration.py
+++ b/src/linkahead/configuration.py
@@ -21,14 +21,14 @@
 #
 # ** end header
 #
-
+from __future__ import annotations
 import os
 import warnings
 
 import yaml
 
 try:
-    optional_jsonschema_validate = None
+    optional_jsonschema_validate: Optional[Callable] = None
     from jsonschema import validate as optional_jsonschema_validate
 except ImportError:
     pass
@@ -37,13 +37,17 @@ from configparser import ConfigParser
 from os import environ, getcwd
 from os.path import expanduser, isfile, join
 
+from typing import Union, Callable, Optional
+
+_pycaosdbconf = ConfigParser(allow_no_value=False)
+
 
 def _reset_config():
     global _pycaosdbconf
     _pycaosdbconf = ConfigParser(allow_no_value=False)
 
 
-def configure(inifile):
+def configure(inifile: str) -> list[str]:
     """read config from file.
 
     Return a list of files which have successfully been parsed.
@@ -61,15 +65,15 @@ def configure(inifile):
     return read_config
 
 
-def get_config():
+def get_config() -> ConfigParser:
     global _pycaosdbconf
     if ("_pycaosdbconf" not in globals() or _pycaosdbconf is None):
         _reset_config()
     return _pycaosdbconf
 
 
-def config_to_yaml(config):
-    valobj = {}
+def config_to_yaml(config: ConfigParser) -> dict[str, dict[str, Union[int, str, bool]]]:
+    valobj: dict[str, dict[str, Union[int, str, bool]]] = {}
     for s in config.sections():
         valobj[s] = {}
         for key, value in config[s].items():
@@ -84,7 +88,7 @@ def config_to_yaml(config):
     return valobj
 
 
-def validate_yaml_schema(valobj):
+def validate_yaml_schema(valobj: dict[str, dict[str, Union[int, str, bool]]]):
     if optional_jsonschema_validate:
         with open(os.path.join(os.path.dirname(__file__), "schema-pycaosdb-ini.yml")) as f:
             schema = yaml.load(f, Loader=yaml.SafeLoader)
@@ -95,10 +99,10 @@ def validate_yaml_schema(valobj):
         """)
 
 
-def _read_config_files():
+def _read_config_files() -> list[str]:
     """Read config files from different paths.
 
-    Read the config from either ``$PYCAOSDBINI`` or home directory (``~/.pylinkahead.ini``), and
+    Read the config from either ``$PYLINKAHEADINI`` or home directory (``~/.pylinkahead.ini``), and
     additionally adds config from a config file in the current working directory
     (``pylinkahead.ini``).
     If deprecated names are used (starting with 'pycaosdb'), those used in addition but the files
@@ -127,15 +131,18 @@ def _read_config_files():
         warnings.warn("\n\nYou have a config file with the old naming scheme (pycaosdb.ini). "
                       f"Please use the new version and rename\n"
                       f"    {ini_cwd_caosdb}\nto\n    {ini_cwd}", DeprecationWarning)
+    if "PYCAOSDBINI" in environ:
+        warnings.warn("\n\nYou have an environment variable PYCAOSDBINI. "
+                      "Please rename it to PYLINKAHEADINI.")
     # End: LinkAhead rename block ##################################################
 
-    if "PYCAOSDBINI" in environ:
-        if not isfile(expanduser(environ["PYCAOSDBINI"])):
+    if "PYLINKAHEADINI" in environ:
+        if not isfile(expanduser(environ["PYLINKAHEADINI"])):
             raise RuntimeError(
-                f"No configuration file found at\n{expanduser(environ['PYCAOSDBINI'])}"
-                "\nwhich was given via the environment variable PYCAOSDBINI"
+                f"No configuration file found at\n{expanduser(environ['PYLINKAHEADINI'])}"
+                "\nwhich was given via the environment variable PYLINKAHEADINI"
             )
-        return_var.extend(configure(expanduser(environ["PYCAOSDBINI"])))
+        return_var.extend(configure(expanduser(environ["PYLINKAHEADINI"])))
     else:
         if isfile(ini_user_caosdb):
             return_var.extend(configure(ini_user_caosdb))
diff --git a/src/linkahead/connection/authentication/external_credentials_provider.py b/src/linkahead/connection/authentication/external_credentials_provider.py
index 3d1b8afa17f58a87f09afba90c4bc7ae6dcba693..8e22c5c5796603dcc6acfbe3eb9345b8fb2e2f4e 100644
--- a/src/linkahead/connection/authentication/external_credentials_provider.py
+++ b/src/linkahead/connection/authentication/external_credentials_provider.py
@@ -23,13 +23,10 @@
 #
 """external_credentials_provider."""
 from __future__ import absolute_import, unicode_literals
-from abc import ABCMeta
+from abc import ABC
 import logging
 from .plain import PlainTextCredentialsProvider
 
-# meta class compatible with Python 2 *and* 3:
-ABC = ABCMeta(str('ABC'), (object, ), {str('__slots__'): ()})
-
 
 class ExternalCredentialsProvider(PlainTextCredentialsProvider, ABC):
     """ExternalCredentialsProvider.
diff --git a/src/linkahead/connection/authentication/input.py b/src/linkahead/connection/authentication/input.py
index 2799207354b3949063461229d7d465e8a83c83ae..14e8196e72a9e6266631511cf36d5e8c0c5c68c4 100644
--- a/src/linkahead/connection/authentication/input.py
+++ b/src/linkahead/connection/authentication/input.py
@@ -25,13 +25,13 @@
 
 A CredentialsProvider which reads the password from the input line.
 """
-from __future__ import absolute_import, unicode_literals, print_function
+from __future__ import annotations
 from .interface import CredentialsProvider, CredentialsAuthenticator
-
+from typing import Optional
 import getpass
 
 
-def get_authentication_provider():
+def get_authentication_provider() -> CredentialsAuthenticator:
     """get_authentication_provider.
 
     Return an authenticator which uses the input for username/password credentials.
@@ -61,8 +61,8 @@ class InputCredentialsProvider(CredentialsProvider):
 
     def __init__(self):
         super(InputCredentialsProvider, self).__init__()
-        self._password = None
-        self._username = None
+        self._password: Optional[str] = None
+        self._username: Optional[str] = None
 
     def configure(self, **config):
         """configure.
diff --git a/src/linkahead/connection/authentication/interface.py b/src/linkahead/connection/authentication/interface.py
index 6de43b81f441ab60401c1c01885eaa514790d3de..b48e27c08312bf1358d32a9a1203627a9d0007c2 100644
--- a/src/linkahead/connection/authentication/interface.py
+++ b/src/linkahead/connection/authentication/interface.py
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 #
-# ** header v3.0
 # This file is a part of the LinkAhead Project.
 #
 # Copyright (C) 2018 Research Group Biomedical Physics,
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2024 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Joscha Schmiedt <joscha@schmiedt.dev>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -18,23 +19,25 @@
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program. If not, see <https://www.gnu.org/licenses/>.
-#
-# ** end header
-#
+
 """This module provides the interfaces for authenticating requests to the
 LinkAhead server.
 
-Implementing modules muts provide a `get_authentication_provider()` method.
+Implementing modules must provide a `get_authentication_provider()` method.
 """
-from abc import ABCMeta, abstractmethod, abstractproperty
+
+from __future__ import annotations
+from abc import ABC, abstractmethod
 import logging
 from ..utils import urlencode
 from ..interface import CaosDBServerConnection
 from ..utils import parse_auth_token, auth_token_to_cookie
 from ...exceptions import LoginFailedError
+from typing import TYPE_CHECKING, Optional
+if TYPE_CHECKING:
+    from ..interface import CaosDBHTTPResponse
+    QueryDict = dict[str, Optional[str]]
 
-# meta class compatible with Python 2 *and* 3:
-ABC = ABCMeta('ABC', (object, ), {'__slots__': ()})
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -108,7 +111,7 @@ class AbstractAuthenticator(ABC):
         """
         pass
 
-    def on_response(self, response):
+    def on_response(self, response: CaosDBHTTPResponse):
         """on_response.
 
         A call-back with is to be called by the connection after each
@@ -125,7 +128,7 @@ class AbstractAuthenticator(ABC):
         self.auth_token = parse_auth_token(
             response.getheader("Set-Cookie"))
 
-    def on_request(self, method, path, headers, **kwargs):
+    def on_request(self, method: str, path: str, headers: QueryDict, **kwargs):
         # pylint: disable=unused-argument
         """on_request.
 
@@ -262,10 +265,12 @@ class CredentialsProvider(ABC):
         None
         """
 
-    @abstractproperty
+    @property
+    @abstractmethod
     def password(self):
         """password."""
 
-    @abstractproperty
+    @property
+    @abstractmethod
     def username(self):
         """username."""
diff --git a/src/linkahead/connection/authentication/keyring.py b/src/linkahead/connection/authentication/keyring.py
index 202520bbab7e940ccce6517e640eff5904039553..dad6ed9b4ad77175db6b75e30b70152878da487d 100644
--- a/src/linkahead/connection/authentication/keyring.py
+++ b/src/linkahead/connection/authentication/keyring.py
@@ -35,7 +35,7 @@ from .external_credentials_provider import ExternalCredentialsProvider
 from .interface import CredentialsAuthenticator
 
 
-def get_authentication_provider():
+def get_authentication_provider() -> CredentialsAuthenticator:
     """get_authentication_provider.
 
     Return an authenticator which uses plain text username/password credentials.
diff --git a/src/linkahead/connection/authentication/pass.py b/src/linkahead/connection/authentication/pass.py
index bec307401f945a6cd2e223195e0cce2396602061..81a901523fcad65680d34947cd9cc741e06a0352 100644
--- a/src/linkahead/connection/authentication/pass.py
+++ b/src/linkahead/connection/authentication/pass.py
@@ -33,7 +33,7 @@ from .interface import CredentialsAuthenticator
 from .external_credentials_provider import ExternalCredentialsProvider
 
 
-def get_authentication_provider():
+def get_authentication_provider() -> CredentialsAuthenticator:
     """get_authentication_provider.
 
     Return an authenticator which uses plain text username/password credentials.
diff --git a/src/linkahead/connection/connection.py b/src/linkahead/connection/connection.py
index 91b4a01da455d0f365e39b0b0f7359e07096e707..c95134fed3fd6b031b01b518c6362bf3b371c960 100644
--- a/src/linkahead/connection/connection.py
+++ b/src/linkahead/connection/connection.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
-#
-# ** header v3.0
 # This file is a part of the LinkAhead Project.
 #
 # Copyright (C) 2018 Research Group Biomedical Physics,
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
 # Copyright (c) 2019 Daniel Hornung
+# Copyright (C) 2024 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Joscha Schmiedt <joscha@schmiedt.dev>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -23,7 +23,7 @@
 # ** end header
 #
 """Connection to a LinkAhead server."""
-from __future__ import absolute_import, print_function, unicode_literals
+from __future__ import annotations
 
 import logging
 import ssl
@@ -32,8 +32,7 @@ import warnings
 from builtins import str  # pylint: disable=redefined-builtin
 from errno import EPIPE as BrokenPipe
 from socket import error as SocketError
-from urllib.parse import quote, urlparse
-from warnings import warn
+from urllib.parse import ParseResult, quote, urlparse
 
 from requests import Session as HTTPSession
 from requests.adapters import HTTPAdapter
@@ -52,22 +51,28 @@ try:
 except ModuleNotFoundError:
     version = "uninstalled"
 
-from pkg_resources import resource_filename
-
 from .encode import MultipartYielder, ReadableMultiparts
 from .interface import CaosDBHTTPResponse, CaosDBServerConnection
-from .utils import make_uri_path, parse_url, urlencode
+from .utils import make_uri_path, urlencode
+
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+    from typing import Optional, Any, Iterator, Union
+    from requests.models import Response
+    from ssl import _SSLMethod
+    from .authentication.interface import AbstractAuthenticator, CredentialsAuthenticator
+
 
 _LOGGER = logging.getLogger(__name__)
 
 
 class _WrappedHTTPResponse(CaosDBHTTPResponse):
 
-    def __init__(self, response):
-        self.response = response
-        self._generator = None
-        self._buffer = b''
-        self._stream_consumed = False
+    def __init__(self, response: Response):
+        self.response: Response = response
+        self._generator: Optional[Iterator[Any]] = None
+        self._buffer: Optional[bytes] = b''
+        self._stream_consumed: bool = False
 
     @property
     def reason(self):
@@ -77,7 +82,7 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse):
     def status(self):
         return self.response.status_code
 
-    def read(self, size=None):
+    def read(self, size: Optional[int] = None):
         if self._stream_consumed is True:
             raise RuntimeError("Stream is consumed")
 
@@ -91,8 +96,13 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse):
             self._stream_consumed = True
             return self.response.content
 
+        if size is None or size == 0:
+            raise RuntimeError(
+                "size parameter should not be None if the stream is not consumed yet")
+
         if len(self._buffer) >= size:
             # still enough bytes in the buffer
+            # FIXME: `chunk`` is used before definition
             result = chunk[:size]
             self._buffer = chunk[size:]
             return result
@@ -117,7 +127,7 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse):
             self._buffer = None
             return result
 
-    def getheader(self, name, default=None):
+    def getheader(self, name: str, default=None):
         return self.response.headers[name] if name in self.response.headers else default
 
     def getheaders(self):
@@ -130,7 +140,7 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse):
 class _SSLAdapter(HTTPAdapter):
     """Transport adapter that allows us to use different SSL versions."""
 
-    def __init__(self, ssl_version):
+    def __init__(self, ssl_version: _SSLMethod):
         self.ssl_version = ssl_version
         super().__init__()
 
@@ -156,7 +166,11 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
         self._session = None
         self._timeout = None
 
-    def request(self, method, path, headers=None, body=None):
+    def request(self,
+                method: str, path: str,
+                headers: Optional[dict[str, str]] = None,
+                body: Union[str, bytes, None] = None,
+                **kwargs) -> _WrappedHTTPResponse:
         """request.
 
         Send a HTTP request to the server.
@@ -169,14 +183,14 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
             An URI path segment (without the 'scheme://host:port/' parts),
             including query and frament segments.
         headers : dict of str -> str, optional
-            HTTP request headers. (Defautl: None)
+            HTTP request headers. (Default: None)
         body : str or bytes or readable, optional
             The body of the HTTP request. Bytes should be a utf-8 encoded
             string.
 
         Returns
         -------
-        response : CaosDBHTTPResponse
+        response : _WrappedHTTPResponse
         """
 
         if headers is None:
@@ -232,14 +246,16 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
                 "No connection url specified. Please "
                 "do so via linkahead.configure_connection(...) or in a config "
                 "file.")
-        if (not config["url"].lower().startswith("https://") and not config["url"].lower().startswith("http://")):
+        url_string: str = config["url"]
+        if (not url_string.lower().startswith("https://")
+                and not url_string.lower().startswith("http://")):
             raise LinkAheadConnectionError("The connection url is expected "
                                            "to be a http or https url and "
                                            "must include the url scheme "
                                            "(i.e. start with https:// or "
                                            "http://).")
 
-        url = urlparse(config["url"])
+        url: ParseResult = urlparse(url=url_string)
         path = url.path.strip("/")
         if len(path) > 0:
             path = path + "/"
@@ -271,7 +287,7 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
         if "timeout" in config:
             self._timeout = config["timeout"]
 
-    def _setup_ssl(self, config):
+    def _setup_ssl(self, config: dict[str, Any]):
         if "ssl_version" in config and config["cacert"] is not None:
             ssl_version = getattr(ssl, config["ssl_version"])
         else:
@@ -325,7 +341,7 @@ _DEFAULT_CONF = {
 }
 
 
-def _get_authenticator(**config):
+def _get_authenticator(**config) -> AbstractAuthenticator:
     """_get_authenticator.
 
     Import and configure the password_method.
@@ -337,7 +353,7 @@ def _get_authenticator(**config):
         Currently, there are four valid values for this parameter: 'plain',
         'pass', 'keyring' and 'auth_token'.
     **config :
-        Any other keyword arguments are passed the configre method of the
+        Any other keyword arguments are passed the configure method of the
         password_method.
 
     Returns
@@ -413,7 +429,9 @@ def configure_connection(**kwargs):
 
     auth_token : str (optional)
         An authentication token which has been issued by the LinkAhead Server.
-        Implies `password_method="auth_token"` if set.  An example token string would be `["O","OneTimeAuthenticationToken","anonymous",["administration"],[],1592995200000,604800000,"3ZZ4WKRB-5I7DG2Q6-ZZE6T64P-VQ","197d0d081615c52dc18fb323c300d7be077beaad4020773bb58920b55023fa6ee49355e35754a4277b9ac525c882bcd3a22e7227ba36dfcbbdbf8f15f19d1ee9",1,30000]`.
+        Implies `password_method="auth_token"` if set.  An example token string would be
+        ``["O","OneTimeAuthenticationToken","anonymous",["administration"],[],1592995200000,
+          604800000,"3ZZ4WKRB-5I7DG2Q6-ZZE6T64P-VQ","197d0d081615...1ee9",1,30000]``.
 
     https_proxy : str, optional
         Define a proxy for the https connections, e.g. `http://localhost:8888`,
@@ -534,8 +552,8 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
     __instance = None
 
     def __init__(self):
-        self._delegate_connection = None
-        self._authenticator = None
+        self._delegate_connection: Optional[CaosDBServerConnection] = None
+        self._authenticator: Optional[CredentialsAuthenticator] = None
         self.is_configured = False
 
     @classmethod
@@ -553,7 +571,8 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
                 "Missing CaosDBServerConnection implementation. You did not "
                 "specify an `implementation` for the connection.")
         try:
-            self._delegate_connection = config["implementation"]()
+            self._delegate_connection: CaosDBServerConnection = config["implementation"](
+            )
 
             if not isinstance(self._delegate_connection,
                               CaosDBServerConnection):
@@ -579,14 +598,19 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
 
         return self
 
-    def retrieve(self, entity_uri_segments=None, query_dict=None, **kwargs):
+    def retrieve(self,
+                 entity_uri_segments: Optional[list[str]] = None,
+                 query_dict: Optional[dict[str, Optional[str]]] = None,
+                 **kwargs) -> CaosDBHTTPResponse:
         path = make_uri_path(entity_uri_segments, query_dict)
 
         http_response = self._http_request(method="GET", path=path, **kwargs)
 
         return http_response
 
-    def delete(self, entity_uri_segments=None, query_dict=None, **kwargs):
+    def delete(self, entity_uri_segments: Optional[list[str]] = None,
+               query_dict: Optional[dict[str, Optional[str]]] = None, **kwargs) -> (
+                   CaosDBHTTPResponse):
         path = make_uri_path(entity_uri_segments, query_dict)
 
         http_response = self._http_request(
@@ -594,15 +618,18 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
 
         return http_response
 
-    def update(self, entity_uri_segment, query_dict=None, **kwargs):
+    def update(self, entity_uri_segment: Optional[list[str]],
+               query_dict: Optional[dict[str, Optional[str]]] = None, **kwargs) -> (
+                   CaosDBHTTPResponse):
         path = make_uri_path(entity_uri_segment, query_dict)
 
         http_response = self._http_request(method="PUT", path=path, **kwargs)
 
         return http_response
 
-    def activate_user(self, link):
-        self._authenticator.logout()
+    def activate_user(self, link: str) -> CaosDBHTTPResponse:
+        if self._authenticator is not None:
+            self._authenticator.logout()
         fullurl = urlparse(link)
         path = fullurl.path
         query = fullurl.query
@@ -611,17 +638,19 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
 
         return http_response
 
-    def put_form_data(self, entity_uri_segment, params):
+    def put_form_data(self, entity_uri_segment: str, params) -> CaosDBHTTPResponse:
         return self._form_data_request(
             method="PUT", path=entity_uri_segment, params=params)
 
-    def post_form_data(self, entity_uri_segment, params):
+    def post_form_data(self, entity_uri_segment: str, params: dict[str, Optional[str]]) -> (
+            CaosDBHTTPResponse):
         return self._form_data_request(
             method="POST",
             path=entity_uri_segment,
             params=params)
 
-    def _form_data_request(self, method, path, params):
+    def _form_data_request(self, method: str, path: str, params: dict[str, Optional[str]]) -> (
+            CaosDBHTTPResponse):
         body = urlencode(params)
         headers = {}
         headers["Content-Type"] = "application/x-www-form-urlencoded"
@@ -633,7 +662,9 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
 
         return response
 
-    def insert(self, entity_uri_segment, query_dict=None, body=None, **kwargs):
+    def insert(self, entity_uri_segment:  Optional[list[str]],
+               query_dict: Optional[dict[str, Optional[str]]] = None,
+               body: Union[str, bytes, None] = None, **kwargs) -> CaosDBHTTPResponse:
         path = make_uri_path(entity_uri_segment, query_dict)
 
         http_response = self._http_request(
@@ -641,7 +672,7 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
 
         return http_response
 
-    def download_file(self, path):
+    def download_file(self, path: str):
         """This function downloads a file via HTTP from the LinkAhead file
         system."""
         try:
@@ -658,7 +689,9 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
     def _logout(self):
         self._authenticator.logout()
 
-    def _http_request(self, method, path, headers=None, body=None, **kwargs):
+    def _http_request(self, method: str, path: str,
+                      headers: Optional[dict["str", Any]] = None,
+                      body: Union[str, bytes, None] = None, **kwargs):
         try:
             return self._retry_http_request(method=method, path=path,
                                             headers=headers, body=body,
@@ -681,16 +714,30 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
                                                 **kwargs)
             raise
 
-    def _retry_http_request(self, method, path, headers, body, **kwargs):
+    def _retry_http_request(self,
+                            method: str,
+                            path: str,
+                            headers: Optional[dict["str", Any]],
+                            body: Union[str, bytes, None], **kwargs) -> CaosDBHTTPResponse:
 
-        if hasattr(body, "encode"):
+        if hasattr(body, "encode") and body is not None:
             # python3
             body = body.encode("utf-8")
 
         if headers is None:
             headers = {}
+
+        if self._authenticator is None:
+            raise ValueError(
+                "No authenticator set. Please call configure_connection() first.")
+
         self._authenticator.on_request(method=method, path=path,
                                        headers=headers)
+
+        if self._delegate_connection is None:
+            raise ValueError(
+                "No connection set. Please call configure_connection() first.")
+
         _LOGGER.debug("request: %s %s %s", method, path, str(headers))
         http_response = self._delegate_connection.request(
             method=method,
@@ -704,10 +751,18 @@ class _Connection(object):  # pylint: disable=useless-object-inheritance
 
         return http_response
 
-    def get_username(self):
+    def get_username(self) -> str:
         """
         Return the username of the current connection.
 
         Shortcut for: get_connection()._authenticator._credentials_provider.username
         """
+        warnings.warn("Deprecated. Please use ``la.Info().user_info.name`` instead.",
+                      DeprecationWarning)
+        if self._authenticator is None:
+            raise ValueError(
+                "No authenticator set. Please call configure_connection() first.")
+        if self._authenticator._credentials_provider is None:
+            raise ValueError(
+                "No credentials provider set. Please call configure_connection() first.")
         return self._authenticator._credentials_provider.username
diff --git a/src/linkahead/connection/encode.py b/src/linkahead/connection/encode.py
index 6b328285e97e4dce2483ddd955134ee64cd3ce84..a76197803c9652e2d0c4e32819ee3e3f97758bfc 100644
--- a/src/linkahead/connection/encode.py
+++ b/src/linkahead/connection/encode.py
@@ -5,6 +5,8 @@
 #
 # Copyright (C) 2018 Research Group Biomedical Physics,
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2024 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Joscha Schmiedt <joscha@schmiedt.dev>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -48,6 +50,7 @@ as multipart/form-data suitable for a HTTP POST or PUT request.
 
 multipart/form-data is the standard way to upload files over HTTP
 """
+from __future__ import annotations
 
 __all__ = [
     'gen_boundary', 'encode_and_quote', 'MultipartParam', 'encode_string',
@@ -61,6 +64,10 @@ import re
 import os
 import mimetypes
 from email.header import Header
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Optional
 
 
 def gen_boundary():
@@ -68,7 +75,7 @@ def gen_boundary():
     return uuid.uuid4().hex
 
 
-def encode_and_quote(data):
+def encode_and_quote(data: Optional[str]) -> Optional[str]:
     """If ``data`` is unicode, return urllib.quote_plus(data.encode("utf-8"))
     otherwise return urllib.quote_plus(data)"""
     if data is None:
@@ -111,7 +118,7 @@ class MultipartParam(object):
     """
 
     def __init__(self,
-                 name,
+                 name: str,
                  value=None,
                  filename=None,
                  filetype=None,
@@ -147,14 +154,6 @@ class MultipartParam(object):
                 except BaseException:
                     raise ValueError("Could not determine filesize")
 
-    def __cmp__(self, other):
-        attrs = [
-            'name', 'value', 'filename', 'filetype', 'filesize', 'fileobj'
-        ]
-        myattrs = [getattr(self, a) for a in attrs]
-        oattrs = [getattr(other, a) for a in attrs]
-        return cmp(myattrs, oattrs)
-
     def reset(self):
         """Reset the file object's read pointer."""
         if self.fileobj is not None:
diff --git a/src/linkahead/connection/interface.py b/src/linkahead/connection/interface.py
index d63dbeb8cc4cd59e056823440948aa54906dd47c..fc22577dffb4f2e0d30924324cd7a4901d2c8b1a 100644
--- a/src/linkahead/connection/interface.py
+++ b/src/linkahead/connection/interface.py
@@ -22,11 +22,14 @@
 # ** end header
 #
 """This module defines the CaosDBServerConnection interface."""
-from abc import ABCMeta, abstractmethod, abstractproperty
+from __future__ import annotations
+from abc import ABCMeta, abstractmethod, ABC
 from warnings import warn
 
-# meta class compatible with Python 2 *and* 3:
-ABC = ABCMeta('ABC', (object, ), {'__slots__': ()})
+
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+    from typing import Optional, Union
 
 
 class CaosDBHTTPResponse(ABC):
@@ -34,14 +37,14 @@ class CaosDBHTTPResponse(ABC):
     LinkAheadServer."""
 
     @abstractmethod
-    def read(self, size=-1):
+    def read(self, size: Optional[int] = -1):
         """Read up to *size* bytes from the response body.
 
         If size is unspecified or -1, all bytes until EOF are returned.
         """
 
     @abstractmethod
-    def getheader(self, name, default=None):
+    def getheader(self, name: str, default=None):
         """Return the value of the header *name* or the value of *default* if
         there is no such header.
 
@@ -50,12 +53,13 @@ class CaosDBHTTPResponse(ABC):
         are returned likewise.
         """
 
-    @abstractproperty
-    def status(self):
+    @property
+    @abstractmethod
+    def status(self) -> int:
         """Status code of the response."""
 
     @abstractmethod
-    def getheaders(self):
+    def getheaders(self) -> dict[str, str]:
         """Return all headers."""
 
     def __enter__(self):
@@ -78,7 +82,12 @@ class CaosDBServerConnection(ABC):
     LinkAhead server."""
 
     @abstractmethod
-    def request(self, method, path, headers=None, body=None, **kwargs):
+    def request(self,
+                method: str,
+                path: str,
+                headers: Optional[dict[str, str]] = None,
+                body: Union[str, bytes, None] = None,
+                **kwargs) -> CaosDBHTTPResponse:
         """Abstract method. Implement this method for HTTP requests to the
         LinkAhead server.
 
diff --git a/src/linkahead/connection/utils.py b/src/linkahead/connection/utils.py
index 90ec6b5ba6789747f5d4452a1260306b716b1f7e..deb97f808ea7bb8e6e35a206d1da66e18a39b7eb 100644
--- a/src/linkahead/connection/utils.py
+++ b/src/linkahead/connection/utils.py
@@ -5,6 +5,8 @@
 #
 # Copyright (C) 2018 Research Group Biomedical Physics,
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2024 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Joscha Schmiedt <joscha@schmiedt.dev>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -22,14 +24,18 @@
 # ** end header
 #
 """Utility functions for the connection module."""
-from __future__ import unicode_literals, print_function
-from builtins import str as unicode
-from urllib.parse import (urlencode as _urlencode, quote as _quote,
-                          urlparse, urlunparse, unquote as _unquote)
+from __future__ import annotations
+
 import re
+from urllib.parse import (urlencode as _urlencode, quote as _quote,
+                          urlparse, urlunparse, unquote)
 
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+    from typing import Optional
 
-def urlencode(query):
+
+def urlencode(query: dict[str, Optional[str]]) -> str:
     """Convert a dict of into a url-encoded (unicode) string.
 
     This is basically a python2/python3 compatibility wrapper for the respective
@@ -79,7 +85,8 @@ modules when they are called with only the query parameter.
         }))
 
 
-def make_uri_path(segments=None, query=None):
+def make_uri_path(segments: Optional[list[str]] = None,
+                  query: Optional[dict[str, Optional[str]]] = None) -> str:
     """Url-encode all segments, concat them with slashes and append the query.
 
     Examples
@@ -105,22 +112,25 @@ def make_uri_path(segments=None, query=None):
     """
     path_no_query = ("/".join([quote(segment) for segment in segments])
                      if segments else "")
-    return str(path_no_query if query is None else "?".join([
+    if query is None:
+        return str(path_no_query)
+
+    return str("?".join([
         path_no_query, "&".join([
             quote(key) + "=" +
-            (quote(query[key]) if query[key] is not None else "")
+            (quote(query[key]) if query[key] is not None else "")  # type: ignore
             for key in query
         ])
     ]))
 
 
-def quote(string):
+def quote(string: str) -> str:
     enc = string.encode('utf-8')
     return _quote(enc).replace('/', '%2F')
 
 
-def parse_url(url):
-    fullurl = urlparse(url)
+def parse_url(url: str):
+    fullurl = urlparse(url=url)
     # make sure the path ends with a slash
     if not fullurl.path.endswith("/"):
         parse_result = list(fullurl)
@@ -132,19 +142,7 @@ def parse_url(url):
 _PATTERN = re.compile(r"^SessionToken=([^;]*);.*$")
 
 
-def unquote(string):
-    """unquote.
-
-    Decode an urlencoded string into a plain text string.
-    """
-    bts = _unquote(string)
-    if hasattr(bts, "decode"):
-        # python 2
-        return bts.decode("utf-8")
-    return bts
-
-
-def parse_auth_token(cookie):
+def parse_auth_token(cookie: Optional[str]) -> Optional[str]:
     """parse_auth_token.
 
     Parse an auth token from a cookie.
@@ -165,7 +163,7 @@ def parse_auth_token(cookie):
     return auth_token
 
 
-def auth_token_to_cookie(auth_token):
+def auth_token_to_cookie(auth_token: str) -> str:
     """auth_token_to_cookie.
 
     Urlencode an auth token string and format it as a cookie.
diff --git a/src/linkahead/high_level_api.py b/src/linkahead/high_level_api.py
index 70f1be36283b706f8d38d450d937ab13a9b9e699..18d219c732672d16d0ab43e562cfe73d682614fe 100644
--- a/src/linkahead/high_level_api.py
+++ b/src/linkahead/high_level_api.py
@@ -23,7 +23,7 @@
 #
 # ** end header
 #
-
+# type: ignore
 """
 A high level API for accessing LinkAhead entities from within python.
 
diff --git a/src/linkahead/utils/get_entity.py b/src/linkahead/utils/get_entity.py
index 282f7c86e10571d0e0d62b93da7f61bba5205cba..0ffd89e4dc7f214bbc72d4508f6ca4481dad7d9c 100644
--- a/src/linkahead/utils/get_entity.py
+++ b/src/linkahead/utils/get_entity.py
@@ -21,32 +21,57 @@
 
 """Convenience functions to retrieve a specific entity."""
 
-from typing import Union
+from typing import Union, Optional
 
 from ..common.models import Entity, execute_query
 from .escape import escape_squoted_text
 
 
-def get_entity_by_name(name: str) -> Entity:
+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 ENTITY WITH name='{name}'".
+Submits the query "FIND {role} WITH name='{name}'".
+
+Parameters
+----------
+
+role: str, optional
+  The role for the query, defaults to ``ENTITY``.
     """
     name = escape_squoted_text(name)
-    return execute_query(f"FIND ENTITY WITH name='{name}'", unique=True)
+    if role is None:
+        role = "ENTITY"
+    # type hint can be ignored, it's a unique query, so never Container or int
+    return execute_query(f"FIND {role} WITH name='{name}'", unique=True)  # type: ignore
 
 
-def get_entity_by_id(eid: Union[str, int]) -> Entity:
+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 ENTITY WITH id='{eid}'".
+Submits the query "FIND {role} WITH id='{eid}'".
+
+Parameters
+----------
+
+role: str, optional
+  The role for the query, defaults to ``ENTITY``.
     """
-    return execute_query(f"FIND ENTITY WITH id='{eid}'", unique=True)
+    if role is None:
+        role = "ENTITY"
+    # type hint can be ignored, it's a unique query
+    return execute_query(f"FIND {role} WITH id='{eid}'", unique=True)  # type: ignore
 
 
 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 FILE WHICH IS STORED AT '{path}'".
+Submits the query "FIND {role} WHICH IS STORED AT '{path}'".
+
+Parameters
+----------
+
+role: str, optional
+  The role for the query, defaults to ``ENTITY``.
     """
-    return execute_query(f"FIND FILE WHICH IS STORED AT '{path}'", unique=True)
+    # 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/server_side_scripting.py b/src/linkahead/utils/server_side_scripting.py
index 06caa3d94a629e368dc99f83dc2957c756b7b487..867155cf1f93bf1936e9b19f14926726f362edaf 100644
--- a/src/linkahead/utils/server_side_scripting.py
+++ b/src/linkahead/utils/server_side_scripting.py
@@ -99,6 +99,19 @@ def _make_request(call, pos_args, opts, files=None):
 
 def run_server_side_script(call, *args, files=None, **kwargs):
     """
+    Parameters
+    ----------
+    call : str
+        name of the script to be called, potentially with path prefix (e.g. ``management/update.py``)
+    *args : list(str)
+        list of positional arguments
+    files : dict
+        dictionary with where keys are the argument names with prefix (e.g. ``-p1`` or ``-Ofile``) and
+        the values are the paths to the files to be uploaded. Note, that only the base name will be
+        used when uploaded. Files will be placed in the ``.upload_files`` folder. Thus, the script
+        will be called with the argument ``<key>=.upload_files/<basename>``.
+    **kwargs : dict
+        kwargs will be passed to ``_make_request``
 
     Return
     ------
diff --git a/tox.ini b/tox.ini
index bbaaa1fc9eec2aba87c247d783818d215d8a7d5e..592c660c5bbbf5805a3ecbb3e60c41f597182a55 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ deps = .
     mypy
     jsonschema>=4.4.0
     setuptools
-commands=py.test --cov=caosdb -vv {posargs}
+commands=py.test --cov=linkahead -vv {posargs}
 
 [flake8]
 max-line-length=100
diff --git a/unittests/test_configuration.py b/unittests/test_configuration.py
index 40506e878b18473587da8b694d9381c15bdbd860..95bc906c6c044c51548aa864326cc93f29a6042a 100644
--- a/unittests/test_configuration.py
+++ b/unittests/test_configuration.py
@@ -45,24 +45,24 @@ def temp_ini_files():
         remove("pylinkahead.ini")
     if created_temp_ini_home:
         remove(expanduser("~/.pylinkahead.ini"))
-    environ["PYCAOSDBINI"] = "~/.pylinkahead.ini"
+    environ["PYLINKAHEADINI"] = "~/.pylinkahead.ini"
 
 
 def test_config_ini_via_envvar(temp_ini_files):
 
     with raises(KeyError):
-        environ["PYCAOSDBINI"]
+        environ["PYLINKAHEADINI"]
 
-    environ["PYCAOSDBINI"] = "bla bla"
-    assert environ["PYCAOSDBINI"] == "bla bla"
+    environ["PYLINKAHEADINI"] = "bla bla"
+    assert environ["PYLINKAHEADINI"] == "bla bla"
     # test wrong configuration file in envvar
     with pytest.raises(RuntimeError):
         db.configuration._read_config_files()
     # test good configuration file in envvar
-    environ["PYCAOSDBINI"] = "~/.pylinkahead.ini"
+    environ["PYLINKAHEADINI"] = "~/.pylinkahead.ini"
     assert expanduser("~/.pylinkahead.ini") in db.configuration._read_config_files()
     # test without envvar
-    environ.pop("PYCAOSDBINI")
+    environ.pop("PYLINKAHEADINI")
     assert expanduser("~/.pylinkahead.ini") in db.configuration._read_config_files()
     # test configuration file in cwd
     assert join(getcwd(), "pylinkahead.ini") in db.configuration._read_config_files()
diff --git a/unittests/test_high_level_api.py b/unittests/test_high_level_api.py
index be57e3c747619852c7cec2002eac6928c7d77702..82c1a5caf0f0719b5946ecd6749b4079bb6794bc 100644
--- a/unittests/test_high_level_api.py
+++ b/unittests/test_high_level_api.py
@@ -21,7 +21,7 @@
 #
 # Test high level api module
 # A. Schlemmer, 02/2022
-
+# type: ignore
 
 import linkahead as db
 from linkahead.high_level_api import (convert_to_entity, convert_to_python_object,
diff --git a/unittests/test_issues.py b/unittests/test_issues.py
index 7472f710cea32c1d76f11e52fe7c3c3617804c3c..e24afbe8b7be8d9a87d85819eccd3a4bf0d453e8 100644
--- a/unittests/test_issues.py
+++ b/unittests/test_issues.py
@@ -24,6 +24,7 @@ import os
 import lxml
 import linkahead as db
 
+from datetime import date, datetime
 from pytest import raises
 
 
@@ -64,3 +65,28 @@ def test_issue_156():
     # </ParentList>
     assert value is project
     assert parents[0].name == "RTName"
+
+
+def test_issue_128():
+    """Test assigning datetime.date(time) values to DATETIME
+    properties:
+    https://gitlab.com/linkahead/linkahead-pylib/-/issues/128.
+
+    """
+    # Test assignement correct assignment for both datatype=DATETIME
+    # and datatype=LIST<DATETIME>, just to be sure.
+    prop = db.Property(name="TestDatetime", datatype=db.DATETIME)
+    prop_list = db.Property(name="TestListDatetime", datatype=db.LIST(db.DATETIME))
+
+    today = date.today()
+    now = datetime.now()
+
+    prop.value = today
+    assert prop.value == today
+    prop.value = now
+    assert prop.value == now
+
+    prop_list.value = [today, today]
+    assert prop_list.value == [today, today]
+    prop_list.value = [now, now]
+    assert prop_list.value == [now, now]