Repository: SINTEF/dlite (language: python)

Issue:
Update and clean Sphinx configuration
Take inspiration from H2020-OpenModel/WP3-WrapperSDK.
Remove Py files under built `mapping-plugins` folder
These come from examples and should not be part of the "installed" plugins.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 9f29d056a..516729642 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,4 +35,7 @@ Dockerfile-*linux*[^.template]
 
 # Units (pint) files
 units.cpython-*.pyc
-bindings/python/triplestore/__pycache__
\ No newline at end of file
+bindings/python/triplestore/__pycache__
+
+# Documentation
+pydoc/autoapi/
diff --git a/bindings/python/dlite-entity-python.i b/bindings/python/dlite-entity-python.i
index aaf8d2e5e..36a2987d7 100644
--- a/bindings/python/dlite-entity-python.i
+++ b/bindings/python/dlite-entity-python.i
@@ -1,6 +1,6 @@
 /* -*- Python -*-  (not really, but good for syntax highlighting) */
 
-/* Python-spesific extensions to dlite-entity.i */
+/* Python-specific extensions to dlite-entity.i */
 %pythoncode %{
 import sys
 import json
@@ -115,18 +115,19 @@ def standardise(v, prop, asdict=False):
         return conv(v)
 
 
-def get_instance(id: "str", metaid: "str"=None, check_storages: "bool"=True) -> "Instance":
+def get_instance(id: "str", metaid: "str" = None, check_storages: "bool" = True) -> "Instance":
     """Return instance with given id.
 
     Arguments:
-        id: Id of instance to return.
+        id: ID of instance to return.
         metaid: If given, dlite will try to convert the instance to a new
-            instance of `metaid`.
+            instance of ``metaid``.
         check_storages: Whether to check for the instance in storages listed
             in dlite.storage_path if the instance is not already in memory.
 
     Returns:
-        DLite instance.
+        DLite Instance.
+
     """
     if isinstance(id, dlite.Instance):
         inst = id
diff --git a/bindings/python/dlite-entity.i b/bindings/python/dlite-entity.i
index f35b3024a..d20acf8d0 100644
--- a/bindings/python/dlite-entity.i
+++ b/bindings/python/dlite-entity.i
@@ -122,16 +122,23 @@ struct _DLiteDimension {
 %feature("docstring", "\
 Creates a new property.
 
+```python
 Property(name, type, dims=None, unit=None, description=None)
-    Creates a new property with the provided attributes.
+```
 
+Creates a new property with the provided attributes.
+
+```python
 Property(seq)
-    Creates a new property from sequence of 6 strings, corresponding to
-    `name`, `type`, `dims`, `unit` and `description`.  Valid
-    values for `dims` are:
-      - '' or '[]': no dimensions
-      - '<dim1>, <dim2>': list of dimension names
-      - '[<dim1>, <dim2>]': list of dimension names
+```
+
+Creates a new property from sequence of 6 strings, corresponding to
+``name``, ``type``, ``dims``, ``unit`` and ``description``.
+Valid values for ``dims`` are:
+
+- ``''`` or ``'[]'``: No dimensions.
+- ``'<dim1>, <dim2>'``: List of dimension names.
+- ``'[<dim1>, <dim2>]'``: List of dimension names.
 
 ") _DLiteProperty;
 %rename(Property) _DLiteProperty;
@@ -608,15 +615,22 @@ Call signatures:
     dlite_swig_set_property_by_index($self, i, obj);
   }
 
-  %feature("docstring",
-           "Return property `name` as a string.\n"
-           "\n"
-           "`width`  Minimum field width. Unused if 0, auto if -1.\n"
-           "`prec`   Precision. Auto if -1, unused if -2.\n"
-           "`flags`  Or'ed sum of formatting flags:\n"
-           "    0  default (json)\n"
-           "    1  raw unquoted output\n"
-           "    2  quoted output")
+  %feature("docstring", "\
+Return property ``name`` as a string.
+
+Parameters:
+    width: Minimum field width. Unused if 0, auto if -1.
+    prec: Precision. Auto if -1, unused if -2.
+    flags: Or'ed sum of formatting flags:
+
+        - ``0``: Default (json).
+        - ``1``: Raw unquoted output.
+        - ``2``: Quoted output.
+
+Returns:
+    Property as a string.
+
+")
      get_property_as_string;
   %newobject get_property_as_string;
   char *get_property_as_string(const char *name,
diff --git a/bindings/python/dlite-misc-python.i b/bindings/python/dlite-misc-python.i
index a10f67f9e..1b38da4ba 100644
--- a/bindings/python/dlite-misc-python.i
+++ b/bindings/python/dlite-misc-python.i
@@ -8,12 +8,14 @@ import dlite
 class err():
     """Context manager for temporary turning off or redirecting errors.
 
-    By default errors are skipped within the err context.  But if
-    `filename` is provided, the error messages are written to that file.
-    Special file names includes
-      - None or empty: no output is written
-      - <stderr>: write errors to stderr (default)
-      - <stdout>: write errors to stdout
+    By default errors are skipped within the err context. But if
+    ``filename`` is provided, the error messages are written to that file.
+    Special file names includes:
+
+    - ``None`` or empty: No output is written.
+    - ``<stderr>``: Write errors to stderr (default).
+    - ``<stdout>``: Write errors to stdout.
+
     """
     def __init__(self, filename=None):
         self.filename = filename
diff --git a/bindings/python/dlite-misc.i b/bindings/python/dlite-misc.i
index e82018885..25da852ea 100644
--- a/bindings/python/dlite-misc.i
+++ b/bindings/python/dlite-misc.i
@@ -41,22 +41,24 @@ const char *dlite_get_license(void);
 
 %feature("docstring", "\
 Returns an UUID, depending on:
-  - If `id` is NULL or empty, generates a new random version 4 UUID.
-  - If `id` is not a valid UUID string, generates a new version 5 sha1-based
-    UUID from `id` using the DNS namespace.
-  - Otherwise return `id` (which already must be a valid UUID).
+
+- If ``id`` is NULL or empty, generates a new random version 4 UUID.
+- If ``id`` is not a valid UUID string, generates a new version 5 sha1-based
+  UUID from ``id`` using the DNS namespace.
+
+Otherwise return ``id`` (which already must be a valid UUID).
 ") dlite_get_uuid;
 %cstring_bounded_output(char *buff36, DLITE_UUID_LENGTH+1);
 void dlite_get_uuid(char *buff36, const char *id=NULL);
 
 %feature("docstring", "\
-Returns the generated UUID version number if `id` had been passed to
-get_uuid() or zero if `id` is already a valid UUID.
+Returns the generated UUID version number if ``id`` had been passed to
+get_uuid() or zero if ``id`` is already a valid UUID.
 ") get_uuid_version;
 posstatus_t get_uuid_version(const char *id=NULL);
 
 %feature("docstring", "\
-Returns a (metadata) uri by combining `name`, `version` and `namespace` as:
+Returns a (metadata) uri by combining ``name``, ``version`` and ``namespace`` as:
 
     namespace/version/name
 ") dlite_join_meta_uri;
@@ -66,7 +68,7 @@ char *dlite_join_meta_uri(const char *name, const char *version,
 
 
 %feature("docstring", "\
-Returns (name, version, namespace)-tuplet from valid metadata `uri`.
+Returns (name, version, namespace)-tuplet from valid metadata ``uri``.
 ") dlite_split_meta_uri;
 %cstring_output_allocate(char **name,      if (*$1) free(*$1));
 %cstring_output_allocate(char **version,   if (*$1) free(*$1));
@@ -81,7 +83,7 @@ Returns an url constructed from the arguments of the form:
 
     driver://location?options#fragment
 
-The `driver`, `options` and `fragment` arguments may be None.
+The ``driver``, ``options`` and ``fragment`` arguments may be None.
 ") dlite_join_url;
 %newobject dlite_join_url;
 char *dlite_join_url(const char *driver, const char *location,
@@ -89,7 +91,7 @@ char *dlite_join_url(const char *driver, const char *location,
 
 %feature("docstring", "\
 Returns a (driver, location, options, fragment)-tuplet by splitting
-`url` of the form
+``url`` of the form
 
     driver://location?options#fragment
 
@@ -108,11 +110,12 @@ Match string 's' against glob pattern 'pattern' and return zero on
 match.
 
 Understands the following patterns:
-     *       any number of characters
-     ?       any single character
-     [a-z]   any single character in the range a-z
-     [^a-z]  any single character not in the range a-z
-     \x      match x
+
+- ``*``: Any number of characters.
+- ``?``: Any single character.
+- ``[a-z]``: Any single character in the range a-z.
+- ``[^a-z]``: Any single character not in the range a-z.
+- ``\\x``: Match x.
 ") globmatch;
 int globmatch(const char *pattern, const char *s);
 
@@ -145,10 +148,12 @@ Set error stream.
 void dlite_err_set_stream(FILE *);
 
 %feature("docstring", "\
-Set error log file.  Special values includes:
-  - None | "": turn off error output
-  - <stderr>:  standard error
-  - <stdout>:  standard output
+Set error log file. Special values includes:
+
+- ``None`` or empty: Turn off error output.
+- ``<stderr>``: Standard error.
+- ``<stdout>``: Standard output.
+
 All other values are treated as a filename that will be opened in append mode.
 ") dlite_err_set_file;
 void dlite_err_set_file(const char *filename);
diff --git a/bindings/python/dlite-storage.i b/bindings/python/dlite-storage.i
index b421371b6..a2418feb9 100644
--- a/bindings/python/dlite-storage.i
+++ b/bindings/python/dlite-storage.i
@@ -9,8 +9,8 @@
 
 /* Storage iterator */
 %feature("docstring", "\
-Iterates over instances in storage `s`.  If `pattern` is given, only
-instances whos metadata URI matches `pattern` are returned.
+Iterates over instances in storage ``s``.  If ``pattern`` is given, only
+instances whos metadata URI matches ``pattern`` are returned.
 ") StorageIterator;
 %inline %{
   struct StorageIterator {
@@ -57,7 +57,7 @@ Returns next instance or None if exhausted.") next;
 enum _DLiteIDFlag {
   dliteIDTranslateToUUID=0, /*!< Translate id's that are not a valid UUID to
                                  a (version 5) UUID (default). */
-  dliteIDRequireUUID=1,     /*!< Require that `id` is a valid UUID. */
+  dliteIDRequireUUID=1,     /*!< Require that ``id`` is a valid UUID. */
   dliteIDKeepID=2           /*!< Store data under the given id, even if it
                                  is not a valid UUID.  Not SOFT compatible,
                                  but may be useful for input files. */
@@ -71,21 +71,23 @@ Represents a data storage.
 Parameters
 ----------
 driver_or_url : string
-    Name of driver used to connect to the storage or, if `location` is not
+    Name of driver used to connect to the storage or, if ``location`` is not
     given, the URL to the storage:
 
         driver://location?options
 
 location : string
-    The location to the storage.  For file storages, this is the file name.
+    The location to the storage. For file storages, this is the file name.
 options : string
     Additional options passed to the driver as a list of semicolon-separated
-    ``key=value`` pairs.  Each driver may have their own options.  Some
+    ``key=value`` pairs. Each driver may have their own options. Some
     common options are:
-      - mode={'append','r','w'}: 'append': append to existing storage or
-        create a new one (hdf5,json).
-      - compact={'yes','no'}: Whether to store in a compact format (json).
-      - meta={'yes','no'}: Whether to format output as metadata (json).
+
+    - ``mode={'append','r','w'}``:
+      - 'append': Append to existing storage or create a new one (hdf5,json).
+    - ``compact={'yes','no'}``: Whether to store in a compact format (json).
+    - ``meta={'yes','no'}``: Whether to format output as metadata (json).
+
 ") _DLiteStorage;
 %rename(Storage) _DLiteStorage;
 
@@ -120,7 +122,7 @@ Returns name of driver for this storage.") get_driver;
 
   %feature("docstring", "\
 Returns a list of UUIDs of all instances in the storage whos metadata
-matches `pattern`.  If `pattern` is None, all UUIDs will be returned.
+matches ``pattern``. If ``pattern`` is None, all UUIDs will be returned.
 ") get_uuids;
   char **get_uuids(const char *pattern=NULL) {
     return dlite_storage_uuids($self, pattern);
diff --git a/bindings/python/factory.py b/bindings/python/factory.py
index 3d24848f2..cc4c242e7 100644
--- a/bindings/python/factory.py
+++ b/bindings/python/factory.py
@@ -1,117 +1,151 @@
-#!/usr/bin/env python
 # -*- coding: utf-8 -*-
 """A module that makes it easy to add dlite functionality to existing classes.
 
-Class customisations
---------------------
-In order to handle special cases, the following methods may be
-defined/overridden in the class that is to be extended:
-    _dlite_get_<name>(self, name)
-    _dlite_set_<name>(self, name, value)
-    _dlite__new__(cls, inst)
+## Class customisations
+
+In order to handle special cases, the following methods may be defined/overridden in
+the class that is to be extended:
+
+```python
+_dlite_get_<name>(self, name)
+_dlite_set_<name>(self, name, value)
+_dlite__new__(cls, inst)
+```
+
 """
+from __future__ import annotations
+
 import copy
+from typing import TYPE_CHECKING
 
 import numpy as np
 
 from .dlite import Instance, Storage
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Any, Callable, List, Optional, Tuple, Union
+
+    from dlite import Metadata
+
 
 class FactoryError(Exception):
     """Base exception for factory errors."""
-    pass
 
 class IncompatibleClassError(FactoryError):
     """Raised if an extended class is not compatible with its dlite
     metadata."""
-    pass
 
 
 class MetaExtension(type):
     """Metaclass for BaseExtension."""
-    def __init__(cls, name, bases, attr):
+
+    def __init__(cls, name, bases, attr) -> None:
         super().__init__(name, bases, attr)
 
 
-class BaseExtension(object, metaclass=MetaExtension):
-    """Base class for extension.  Except for `dlite_id`, all
-    arguments are passed further to the __init__() function of the
-    class we are inheriting from.
-    If `instanceid` is given, the id of the underlying dlite instance
-    will be set to it.
+class BaseExtension(metaclass=MetaExtension):
+    """Base class for extension.
+
+    Except for ``dlite_id``, all arguments are passed further to the ``__init__()``
+    function of the class we are inheriting from. If ``instanceid`` is given, the id of
+    the underlying dlite instance will be set to it.
+
     """
-    def __init__(self, *args, instanceid=None, **kw):
-        self._theclass.__init__(self, *args, **kw)
+
+    def __init__(self, *args, instanceid: str = None, **kwargs) -> None:
+        self._theclass.__init__(self, *args, **kwargs)
         self._dlite_init(instanceid=instanceid)
 
-    def _dlite_init(self, instanceid=None):
-        """Initialise the underlying dlite instance.  If `id` is given,
-        the id of the underlying dlite instance will be set to it."""
+    def _dlite_init(self, instanceid: str = None) -> None:
+        """Initialise the underlying DLite Instance.
+
+        If ``id`` is given, the id of the underlying DLite Instance will be set to it.
+
+        Parameters:
+            instanceid: A DLite ID (UUID) to use for the underlying DLite Instance.
+
+        """
         dims = self._dlite_infer_dimensions()
         self.dlite_inst = Instance.from_metaid(
-            self.dlite_meta.uri, dims, instanceid)
+            self.dlite_meta.uri, dims, instanceid
+        )
         self._dlite_assign_properties()
 
-    def _dlite_get(self, name):
-        """Returns the value of property `name` from the wrapped opject."""
-        if hasattr(self, '_dlite_get_' + name):
-            getter = getattr(self, '_dlite_get_' + name)
-            value = getter()
-        elif hasattr(self, 'get_' + name):
-            getter = getattr(self, 'get_' + name)
-            value = getter()
-        else:
-            value = getattr(self, name)
-        return value
-
-    def _dlite_set(self, name, value):
-        """Sets value of property `name` in the wrapped opject."""
-        if hasattr(self, '_dlite_set_' + name):
-            setter = getattr(self, '_dlite_set_' + name)
-            setter(value)
-        elif hasattr(self, 'set_' + name):
-            setter = getattr(self, 'set_' + name)
-            setter(value)
+    def _dlite_get(self, name: str) -> "Any":
+        """Returns the value of property `name` from the wrapped object.
+
+        Parameters:
+            name: The property to retrieve.
+
+        Returns:
+            The property value.
+
+        """
+        if hasattr(self, f"_dlite_get_{name}"):
+            return getattr(self, f"_dlite_get_{name}")()
+
+        if hasattr(self, f"get_{name}"):
+            return getattr(self, f"get_{name}")()
+
+        return getattr(self, name)
+
+    def _dlite_set(self, name: str, value: "Any") -> None:
+        """Sets value of property ``name`` in the wrapped object.
+
+        Parameters:
+            name: The property to set a value for.
+            value: The value to set for the property.
+
+        """
+        if hasattr(self, f"_dlite_set_{name}"):
+            getattr(self, f"_dlite_set_{name}")(value)
+        elif hasattr(self, f"set_{name}"):
+            getattr(self, f"set_{name}")(value)
         else:
             setattr(self, name, value)
 
-    def _dlite_infer_dimensions(self, meta=None, getter=None):
+    def _dlite_infer_dimensions(
+        self,
+        meta: "Optional[Metadata]" = None,
+        getter: "Optional[Callable[[str], Any]]" = None,
+    ) -> "List[int]":
         """Returns inferred property dimensions from __dict__."""
-        if not meta:
-            meta = self.dlite_meta
-        if not getter:
-            getter = self._dlite_get
-        dims = [-1] * len(meta.properties['dimensions'])
-        dimnames = [d.name for d in meta.properties['dimensions']]
-        for prop in meta.properties['properties']:
+        meta = meta if meta is not None else self.dlite_meta
+        getter = getter if getter is not None else self._dlite_get
+
+        dims = [-1] * len(meta.properties["dimensions"])
+        dimnames = [dim.name for dim in meta.properties["dimensions"]]
+        for prop in meta.properties["properties"]:
             if prop.ndims:
                 value = getter(prop.name)
-                if len(value) > 0:
-                    arr = np.array(value, copy=False)
-                else:
-                    arr = np.zeros([0] * prop.ndims)
-                if arr.ndim < prop.ndims:
-                    raise ValueError('expected %d dimensions for array '
-                                     'property "%s"; got %d' % (
-                                         prop.ndims, prop.name, arr.ndim))
+                array = (
+                    np.array(value, copy=False)
+                    if value else np.zeros([0] * prop.ndims)
+                )
+                if array.ndim < prop.ndims:
+                    raise ValueError(
+                        f"Expected {prop.ndims} dimensions for array property "
+                        f"{prop.name!r}; got {array.ndim}"
+                    )
                 for i, pdim in enumerate(prop.dims):
                     if pdim in dimnames:
                         n = dimnames.index(pdim)
                         if dims[n] == -1:
-                            dims[n] = arr.shape[i]
-                        elif arr.shape[i] and dims[n] != arr.shape[i]:
+                            dims[n] = array.shape[i]
+                        elif array.shape[i] and dims[n] != array.shape[i]:
                             raise ValueError(
-                                'inconsistent length of dimension "%s"; was '
-                                '%d but got %d for property %r' % (
-                                meta.properties['dimensions'][n].name, dims[n],
-                                arr.shape[i], prop.name))
+                                "Inconsistent length of dimension "
+                                f"{meta.properties['dimensions'][n].name!r}; was "
+                                f"{dims[n]} but got {array.shape[i]} for property "
+                                f"{prop.name!r}"
+                            )
         if min(dims) < 0:
-            raise ValueError('cannot infer all dimensions')
+            raise ValueError("Cannot infer all dimensions")
         return dims
 
-    def _dlite_assign_properties(self):
+    def _dlite_assign_properties(self) -> None:
         """Assigns all dlite properties from extended object."""
-        for prop in self.dlite_meta.properties['properties']:
+        for prop in self.dlite_meta.properties["properties"]:
             name = prop.name
             self.dlite_inst[name] = self._dlite_get(name)
 
@@ -119,31 +153,41 @@ def _dlite_assign_properties(self):
     def _dlite__new__(cls, inst=None):
         """Class method returning a new uninitialised instance of the class
         that is extended.
-        This method simply returns ``cls.__new__(cls)``.  Override
+        This method simply returns ``cls.__new__(cls)``. Override
         this method if the extended class already defines a __new__()
         method.
         """
         return cls.__new__(cls)
 
-    def dlite_assign(self, inst):
+    def dlite_assign(self, inst: Instance) -> None:
         """Assigns self from dlite instance `inst`."""
         if self.dlite_meta.uri != inst.meta.uri:
-            raise TypeError('Expected instance of metadata "%s", got "%s"' % (
-                self.dlite_meta.uri, inst.meta.uri))
-        for prop in self.dlite_meta.properties['properties']:
+            raise TypeError(
+                f"Expected instance of metadata {self.dlite_meta.uri!r}, got "
+                f"{inst.meta.uri!r}"
+            )
+        for prop in self.dlite_meta.properties["properties"]:
             name = prop.name
             self._dlite_set(name, inst[name])
 
-    def dlite_load(self, *args):
+    def dlite_load(self, *args) -> None:
         """Loads dlite instance from storage and assign self from it.
         The arguments `args` are passed to dlite.Instance.from_storage()."""
         inst = Instance.from_storage(*args)
         self._dlite_assign(inst)
 
 
-def instancefactory(theclass, inst):
-    """Returns an extended instance of `theclass` initiated from dlite
-    instance `inst`.
+def instancefactory(theclass: type, inst: Instance) -> "Any":
+    """Returns an extended instance of ``theclass`` initiated from dlite
+    instance ``inst``.
+
+    Parameters:
+        theclass: The class to instantiate an object from using ``inst``.
+        inst: A DLite Instance to use as source for a ``theclass`` instance object.
+
+    Returns:
+        A ``theclass`` instance object based on the DLite Instance ``inst``.
+
     """
     cls = classfactory(theclass, meta=inst.meta)
     obj = cls._dlite__new__(inst)
@@ -153,40 +197,76 @@ def instancefactory(theclass, inst):
     return obj
 
 
-def objectfactory(obj, meta=None, deepcopy=False, cls=None,
-                  url=None, storage=None, id=None, instanceid=None):
-    """Returns an extended copy of `obj`.  If `deepcopy` is true, a deep
-    copy is returned, otherwise a shallow copy is returned.
-    By default, the returned object will have the same class as `obj`.  If
-    `cls` is provided, the class of the returned object will be set to `cls`
+def objectfactory(
+    obj: "Any",
+    meta: "Optional[Metadata]" = None,
+    deepcopy: bool = False,
+    cls: "Optional[type]" = None,
+    url: "Optional[str]" = None,
+    storage: "Optional[Union[Storage, Tuple[str, str, str]]]" = None,
+    id: "Optional[str]" = None,
+    instanceid: "Optional[str]" = None,
+) -> "Any":
+    """Returns an extended copy of ``obj``.
+
+    If ``deepcopy`` is ``True``, a deep copy is returned, otherwise a shallow copy is
+    returned. By default, the returned object will have the same class as ``obj``.
+    If ``cls`` is provided, the class of the returned object will be set to ``cls``
     (typically a subclass of ``obj.__class__``).
-    The `url`, `storage` and `id` arguments are passed to classfactory().
+
+    The ``url``, ``storage``, and ``id`` arguments are passed to ``classfactory()``.
+
+    Parameters:
+        obj: A Python object.
+        meta: A DLite Metadata Instance.
+        deepcopy: Whether or not to perform a deep or shallow copy of ``obj``.
+        cls: A class to use for the new object. If this is not supplied, the new object
+            will be the same class as the original.
+        url: URL referring to the metadata. Should be of the form
+            ``driver://location?options#id``. Only used if ``cls`` is not specified.
+        storage: Storage from which to load meta data. Only used if ``cls`` is not
+            specified.
+        id: A unique ID referring to the meta data if ``storage`` is provided.
+        instanceid: A DLite ID (UUID) to use for the underlying DLite Instance.
+
+    Returns:
+        A new, extended copy of the Python object ``obj``.
+
     """
-    if cls is None:
-        cls = classfactory(obj.__class__, meta=meta, url=url,
-                           storage=storage, id=id)
+    cls = cls if cls is not None else classfactory(
+        obj.__class__,
+        meta=meta,
+        url=url,
+        storage=storage,
+        id=id,
+    )
     new = copy.deepcopy(obj) if deepcopy else copy.copy(obj)
     new.__class__ = cls
     new._dlite_init(instanceid=instanceid)
     return new
 
 
-def classfactory(theclass, meta=None, url=None, storage=None, id=None):
+def classfactory(
+    theclass: type,
+    meta: "Optional[Metadata]" = None,
+    url: "Optional[str]" = None,
+    storage: "Optional[Union[Storage, Tuple[str, str, str]]]" = None,
+    id: "Optional[str]" = None,
+) -> type:
     """Factory function that returns a new class that inherits from both
-    `theclass` and BaseInstance.
-    Parameters
-    ----------
-    theclass : class instance
-        The class to extend.
-    meta : Instance
-        Metadata instance.
-    url : string
-        URL referring to the metadata.  Should be of the form
-        ``driver://location?options#id``
-    storage : Storage | (driver, location, options) tuple
-        Storage to load meta from.
-    id : string
-        A unique id referring to the metadata if `storage` is provided.
+    ``theclass`` and ``BaseExtension``.
+
+    Parameters:
+        theclass: The class to extend.
+        meta: Metadata instance.
+        url: URL referring to the metadata. Should be of the form
+            ``driver://location?options#id``.
+        storage: Storage to load meta from.
+        id: A unique ID referring to the metadata if ``storage`` is provided.
+
+    Returns:
+        A new class based on ``theclass`` and ``BaseExtensions``.
+
     """
     if meta is None:
         if url is not None:
@@ -197,16 +277,15 @@ def classfactory(theclass, meta=None, url=None, storage=None, id=None):
             else:
                 meta = Instance.from_driver(*storage, id=id)
         else:
-            raise TypeError('`meta`, `url` or `storage` must be provided')
+            raise TypeError("`meta`, `url`, or `storage` must be provided.")
 
     if not meta.is_meta:
-        raise TypeError('`meta` must refer to metadata')
-
-    attr = dict(
-        dlite_meta=meta,
-        _theclass=theclass,
-        __init__=BaseExtension.__init__
-    )
+        raise TypeError("`meta` must refer to metadata.")
 
+    attr = {
+        "dlite_meta": meta,
+        "_theclass": theclass,
+        "__init__": BaseExtension.__init__,
+    }
 
     return type(meta.name, (theclass, BaseExtension), attr)
diff --git a/bindings/python/mappings.py b/bindings/python/mappings.py
index 4c9062bdf..6c5575305 100644
--- a/bindings/python/mappings.py
+++ b/bindings/python/mappings.py
@@ -11,21 +11,23 @@
 """
 from __future__ import annotations
 
-import itertools
-import types
 import warnings
 from collections import defaultdict
-from collections.abc import Sequence
 from enum import Enum
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import TYPE_CHECKING
 
 import numpy as np
 from pint import Quantity
-from tripper import Triplestore, DM, FNO, MAP, RDF, RDFS
+from tripper import DM, FNO, MAP, RDF, RDFS
 
 import dlite
 from dlite.utils import infer_dimensions
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Any, Callable, Generator, Optional, Sequence, Type
+
+    from tripper.triplestore import Triplestore
+
 
 class MappingError(dlite.DLiteError):
     """Base class for mapping errors."""
@@ -72,61 +74,78 @@ class Value:
         property_iri: IRI of datamodel property that this value is an
             instance of.
         cost: Cost of accessing this value.
+
     """
-    def __init__(self, value, unit=None, iri=None, property_iri=None, cost=0.0):
+    def __init__(
+        self,
+        value: "Any",
+        unit: "Optional[str]" =None,
+        iri: "Optional[str]" = None,
+        property_iri: "Optional[str]" = None,
+        cost: "Any | Callable" = 0.0,
+    ) -> None:
         self.value = value
         self.unit = unit
         self.iri = iri
         self.property_iri = property_iri
         self.cost = cost
 
-    def show(self, routeno=None, name=None, indent=0):
+    def show(
+        self,
+        routeno: "Optional[Any]" = None,
+        name: "Optional[str]" = None,
+        indent: int = 0,
+    ) -> str:
         """Returns a string representation of the Value.
 
         Arguments:
-            routeno: Unused.  The argument exists for consistency with
+            routeno: Unused. The argument exists for consistency with
                 the corresponding method in Step.
             name: Name of value.
             indent: Indentation level.
+
+        Returns:
+            String representation of the value.
+
         """
-        s = []
-        ind = ' '*indent
-        s.append(ind + f'{name if name else "Value"}:')
-        s.append(ind + f'  iri: {self.iri}')
-        s.append(ind + f'  property_iri: {self.property_iri}')
-        s.append(ind + f'  unit: {self.unit}')
-        s.append(ind + f'  cost: {self.cost}')
-        s.append(ind + f'  value: {self.value}')
-        return '\n'.join(s)
+        res = []
+        ind = " " * indent
+        res.append(ind + f"{name if name else 'Value'}:")
+        res.append(ind + f"  iri: {self.iri}")
+        res.append(ind + f"  property_iri: {self.property_iri}")
+        res.append(ind + f"  unit: {self.unit}")
+        res.append(ind + f"  cost: {self.cost}")
+        res.append(ind + f"  value: {self.value}")
+        return "\n".join(res)
 
 
 class MappingStep:
     """A step in a mapping route from a target to one or more sources.
 
-    A mapping step corresponds to one or more RDF triples.  In the
+    A mapping step corresponds to one or more RDF triples. In the
     simple case of a `mo:mapsTo` or `rdfs:isSubclassOf` relation, it is
-    only one triple.  For transformations that has several input and
+    only one triple. For transformations that has several input and
     output, a set of triples are expected.
 
     Arguments:
         output_iri: IRI of the output concept.
         steptype: One of the step types from the StepType enum.
         function: Callable that evaluates the output from the input.
-        cost: The cost related to this mapping step.  Should be either a
-            float or a callable taking the same arguments as `function` as
+        cost: The cost related to this mapping step. Should be either a
+            float or a callable taking the same arguments as ``function`` as
             input returning the cost as a float.
         output_unit: Output unit.
 
     The arguments can also be assigned as attributes.
     """
     def __init__(
-            self,
-            output_iri: Optional[str] = None,
-            steptype: Optional[StepType] = None,
-            function: Optional[Callable] = None,
-            cost: Union[Any, Callable] = 1.0,
-            output_unit: Optional[str] = None,
-    ):
+        self,
+        output_iri: "Optional[str]" = None,
+        steptype: "Optional[StepType]" = None,
+        function: "Optional[Callable]" = None,
+        cost: "Any | Callable" = 1.0,
+        output_unit: "Optional[str]" = None,
+    ) -> None:
         self.output_iri = output_iri
         self.steptype = steptype
         self.function = function
@@ -136,71 +155,99 @@ def __init__(
         self.join_mode = False  # whether to join upcoming input
         self.joined_input = {}
 
-    def add_inputs(self, inputs):
+    def add_inputs(self, inputs: dict) -> None:
         """Add input dict for an input route."""
-        assert isinstance(inputs, dict)
+        if not isinstance(inputs, dict):
+            raise TypeError("inputs must be a dict.")
         self.input_routes.append(inputs)
 
-    def add_input(self, input, name=None):
-        """Add an input (MappingStep or Value), where `name` is the name
+    def add_input(
+        self, input: "MappingStep | Value", name: "Optional[str]" = None
+    ) -> None:
+        """Add an input (MappingStep or Value), where ``name`` is the name
         assigned to the argument.
 
-        If the `join_mode` attribute is false, a new route is created with
+        If the ``join_mode`` attribute is ``False``, a new route is created with
         only one input.
 
-        If the `join_mode` attribute is true, the input is remembered, but
-        first added when join_input() is called.
+        If the ``join_mode`` attribute is ``True``, the input is remembered, but
+        first added when ``join_input()`` is called.
+
+        Arguments:
+            input: A mapping step or a value.
+            name: Name assigned to the argument.
+
         """
-        assert isinstance(input, (MappingStep, Value))
-        argname = name if name else f'arg{len(self.joined_input)+1}'
+        if not isinstance(input, (MappingStep, Value)):
+            raise TypeError("input must be either a MappingStep or a Value.")
+
+        argname = name if name else f"arg{len(self.joined_input)+1}"
         if self.join_mode:
             self.joined_input[argname] = input
         else:
             self.add_inputs({argname: input})
 
-    def join_input(self):
+    def join_input(self) -> None:
         """Join all input added with add_input() since `join_mode` was set true.
         Resets `join_mode` to false."""
         if not self.join_mode:
-            raise MappingError('Calling join_input() when join_mode is false.')
+            raise MappingError("Calling join_input() when join_mode is false.")
         self.join_mode = False
         self.add_inputs(self.joined_input)
         self.joined_input = {}
 
-    def eval(self, routeno=None, unit=None, magnitude=False, quantity=Quantity):
+    def eval(
+        self,
+        routeno: "Optional[int]" = None,
+        unit: "Optional[str]" = None,
+        magnitude: bool = False,
+        quantity: "Type[Quantity]" = Quantity,
+    ) -> "Any":
         """Returns the evaluated value of given input route number.
 
-        Args:
-            routeno: The route number to evaluate.  If None (default)
+        Arguments:
+            routeno: The route number to evaluate. If ``None`` (default)
                 the route with the lowest cost is evalueated.
-            unit: return the result in the given unit.
-                Implies `magnitude=True`.
-            magnitude: Whether to only return the magitude of the evaluated
+            unit: return the result in the given unit. Implies `magnitude=True`.
+            magnitude: Whether to only return the magnitude of the evaluated
                 value (with no unit).
-            quantity: Quantity class to use for evaluation.  Defaults to pint.
+            quantity: Quantity class to use for evaluation. Defaults to pint.
+
         """
         if routeno is None:
             (_, routeno), = self.lowest_costs(nresults=1)
         inputs, idx = self.get_inputs(routeno)
         values = get_values(inputs, idx, quantity=quantity)
+
         if self.function:
             value = self.function(**values)
         elif len(values) == 1:
             value, = values.values()
         else:
             raise TypeError(
-                f"Expected inputs to be a single argument: {values}")
+                f"Expected inputs to be a single argument: {values}"
+            )
 
         if isinstance(value, quantity) and unit:
             return value.m_as(unit)
-        elif isinstance(value, quantity) and magnitude:
+
+        if isinstance(value, quantity) and magnitude:
             return value.m
-        else:
-            return value
 
-    def get_inputs(self, routeno):
-        """Returns input and input index `(inputs, idx)` for route number
-        `routeno`."""
+        return value
+
+    def get_inputs(self, routeno: int) -> tuple[dict, int]:
+        """Returns input and input index ``(inputs, idx)`` for route number
+        ``routeno``.
+
+        Arguments:
+            routeno: The route number to return inputs for.
+
+        Returns:
+            Inputs and difference between route number and number of routes for an
+            input dictioary.
+
+        """
         n = 0
         for inputs in self.input_routes:
             n0 = n
@@ -209,23 +256,46 @@ def get_inputs(self, routeno):
                 return inputs, routeno - n0
         raise ValueError(f"routeno={routeno} exceeds number of routes")
 
-    def get_input_iris(self, routeno):
+    def get_input_iris(self, routeno: int) -> dict[str, str]:
         """Returns a dict mapping input names to iris for the given route
-        number."""
-        inputs, idx = self.get_inputs(routeno)
-        return {k: v.output_iri if isinstance(v, MappingStep) else v.iri
-                for k, v in inputs.items()}
+        number.
+
+        Arguments:
+            routeno: The route number to return a mapping for.
+
+        Returns:
+            Mapping of input names to IRIs.
+
+        """
+        inputs, _ = self.get_inputs(routeno)
+        return {
+            key: value.output_iri if isinstance(value, MappingStep) else value.iri
+            for key, value in inputs.items()
+        }
+
+    def number_of_routes(self) -> int:
+        """Total number of routes to this mapping step.
+        
+        Returns:
+            Total number of routes to this mapping step.
 
-    def number_of_routes(self):
-        """Returns total number of routes to this mapping step."""
+        """
         n = 0
         for inputs in self.input_routes:
             n += get_nroutes(inputs)
         return n
 
-    def lowest_costs(self, nresults=5):
-        """Returns a list of `(cost, routeno)` tuples with up to the `nresult`
-        lowest costs and their corresponding route numbers."""
+    def lowest_costs(self, nresults: int = 5) -> list:
+        """Returns a list of ``(cost, routeno)`` tuples with up to the ``nresults``
+        lowest costs and their corresponding route numbers.
+
+        Arguments:
+            nresults: Number of results to return.
+
+        Returns:
+            A list of ``(cost, routeno)`` tuples.
+
+        """
         result = []
         n = 0  # total number of routes
 
@@ -289,81 +359,129 @@ def lowest_costs(self, nresults=5):
         # `nresults` rows with lowest cost.
         return sorted(result)[:nresults]
 
-    def show(self, routeno=None, name=None, indent=0):
+    def show(
+        self,
+        routeno: "Optional[int]" = None,
+        name: "Optional[str]" = None,
+        indent: int = 0,
+    ) -> str:
         """Returns a string representation of the mapping routes to this step.
 
         Arguments:
-            routeno: show given route.  The default is to show all routes.
+            routeno: show given route. The default is to show all routes.
             name: Name of the last mapping step (mainly for internal use).
-            indent: How of blanks to prepend each line with (mainly for
-                internal use).
+            indent: How of blanks to prepend each line with (mainly for internal use).
+
+        Returns:
+            String representation of the mapping routes.
+
         """
-        s = []
-        ind = ' '*indent
-        s.append(ind + f'{name if name else "Step"}:')
-        s.append(ind + f'  steptype: '
-                 f'{self.steptype.name if self.steptype else None}')
-        s.append(ind + f'  output_iri: {self.output_iri}')
-        s.append(ind + f'  output_unit: {self.output_unit}')
-        s.append(ind + f'  cost: {self.cost}')
+        res = []
+        ind = " " * indent
+        res.append(ind + f"{name if name else 'Step'}:")
+        res.append(ind + f"  steptype: {self.steptype.name if self.steptype else None}")
+        res.append(ind + f"  output_iri: {self.output_iri}")
+        res.append(ind + f"  output_unit: {self.output_unit}")
+        res.append(ind + f"  cost: {self.cost}")
         if routeno is None:
-            s.append(ind + f'  routes:')
+            res.append(ind + "  routes:")
             for inputs in self.input_routes:
-                t = '\n'.join([input_.show(name=name_, indent=indent+6)
-                               for name_, input_ in inputs.items()])
-                s.append(ind + '    - ' + t[indent+6:])
+                input_repr = "\n".join(
+                    [
+                        input_.show(name=name_, indent=indent + 6)
+                        for name_, input_ in inputs.items()
+                    ]
+                )
+                res.append(ind + f"    - {input_repr[indent+6:]}")
         else:
-            s.append(ind + f'  inputs:')
+            res.append(ind + "  inputs:")
             inputs, idx = self.get_inputs(routeno)
-            t = '\n'.join([input_.show(routeno=idx, name=name_, indent=indent+6)
-                           for name_, input_ in inputs.items()])
-            s.append(ind + '    - ' + t[indent+6:])
-        return '\n'.join(s)
+            input_repr = "\n".join(
+                [
+                    input_.show(routeno=idx, name=name_, indent=indent + 6)
+                    for name_, input_ in inputs.items()
+                ]
+            )
+            res.append(ind + f"    - {input_repr[indent+6:]}")
+        return "\n".join(res)
+
+
+def get_nroutes(inputs: "dict[str, Any]") -> int:
+    """Help function returning the number of routes for an input dict.
 
+    Arguments:
+        inputs: Input dictionary.
+
+    Returns:
+        Number of routes in the ``inputs`` input dictionary.
 
-def get_nroutes(inputs):
-    """Help function returning the number of routes for an input dict."""
-    m = 1
+    """
+    nroutes = 1
     for input in inputs.values():
         if isinstance(input, MappingStep):
-            m *= input.number_of_routes()
-    return m
+            nroutes *= input.number_of_routes()
+    return nroutes
 
 
-def get_values(inputs, routeno, quantity=Quantity, magnitudes=False):
+def get_values(
+    inputs: "dict[str, Any]",
+    routeno: int,
+    quantity: "Type[Quantity]" = Quantity,
+    magnitudes: bool = False,
+) -> "dict[str, Any]":
     """Help function returning a dict mapping the input names to actual value
     of expected input unit.
 
-    There exists `get_nroutes(inputs)` routes to populate `inputs`.
-    `routeno` is the index of the specific route we will use to obtain the
-    values."""
+    There exists ``get_nroutes(inputs)`` routes to populate ``inputs``.
+    ``routeno`` is the index of the specific route we will use to obtain the
+    values.
+
+    Arguments:
+        inputs: Input dictionary.
+        routeno: Route number index.
+        quantity: A unit quantity class.
+        magnitudes: Whether to only return the magnitude of the evaluated
+            value (with no unit).
+
+    Returns:
+        A mapping between input names and values of expected input unit.
+
+    """
     values = {}
-    for k, v in inputs.items():
-        if isinstance(v, MappingStep):
-            value = v.eval(routeno=routeno, quantity=quantity)
-            values[k] = (
-                value.to(v.output_unit)
-                if v.output_unit and isinstance(v, quantity) else value
+    for key, input_value in inputs.items():
+        if isinstance(input_value, MappingStep):
+            value = input_value.eval(routeno=routeno, quantity=quantity)
+            values[key] = (
+                value.to(input_value.output_unit)
+                if input_value.output_unit and isinstance(input_value, quantity) else value
             )
+        elif isinstance(input_value, Value):
+            values[key] = quantity(input_value.value, input_value.unit)
         else:
-            values[k] = quantity(v.value, v.unit)
+            raise TypeError(
+                "Expected values in inputs to be either MappingStep or Value objects."
+            )
 
         if magnitudes:
-            values = {k: v.m if isinstance(v, quantity) else v
-                      for k, v in values.items()}
+            values = {
+                key: value.m if isinstance(value, quantity) else value
+                for key, value in values.items()
+            }
 
     return values
 
 
-def fno_mapper(triplestore):
+def fno_mapper(triplestore: "Triplestore") -> defaultdict:
     """Finds all function definitions in `triplestore` based on the function
     ontololy (FNO).
 
-    Return a dict mapping output IRIs to a list of
+    Arguments:
+        triplestore: The triplestore to investigate.
 
-        (function_iri, [input_iris, ...])
+    Returns:
+        A mapping of output IRIs to a list of ``(function_iri, [input_iris, ...])``
+        tuples.
 
-    tuples.
     """
     # Temporary dicts for fast lookup
     Dfirst = {s: o for s, o in triplestore.subject_objects(RDF.first)}
@@ -375,7 +493,7 @@ def fno_mapper(triplestore):
     for s, o in triplestore.subject_objects(FNO.returns):
         Dreturns[s].append(o)
 
-    d = defaultdict(list)
+    res = defaultdict(list)
     for func, lst in Dreturns.items():
         input_iris = []
         for exp in Dexpects.get(func, ()):
@@ -393,34 +511,39 @@ def fno_mapper(triplestore):
         for ret in lst:
             if ret in Dfirst:
                 while ret in Dfirst:
-                    d[Dfirst[ret]].append((func, input_iris))
+                    res[Dfirst[ret]].append((func, input_iris))
                     if ret not in Drest:
                         break
                     ret = Drest[ret]
             else:
                 # Support also misuse of FNO, where fno:returns refers
                 # directly to the returned individual
-                d[ret].append((func, input_iris))
+                res[ret].append((func, input_iris))
 
-    return d
+    return res
 
 
 def mapping_route(
-        target,
-        sources,
-        triplestore,
-        function_repo=None,
-        function_mappers=(fno_mapper, ),
-        default_costs={'function': 10.0, 'mapsTo': 2.0, 'instanceOf': 1.0,
-                       'subClassOf': 1.0, 'value': 0.0},
-        mapsTo=MAP.mapsTo,
-        instanceOf=DM.instanceOf,
-        subClassOf=RDFS.subClassOf,
-        #description=DCTERMS.description,
-        label=RDFS.label,
-        hasUnit=DM.hasUnit,
-        hasCost=DM.hasCost,  # TODO - add hasCost to the DM ontology
-):
+        target: str,
+        sources: dict,
+        triplestore: "Triplestore",
+        function_repo: "Optional[dict[str, Callable]]" = None,
+        function_mappers: "Sequence[Callable]" = (fno_mapper, ),
+        default_costs: dict[str, float] = {
+            "function": 10.0,
+            "mapsTo": 2.0,
+            "instanceOf": 1.0,
+            "subClassOf": 1.0,
+            "value": 0.0,
+        },
+        mapsTo: str = MAP.mapsTo,
+        instanceOf: str = DM.instanceOf,
+        subClassOf: str = RDFS.subClassOf,
+        #description: str = DCTERMS.description,
+        label: str = RDFS.label,
+        hasUnit: str = DM.hasUnit,
+        hasCost: str = DM.hasCost,  # TODO - add hasCost to the DM ontology
+) -> MappingStep:
     """Find routes of mappings from any source in `sources` to `target`.
 
     This implementation supports functions (using FnO) and subclass
@@ -432,8 +555,6 @@ def mapping_route(
         sources: Dict mapping source IRIs to source values.
         triplestore: Triplestore instance.
             It is safe to pass a generator expression too.
-
-    Additional arguments for fine-grained tuning:
         function_repo: Dict mapping function IRIs to corresponding Python
             function.  Default is to use `triplestore.function_repo`.
         function_mappers: Sequence of mapping functions that takes
@@ -456,20 +577,21 @@ def mapping_route(
             property.
 
     Returns:
-        A MappingStep instance.  This is a root of a nested tree of
+        A MappingStep instance. This is a root of a nested tree of
         MappingStep instances providing an (efficient) internal description
         of all possible mapping routes from `sources` to `target`.
+
     """
     if function_repo is None:
         function_repo = triplestore.function_repo
 
     # Create lookup tables for fast access to properties
-    # This only transverse `tiples` once
-    soMaps  = defaultdict(list)  # (s, mapsTo, o)     ==> soMaps[s]  -> [o, ..]
-    osMaps  = defaultdict(list)  # (o, mapsTo, s)     ==> osMaps[o]  -> [s, ..]
+    # This only transverse `triples` once
+    soMaps = defaultdict(list)   # (s, mapsTo, o)     ==> soMaps[s]  -> [o, ..]
+    osMaps = defaultdict(list)   # (o, mapsTo, s)     ==> osMaps[o]  -> [s, ..]
     osSubcl = defaultdict(list)  # (o, subClassOf, s) ==> osSubcl[o] -> [s, ..]
-    soInst  = dict()             # (s, instanceOf, o) ==> soInst[s]  -> o
-    osInst  = defaultdict(list)  # (o, instanceOf, s) ==> osInst[o]  -> [s, ..]
+    soInst = {}                  # (s, instanceOf, o) ==> soInst[s]  -> o
+    osInst = defaultdict(list)   # (o, instanceOf, s) ==> osInst[o]  -> [s, ..]
     for s, o in triplestore.subject_objects(mapsTo):
         soMaps[s].append(o)
         osMaps[o].append(s)
@@ -478,8 +600,9 @@ def mapping_route(
     for s, o in triplestore.subject_objects(instanceOf):
         if s in soInst:
             raise InconsistentTriplesError(
-                f'The same individual can only relate to one datamodel '
-                f'property via {instanceOf} relations.')
+                "The same individual can only relate to one datamodel "
+                f"property via {instanceOf} relations."
+            )
         soInst[s] = o
         osInst[o].append(s)
     soName = {s: o for s, o in triplestore.subject_objects(label)}
@@ -560,13 +683,19 @@ def addnode(node, steptype, stepname):
     return step
 
 
-def instance_routes(meta, instances, triplestore, allow_incomplete=False,
-                    quantity=Quantity, **kwargs):
+def instance_routes(
+    meta: str | dlite.Metadata,
+    instances: "dlite.Instance | Sequence[dlite.Instance]",
+    triplestore: "Triplestore",
+    allow_incomplete: bool = False,
+    quantity: "Type[Quantity]" = Quantity,
+    **kwargs,
+) -> dict[str, MappingStep]:
     """Find all mapping routes for populating an instance of `meta`.
 
     Arguments:
         meta: Metadata for the instance we will create.
-        instances: sequence of instances that the new intance will be
+        instances: sequence of instances that the new instance will be
             populated from.
         triplestore: Triplestore containing the mappings.
         allow_incomplete: Whether to allow not populating all properties
@@ -577,6 +706,7 @@ def instance_routes(meta, instances, triplestore, allow_incomplete=False,
 
     Returns:
         A dict mapping property names to a MappingStep instance.
+
     """
     if isinstance(meta, str):
         meta = dlite.get_instance(meta)
@@ -585,9 +715,9 @@ def instance_routes(meta, instances, triplestore, allow_incomplete=False,
 
     sources = {}
     for inst in instances:
-        props = {p.name: p for p in inst.meta['properties']}
-        for k, v in inst.properties.items():
-            sources[f'{inst.meta.uri}#{k}'] = quantity(v, props[k].unit)
+        props = {prop.name: prop for prop in inst.meta['properties']}
+        for key, value in inst.properties.items():
+            sources[f'{inst.meta.uri}#{key}'] = quantity(value, props[key].unit)
 
     routes = {}
     for prop in meta['properties']:
@@ -599,14 +729,19 @@ def instance_routes(meta, instances, triplestore, allow_incomplete=False,
                 continue
             raise
         if not allow_incomplete and not route.number_of_routes():
-            raise InsufficientMappingError(f'no mappings for {target}')
+            raise InsufficientMappingError(f'No mappings for {target}')
         routes[prop.name] = route
 
     return routes
 
 
-def instantiate_from_routes(meta, routes, routedict=None, id=None,
-                            quantity=Quantity):
+def instantiate_from_routes(
+    meta: str | dlite.Metadata,
+    routes: dict[str, MappingStep],
+    routedict: dict[str, int] = None,
+    id: "Optional[str]" = None,
+    quantity: "Type[Quantity]" = Quantity,
+) -> dlite.Instance:
     """Create a new instance of `meta` from selected mapping route returned
     by instance_routes().
 
@@ -623,31 +758,41 @@ def instantiate_from_routes(meta, routes, routedict=None, id=None,
 
     Returns:
         New instance.
+
     """
     if isinstance(meta, str):
         meta = dlite.get_instance(meta)
 
-    if routedict is None:
-        routedict = {}
+    routedict = routedict or {}
 
     values = {}
     for prop in meta['properties']:
         if prop.name in routes:
             step = routes[prop.name]
-            values[prop.name] = step.eval(routeno=routedict.get(prop.name),
-                                          unit=prop.unit,
-                                          quantity=quantity)
+            values[prop.name] = step.eval(
+                routeno=routedict.get(prop.name),
+                unit=prop.unit,
+                quantity=quantity,
+            )
     dims = infer_dimensions(meta, values)
     inst = meta(dims=dims, id=id)
 
-    for k, v in routes.items():
-        inst[k] = v.eval(magnitude=True, unit=meta.getprop(k).unit)
+    for key, value in routes.items():
+        inst[key] = value.eval(magnitude=True, unit=meta.getprop(key).unit)
 
     return inst
 
 
-def instantiate(meta, instances, triplestore, routedict=None, id=None,
-                allow_incomplete=False, quantity=Quantity, **kwargs):
+def instantiate(
+    meta: str | dlite.Metadata,
+    instances: "dlite.Instance | Sequence[dlite.Instance]",
+    triplestore: "Triplestore",
+    routedict: "Optional[dict[str, int]]" = None,
+    id: "Optional[str]" = None,
+    allow_incomplete: bool = False,
+    quantity: "Type[Quantity]" = Quantity,
+    **kwargs,
+) -> dlite.Instance:
     """Create a new instance of `meta` populated with the selected mapping
     routes.
 
@@ -670,25 +815,12 @@ def instantiate(meta, instances, triplestore, routedict=None, id=None,
         quantity: Class implementing quantities with units.  Defaults to
             pint.Quantity.
 
-    Keyword arguments (passed to mapping_route()):
-        function_repo: Dict mapping function IRIs to corresponding Python
-            function.  Default is to use `triplestore.function_repo`.
-        function_mappers: Sequence of mapping functions that takes
-            `triplestore` as argument and return a dict mapping output IRIs
-            to a list of `(function_iri, [input_iris, ...])` tuples.
-        mapsTo: IRI of 'mapsTo' in `triplestore`.
-        instanceOf: IRI of 'instanceOf' in `triplestore`.
-        subClassOf: IRI of 'subClassOf' in `triplestore`.  Set it to None if
-            subclasses should not be considered.
-        label: IRI of 'label' in `triplestore`.  Used for naming function
-            input parameters.  The default is to use rdfs:label.
-        hasUnit: IRI of 'hasUnit' in `triplestore`.
-        hasCost: IRI of 'hasCost' in `triplestore`.
-
     Returns:
         New instance.
+
     """
-    meta = dlite.get_instance(meta)
+    if isinstance(meta, str):
+        meta = dlite.get_instance(meta)
 
     routes = instance_routes(
         meta=meta,
@@ -696,19 +828,26 @@ def instantiate(meta, instances, triplestore, routedict=None, id=None,
         triplestore=triplestore,
         allow_incomplete=allow_incomplete,
         quantity=quantity,
-        **kwargs
+        **kwargs,
     )
     return instantiate_from_routes(
         meta=meta,
         routes=routes,
         routedict=routedict,
         id=id,
-        quantity=quantity
+        quantity=quantity,
     )
 
 
-def instantiate_all(meta, instances, triplestore, routedict=None,
-                allow_incomplete=False, quantity=Quantity, **kwargs):
+def instantiate_all(
+    meta: str | dlite.Metadata,
+    instances: "dlite.Instance | Sequence[dlite.Instance]",
+    triplestore: "Triplestore",
+    routedict: "Optional[dict[str, int]]" = None,
+    allow_incomplete: bool = False,
+    quantity: "Type[Quantity]" = Quantity,
+    **kwargs,
+) -> "Generator[dlite.Instance, None, None]":
     """Like instantiate(), but returns a generator over all possible instances.
 
     The number of instances iterated over is the product of the number
@@ -729,25 +868,12 @@ def instantiate_all(meta, instances, triplestore, routedict=None,
         quantity: Class implementing quantities with units.  Defaults to
             pint.Quantity.
 
-    Keyword arguments (passed to mapping_route()):
-        function_repo: Dict mapping function IRIs to corresponding Python
-            function.  Default is to use `triplestore.function_repo`.
-        function_mappers: Sequence of mapping functions that takes
-            `triplestore` as argument and return a dict mapping output IRIs
-            to a list of `(function_iri, [input_iris, ...])` tuples.
-        mapsTo: IRI of 'mapsTo' in `triplestore`.
-        instanceOf: IRI of 'instanceOf' in `triplestore`.
-        subClassOf: IRI of 'subClassOf' in `triplestore`.  Set it to None if
-            subclasses should not be considered.
-        label: IRI of 'label' in `triplestore`.  Used for naming function
-            input parameters.  The default is to use rdfs:label.
-        hasUnit: IRI of 'hasUnit' in `triplestore`.
-        hasCost: IRI of 'hasCost' in `triplestore`.
+    Yields:
+        New instances.
 
-    Returns:
-        Generator over new instances.
     """
-    meta = dlite.get_instance(meta)
+    if isinstance(meta, str):
+        meta = dlite.get_instance(meta)
 
     routes = instance_routes(
         meta=meta,
@@ -758,10 +884,9 @@ def instantiate_all(meta, instances, triplestore, routedict=None,
         **kwargs
     )
 
-    property_names = [p.name for p in meta.properties['properties']]
-    nprops = len(property_names)
+    property_names = [prop.name for prop in meta.properties['properties']]
 
-    def routedicts(n):
+    def routedicts(n: int) -> "Generator[dict[str, int], None, None]":
         """Recursive help function returning an iterator over all possible
         routedicts.  `n` is the index of the current property."""
         if n < 0:
@@ -778,20 +903,18 @@ def routedicts(n):
                     outer[name] = inner
                     yield outer
 
-    for rd in routedicts(len(property_names) - 1):
+    for route_dict in routedicts(len(property_names) - 1):
         yield instantiate_from_routes(
             meta=meta,
             routes=routes,
-            routedict=rd,
+            routedict=route_dict,
             quantity=quantity
         )
 
 
-
-
 # ------------- Old implementation -----------------
 
-def unitconvert_pint(dest_unit, value, unit):
+def unitconvert_pint(dest_unit: "Any", value: "Any", unit: "Any") -> "Any":
     """Returns `value` converted to `dest_unit`.
 
     A unitconvert function based on Pint.  Alternative functions
@@ -801,6 +924,7 @@ def unitconvert_pint(dest_unit, value, unit):
         dest_unit: Destination unit that `value` should be converted to.
         value: Source value.
         unit: The unit of the source value.
+
     """
     import pint
     ureg = pint.UnitRegistry()
@@ -812,7 +936,9 @@ def unitconvert_pint(dest_unit, value, unit):
 unitconvert = unitconvert_pint
 
 
-def match_factory(triples, match_first=False):
+def match_factory(
+    triples: "Sequence[tuple[str, str, str]]", match_first: bool = False
+) -> "Callable[[Optional[str], Optional[str], Optional[str]], Generator[tuple[str, str, str], None, None]]":
     """A factory function that returns a match functions for `triples`.
 
     If `match_first` is false, the returned match function will return
@@ -825,39 +951,47 @@ def match_factory(triples, match_first=False):
           only the first match.
 
     Example:
-    >>> triples = [
-    ...     (':mamal', 'rdfs:subClassOf', ':animal'),
-    ...     (':cat', 'rdfs:subClassOf', ':mamal'),
-    ...     (':mouse', 'rdfs:subClassOf', ':mamal'),
-    ...     (':cat', ':eats', ':mouse'),
-    ... ]
-    >>> match = match_factory(triples)
-    >>> match_first = match_factory(triples, only_first=True)
-    >>> list(match(':cat'))
-    [(':cat', 'rdfs:subClassOf', ':mamal'),
-     (':cat', ':eats', ':mouse')]
-    >>> match_first(':cat', None, None)
-    (':cat', 'rdfs:subClassOf', ':mamal')
+        >>> triples = [
+        ...     (':mamal', 'rdfs:subClassOf', ':animal'),
+        ...     (':cat', 'rdfs:subClassOf', ':mamal'),
+        ...     (':mouse', 'rdfs:subClassOf', ':mamal'),
+        ...     (':cat', ':eats', ':mouse'),
+        ... ]
+        >>> match = match_factory(triples)
+        >>> match_first = match_factory(triples, only_first=True)
+        >>> list(match(':cat'))
+        [(':cat', 'rdfs:subClassOf', ':mamal'),
+        (':cat', ':eats', ':mouse')]
+        >>> match_first(':cat', None, None)
+        (':cat', 'rdfs:subClassOf', ':mamal')
+
     """
-    def match(s=None, p=None, o=None):
+
+    def match(
+        s: "Optional[str]" = None, p: "Optional[str]" = None, o: "Optional[str]" = None
+    ) -> "Generator[tuple[str, str, str], None, None]":
         """Returns generator over all triples that matches (s, p, o)."""
-        return (t for t in triples if
-                (s is None or t[0] == s) and
-                (p is None or t[1] == p) and
-                (o is None or t[2] == o))
+        return (
+            triple for triple in triples
+            if (
+                (s is None or triple[0] == s) and
+                (p is None or triple[1] == p) and
+                (o is None or triple[2] == o)
+            )
+        )
+
     if match_first:
         return lambda s=None, p=None, o=None: next(
             iter(match(s, p, o) or ()), (None, None, None))
-    else:
-        return match
-
 
+    return match
 
 
-
-def assign_dimensions(dims: Dict,
-                      inst: dlite.Instance,
-                      propname: str):
+def assign_dimensions(
+    dims: dict[str, int],
+    inst: dlite.Instance,
+    propname: str,
+) -> None:
     """Assign dimensions from property assignment.
 
     Args:
@@ -865,26 +999,35 @@ def assign_dimensions(dims: Dict,
           should be assigned.  Only values that are None will be assigned.
         inst: Source instance.
         propname: Source property name.
+
     """
-    lst = [p.dims for p in inst.meta['properties'] if p.name == propname]
+    lst = [prop.dims for prop in inst.meta['properties'] if prop.name == propname]
     if not lst:
-        raise MappingError('Unexpected property name: {src_propname}')
+        raise MappingError(f"Unexpected property name: {propname}")
+
     src_dims, = lst
     for dim in src_dims:
         if dim not in dims:
-            raise InconsistentDimensionError('Unexpected dimension: {dim}')
+            raise InconsistentDimensionError(f"Unexpected dimension: {dim}")
+
         if dims[dim] is None:
             dims[dim] = inst.dimensions[dim]
         elif dims[dim] != inst.dimensions[dim]:
             raise InconsistentDimensionError(
-                f'Trying to assign dimension {dim} of {src_inst.meta.uri} '
-                f'to {src_inst.dimensions[dim]}, but it is already assigned '
-                f'to {dims[dim]}')
+                f"Trying to assign dimension {dim} of {inst.meta.uri} "
+                f"to {inst.dimensions[dim]}, but it is already assigned "
+                f"to {dims[dim]}"
+            )
 
 
-def make_instance(meta, instances, mappings=(), strict=True,
-                  allow_incomplete=False, unitconvert=unitconvert_pint,
-                  mapsTo=':mapsTo'):
+def make_instance(
+    meta: dlite.Metadata,
+    instances: "dlite.Instance | Sequence[dlite.Instance]",
+    mappings: "Sequence[tuple[str, str, str]]" = (),
+    strict: bool = True,
+    allow_incomplete: bool = False,
+    mapsTo: str = ':mapsTo',
+) -> dlite.Instance:
     """Create an instance of `meta` using data found in `*instances`.
 
     Args:
@@ -896,13 +1039,6 @@ def make_instance(meta, instances, mappings=(), strict=True,
           with the same name.
         allow_incomplete: Whether to allow not populating all properties
           of the returned instance.
-        unitconvert: A callable that converts between units.  It has
-          prototype
-
-              unitconvert(dest_unit, value, unit)
-
-          and should return `value` (in units `unit`) converted to
-          `dest_unit`.
         mapsTo: How the 'mapsTo' predicate is written in `mappings`.
 
     Returns:
@@ -917,11 +1053,12 @@ def make_instance(meta, instances, mappings=(), strict=True,
           returned instance is assigned to more than one value.
 
     Todo:
+
     - Consider that mapsTo is a transitive relation.
     - Use EMMOntoPy to also account for rdfs:subClassOf relations.
     - Consider the possibility to assign values via the `mappings`
-      triples.  Do we really want that?  May be useful, but will add
-      a lot of complexity.  Should we have different relations for
+      triples. Do we really want that?  May be useful, but will add
+      a lot of complexity. Should we have different relations for
       default values and values that will overwrite what is provided
       from a matching input instance?
     - Add `mapsToPythonExpression` subproperty of `mapsTo` to allow
@@ -930,6 +1067,7 @@ def make_instance(meta, instances, mappings=(), strict=True,
       Use the ast module for safe evaluation to ensure that this feature
       cannot be misused for code injection.
     - Add a function that visualise the possible mapping paths.
+
     """
     warnings.warn(
         "make_instance() is deprecated. Use instantiate() instead.",
@@ -941,7 +1079,7 @@ def make_instance(meta, instances, mappings=(), strict=True,
     if isinstance(instances, dlite.Instance):
         instances = [instances]
 
-    dims = {d.name: None for d in meta['dimensions']}
+    dims = {dim.name: None for dim in meta['dimensions']}
     props = {}
 
     for prop in meta['properties']:
@@ -950,15 +1088,16 @@ def make_instance(meta, instances, mappings=(), strict=True,
             for inst in instances:
                 for prop2 in inst.meta['properties']:
                     prop2_uri = f'{inst.meta.uri}#{prop2.name}'
-                    for _, _, o2 in match(prop2_uri, mapsTo, o):
+                    for _ in match(prop2_uri, mapsTo, o):
                         value = inst[prop2.name]
                         if prop.name not in props:
                             assign_dimensions(dims, inst, prop2.name)
                             props[prop.name] = value
                         elif props[prop.name] != value:
                             raise AmbiguousMappingError(
-                                f'"{prop.name}" maps to both '
-                                f'"{props[prop.name]}" and "{value}"')
+                                f"{prop.name!r} maps to both "
+                                f"{props[prop.name]!r} and {value!r}"
+                            )
 
         if prop.name not in props and not strict:
             for inst in instances:
@@ -969,20 +1108,23 @@ def make_instance(meta, instances, mappings=(), strict=True,
                         props[prop.name] = value
                     elif props[prop.name] != value:
                         raise AmbiguousMappingError(
-                            f'"{prop.name}" assigned to both '
-                            f'"{props[prop.name]}" and "{value}"')
+                            f"{prop.name!r} assigned to both "
+                            f"{props[prop.name]!r} and {value!r}"
+                        )
 
         if not allow_incomplete and prop.name not in props:
             raise InsufficientMappingError(
-                f'no mapping for assigning property "{prop.name}" '
-                f'in {meta.uri}')
+                f"no mapping for assigning property {prop.name!r} "
+                f"in {meta.uri}"
+            )
 
     if None in dims:
-        dimname = [k for k, v in dims.items() if v is None][0]
+        dimname = [name for name, dim in dims.items() if dim is None][0]
         raise InsufficientMappingError(
-            f'dimension "{dimname}" is not assigned')
+            f"dimension {dimname!r} is not assigned"
+        )
 
     inst = meta(list(dims.values()))
-    for k, v in props.items():
-        inst[k] = v
+    for key, value in props.items():
+        inst[key] = value
     return inst
diff --git a/bindings/python/triplestore/__init__.py b/bindings/python/triplestore/__init__.py
index 334acf5be..cb99656fe 100644
--- a/bindings/python/triplestore/__init__.py
+++ b/bindings/python/triplestore/__init__.py
@@ -4,39 +4,6 @@
 See the README.md file for a description for how to use this package.
 """
 import warnings
-from typing import TYPE_CHECKING
-
-from .triplestore import (
-    Literal, Namespace, Triplestore,
-    en,
-    XML, RDF, RDFS, XSD, OWL, SKOS, DC, DCTERMS, FOAF, DOAP, FNO, EMMO, MAP, DM,
-)
-
-if TYPE_CHECKING:  # pragma: no cover
-    from .triplestore import Triple
-
-
-__all__ = (
-    "Literal",
-    "Namespace",
-    "Triplestore",
-    "en",
-    "XML",
-    "RDF",
-    "RDFS",
-    "XSD",
-    "OWL",
-    "SKOS",
-    "DC",
-    "DCTERMS",
-    "FOAF",
-    "DOAP",
-    "FNO",
-    "EMMO",
-    "MAP",
-    "DM",
-)
-
 
 warnings.warn(
     "dlite.triplestore is deprecated.\n"
diff --git a/bindings/python/triplestore/backends/__init__.py b/bindings/python/triplestore/backends/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/bindings/python/triplestore/backends/ontopy.py b/bindings/python/triplestore/backends/ontopy.py
deleted file mode 100644
index 60e8b1855..000000000
--- a/bindings/python/triplestore/backends/ontopy.py
+++ /dev/null
@@ -1,208 +0,0 @@
-import io
-import os
-import tempfile
-import warnings
-from typing import TYPE_CHECKING
-
-from ontopy.ontology import get_ontology, Ontology, _unabbreviate
-
-
-from dlite.triplestore import Literal
-
-if TYPE_CHECKING:  # pragma: no cover
-    from collections.abc import Sequence
-    from typing import Generator
-
-    from dlite.triplestore import Triple
-
-
-class OntopyStrategy:
-    """Triplestore strategy for EMMOntoPy.
-
-    Arguments:
-        base_iri: The base iri of the ontology.
-        onto: Ontology to initiate the triplestore from.  Defaults to an new
-            ontology with the given `base_iri`.
-        load: Whether to load the ontology.
-        kwargs: Keyword arguments passed to the ontology load() method.
-
-    Either the `base_iri` or `onto` argument must be provided.
-    """
-    def __init__(
-            self,
-            base_iri: str = None,
-            onto: Ontology = None,
-            load: bool = False,
-            kwargs: dict = {}
-    ):
-        if onto is None:
-            if base_iri is None:
-                raise TypeError("either `base_iri` or `onto` must be provided")
-            self.onto = get_ontology(base_iri)
-        elif isinstance(onto, Ontology):
-            self.onto = onto
-        else:
-            raise TypeError("`onto` must be either an ontology or None")
-
-        if load:
-            self.onto.load(**kwargs)
-
-    def triples(self, triple: "Triple") -> "Generator":
-        """Returns a generator over matching triples."""
-
-        def to_literal(o, d):
-            """Returns a literal from (o, d)."""
-            if isinstance(d, str) and d.startswith("@"):
-                lang, datatype = d[1:], None
-            else:
-                lang, datatype = None, d
-            return Literal(o, lang=lang, datatype=datatype)
-
-        s, p, o = triple
-        abb = (
-            None if s is None else self.onto._abbreviate(s),
-            None if p is None else self.onto._abbreviate(p),
-            None if o is None else self.onto._abbreviate(o),
-        )
-        for s, p, o in self.onto._get_obj_triples_spo_spo(*abb):
-            yield (
-                _unabbreviate(self.onto, s),
-                _unabbreviate(self.onto, p),
-                _unabbreviate(self.onto, o),
-            )
-        for s, p, o, d in self.onto._get_data_triples_spod_spod(*abb, d=''):
-            yield (
-                _unabbreviate(self.onto, s),
-                _unabbreviate(self.onto, p),
-                to_literal(o, d),
-            )
-
-    def add_triples(self, triples: "Sequence[Triple]"):
-        """Add a sequence of triples."""
-        for s, p, o in triples:
-            if isinstance(o, Literal):
-                if o.lang:
-                    d = f"@{o.lang}"
-                elif o.datatype:
-                    d = f"^^{o.datatype}"
-                else:
-                    d = 0
-                self.onto._add_data_triple_spod(
-                    self.onto._abbreviate(s),
-                    self.onto._abbreviate(p),
-                    self.onto._abbreviate(o),
-                    d,
-                )
-            else:
-                self.onto._add_obj_triple_spo(
-                    self.onto._abbreviate(s),
-                    self.onto._abbreviate(p),
-                    self.onto._abbreviate(o),
-                )
-
-    def remove(self, triple: "Triple"):
-        """Remove all matching triples from the backend."""
-        s, p, o = triple
-        to_remove = list(self.onto._get_triples_spod_spod(
-            self.onto._abbreviate(s) if s is not None else None,
-            self.onto._abbreviate(p) if s is not None else None,
-            self.onto._abbreviate(o) if s is not None else None,
-        ))
-        for s, p, o, d in to_remove:
-            if d:
-                self.onto._del_data_triple_spod(s, p, o, d)
-            else:
-                self.onto._del_obj_triple_spo(s, p, o)
-
-    # Optional methods
-    def parse(self, source=None, location=None, data=None, format=None,
-              encoding=None, **kwargs):
-        """Parse source and add the resulting triples to triplestore.
-
-        The source is specified using one of `source`, `location` or `data`.
-
-        Parameters:
-            source: File-like object or file name.
-            location: String with relative or absolute URL to source.
-            data: String containing the data to be parsed.
-            format: Needed if format can not be inferred from source.
-            encoding: Encoding argument to io.open().
-            kwargs: Additional keyword arguments passed to Ontology.load().
-        """
-        if sum(arg is not None for arg in (source, location, data)) != 1:
-            raise ValueError(
-                "one (and only one) of `source`, `location` and `data` "
-                "should be provided")
-
-        if source:
-            self.onto.load(filename=source, format=format, **kwargs)
-        elif location:
-            self.onto.load(filename=location, format=format, **kwargs)
-        elif data:
-            #s = io.StringIO(data)
-            #self.onto.load(filename=s, format=format, **kwargs)
-
-            # Could have been done much nicer if it hasn't been for Windows
-            filename = None
-            try:
-                kw = {"delete": False}
-                if isinstance(data, str):
-                    kw.update(mode="w+t", encoding=encoding)
-                with tempfile.NamedTemporaryFile(**kw) as f:
-                    f.write(data)
-                    filename = f.name
-                self.onto.load(filename=filename, format=format, **kwargs)
-            finally:
-                if filename:
-                    os.remove(filename)
-
-        else:
-            raise ValueError(
-                "either `source`, `location` or `data` must be given"
-            )
-
-    def serialize(self, destination=None, format='turtle', **kwargs):
-        """Serialise to destination.
-
-        Parameters:
-            destination: File name or object to write to.  If None, the
-                serialisation is returned.
-            format: Format to serialise as.  Supported formats, depends on
-                the backend.
-            kwargs: Passed to the Ontology.save() method.
-
-        Returns:
-            Serialised string if `destination` is None.
-        """
-        if destination:
-            self.onto.save(destination, format=format, **kwargs)
-        else:
-            # Clumsy implementation due to Windows file locking...
-            filename = None
-            try:
-                with tempfile.NamedTemporaryFile(delete=False) as f:
-                    filename = f.name
-                    self.onto.save(filename, format=format, **kwargs)
-                with open(filename, 'rt') as f:
-                    return f.read()
-            finally:
-                if filename:
-                    os.remove(filename)
-
-    def query(self, query_object, native=True, **kwargs):
-        """SPARQL query."""
-        if native:
-            res = self.onto.world.sparql(query_object)
-        else:
-            graph = self.onto.world.as_rdflib_graph()
-            res = graph.query(query_object, **kwargs)
-        # TODO: Convert result to expected type
-        return res
-
-    def update(self, update_object, native=True, **kwargs):
-        """Update triplestore with SPARQL."""
-        if native:
-            self.onto.world.sparql(update_object)
-        else:
-            graph = self.onto.world.as_rdflib_graph()
-            graph.update(update_object, **kwargs)
diff --git a/bindings/python/triplestore/backends/rdflib.py b/bindings/python/triplestore/backends/rdflib.py
deleted file mode 100644
index 5dffc67f8..000000000
--- a/bindings/python/triplestore/backends/rdflib.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import warnings
-from typing import TYPE_CHECKING
-
-import rdflib
-from rdflib import BNode, Graph, URIRef
-
-from dlite.triplestore import Literal
-
-if TYPE_CHECKING:  # pragma: no cover
-    from collections.abc import Sequence
-    from typing import Generator
-
-    from dlite.triplestore import Triple
-
-
-def asuri(v):
-    """Help function converting a spo-value to proper rdflib type."""
-    if v is None:
-        return None
-    if isinstance(v, Literal):
-        return rdflib.Literal(v.value, lang=v.lang, datatype=v.datatype)
-    if v.startswith("_:"):
-        return BNode(v)
-    return URIRef(v)
-
-
-def astriple(t):
-    """Help function converting a triple to rdflib triple."""
-    s, p, o = t
-    return asuri(s), asuri(p), asuri(o)
-
-
-class RdflibStrategy:
-    """Triplestore strategy for rdflib."""
-
-    def __init__(self, base_iri):
-        self.graph = Graph()
-
-    def triples(self, triple: "Triple") -> "Generator":
-        """Returns a generator over matching triples."""
-        for s, p, o in self.graph.triples(astriple(triple)):
-            yield (str(s), str(p),
-                   Literal(o.value, lang=o.language, datatype=o.datatype)
-                   if isinstance(o, rdflib.Literal) else str(o))
-
-    def add_triples(self, triples: "Sequence[Triple]"):
-        """Add a sequence of triples."""
-        for t in triples:
-            self.graph.add(astriple(t))
-
-    def remove(self, triple: "Triple"):
-        """Remove all matching triples from the backend."""
-        self.graph.remove(astriple(triple))
-
-    # Optional methods
-    def parse(self, source=None, location=None, data=None, format=None,
-              **kwargs):
-        """Parse source and add the resulting triples to triplestore.
-
-        The source is specified using one of `source`, `location` or `data`.
-
-        Parameters:
-            source: File-like object or file name.
-            location: String with relative or absolute URL to source.
-            data: String containing the data to be parsed.
-            format: Needed if format can not be inferred from source.
-            kwargs: Additional less used keyword arguments.
-                See https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#rdflib.Graph.parse
-        """
-        self.graph.parse(source=source, location=location, data=data,
-                         format=format, **kwargs)
-
-    def serialize(self, destination=None, format='turtle', **kwargs):
-        """Serialise to destination.
-
-        Parameters:
-            destination: File name or object to write to.  If None, the
-                serialisation is returned.
-            format: Format to serialise as.  Supported formats, depends on
-                the backend.
-            kwargs: Passed to the rdflib.Graph.serialize() method.
-                See https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#rdflib.Graph.serialize
-
-        Returns:
-            Serialised string if `destination` is None.
-        """
-        s = self.graph.serialize(destination=destination, format=format,
-                                 **kwargs)
-        if destination is None:
-            # Depending on the version of rdflib the return value of
-            # graph.serialize() man either be a string or a bytes object...
-            return s if isinstance(s, str) else s.decode()
-
-    def query(self, query_object, **kwargs):
-        """SPARQL query.
-
-        Parameters:
-            query_object: String with the SPARQL query.
-            kwargs: Keyword arguments passed to rdflib.Graph.query().
-
-        Returns:
-            List of tuples of IRIs for each matching row.
-        """
-        rows = self.graph.query(query_object=query_object, **kwargs)
-        return [tuple(str(v) for v in row) for row in rows]
-
-    def update(self, update_object, **kwargs):
-        """Update triplestore with SPARQL."""
-        return self.graph.update(update_object=update_object, **kwargs)
-
-    def bind(self, prefix: str, namespace: str):
-        """Bind prefix to namespace.
-
-        Should only be defined if the backend supports namespaces.
-        Called by triplestore.bind().
-        """
-        if namespace:
-            self.graph.bind(prefix, namespace, replace=True)
-        else:
-            warnings.warn(
-                "rdflib does not support removing namespace prefixes")
-
-    def namespaces(self) -> dict:
-        """Returns a dict mapping prefixes to namespaces.
-
-        Should only be defined if the backend supports namespaces.
-        Used by triplestore.parse() to get prefixes after reading
-        triples from an external source.
-        """
-        return {prefix: str(ns) for prefix, ns in self.graph.namespaces()}
diff --git a/bindings/python/triplestore/interface.py b/bindings/python/triplestore/interface.py
deleted file mode 100644
index c21f3973f..000000000
--- a/bindings/python/triplestore/interface.py
+++ /dev/null
@@ -1,102 +0,0 @@
-"""Provides the ITriplestore protocol class, that documents the interface
-of the triplestore backends."""
-from typing import TYPE_CHECKING, Protocol
-
-if TYPE_CHECKING:  # pragma: no cover
-    from collections.abc import Sequence
-    from typing import Generator
-
-    from dlite.triplestore import Triple
-
-
-class ITriplestore(Protocol):
-    '''Interface for triplestore backends.
-
-    In addition to the methods specified by this interface, a backend
-    may also implement the following optional methods:
-
-    ```python
-
-    def __init__(self, base_iri=None, **kwargs)
-
-    def parse(self, source=None, location=None, data=None, format=None,
-              **kwargs):
-        """Parse source and add the resulting triples to triplestore.
-
-        The source is specified using one of `source`, `location` or `data`.
-
-        Parameters:
-            source: File-like object or file name.
-            location: String with relative or absolute URL to source.
-            data: String containing the data to be parsed.
-            format: Needed if format can not be inferred from source.
-            kwargs: Additional backend-specific parameters controlling
-                the parsing.
-        """
-
-    def serialize(self, destination=None, format='xml', **kwargs)
-        """Serialise to destination.
-
-        Parameters:
-            destination: File name or object to write to.  If None, the
-                serialisation is returned.
-            format: Format to serialise as.  Supported formats, depends on
-                the backend.
-            kwargs: Additional backend-specific parameters controlling
-                the serialisation.
-
-        Returns:
-            Serialised string if `destination` is None.
-        """
-
-    def query(self, query_object, **kwargs)
-        """SPARQL query.
-
-        Parameters:
-            query_object: String with the SPARQL query.
-            kwargs: Keyword arguments passed to rdflib.Graph.query().
-
-        Returns:
-            List of tuples of IRIs for each matching row.
-
-        Note:
-            This method is intended for SELECT queries.  Use
-            the update() method for INSERT and DELETE  queries.
-        """
-
-    def update(self, update_object, **kwargs)
-        """Update triplestore with SPARQL.
-
-        Parameters:
-            query_object: String with the SPARQL query.
-            kwargs: Keyword arguments passed to rdflib.Graph.query().
-
-        Note:
-            This method is intended for INSERT and DELETE queries.  Use
-            the query() method for SELECT queries.
-        """
-
-    def bind(self, prefix: str, namespace: str)
-        """Bind prefix to namespace.
-
-        Should only be defined if the backend supports namespaces.
-        """
-
-    def namespaces(self) -> dict
-        """Returns a dict mapping prefixes to namespaces.
-
-        Should only be defined if the backend supports namespaces.
-        Used by triplestore.parse() to get prefixes after reading
-        triples from an external source.
-        """
-    ```
-    '''
-
-    def triples(self, triple: "Triple") -> "Generator":
-        """Returns a generator over matching triples."""
-
-    def add_triples(self, triples: "Sequence[Triple]"):
-        """Add a sequence of triples."""
-
-    def remove(self, triple: "Triple"):
-        """Remove all matching triples from the backend."""
diff --git a/bindings/python/triplestore/triplestore.py b/bindings/python/triplestore/triplestore.py
deleted file mode 100644
index d925b6c7b..000000000
--- a/bindings/python/triplestore/triplestore.py
+++ /dev/null
@@ -1,776 +0,0 @@
-'''A module encapsulating different triplestores using the strategy design
-pattern.
-
-See https://github.com/SINTEF/dlite/tree/master/bindings/python/triplestore
-for an introduction.
-
-This module has no dependencies outside the standard library, but the
-triplestore backends may have.
-'''
-from __future__ import annotations  # Support Python 3.7 (PEP 585)
-
-import hashlib
-import inspect
-import re
-import warnings
-from collections.abc import Sequence
-from datetime import datetime
-from importlib import import_module
-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:  # pragma: no cover
-    from collections.abc import Mapping
-    from typing import Callable, Generator, Tuple, Union
-
-    Triple = Tuple[Union[str, None], Union[str, None], Union[str, None]]
-
-
-# Regular expression matching a prefixed IRI
-_MATCH_PREFIXED_IRI = re.compile(r"^([a-z]+):([^/]{2}.*)$")
-
-
-class TriplestoreError(Exception):
-    """Base exception for triplestore errors."""
-
-class UniquenessError(TriplestoreError):
-    """More than one matching triple."""
-
-class NamespaceError(TriplestoreError):
-    """Namespace error."""
-
-class NoSuchIRIError(NamespaceError):
-    """Namespace has no such IRI."""
-
-
-class Namespace:
-    """Represent a namespace.
-
-    Arguments:
-        iri: IRI of namespace to represent.
-        label_annotations: Sequence of label annotations. If given, check
-            the underlying ontology during attribute access if the name
-            correspond to a label. The label annotations should be ordered
-            from highest to lowest precedense.
-            If True is provided, `label_annotations` is set to
-            ``(SKOS.prefLabel, RDF.label, SKOS.altLabel)``.
-        check: Whether to check underlying ontology if the IRI exists during
-            attribute access.  If true, NoSuchIRIError will be raised if the
-            IRI does not exist in this namespace.
-        cachemode: Should be one of:
-              - Namespace.NO_CACHE: Turn off caching.
-              - Namespace.USE_CACHE: Cache attributes as they are looked up.
-              - Namespace.ONLY_CACHE: Cache all names at initialisation time.
-                Do not access the triplestore after that.
-            Default is `NO_CACHE` if neither `label_annotations` or `check`
-            is given, otherwise `USE_CACHE`.
-        triplestore: Use this triplestore for label lookup and checking.
-            If not given, and either `label_annotations` or `check` are
-            enabled, a new rdflib triplestore will be created.
-        triplestore_url: Alternative URL to use for loading the underlying
-            ontology if `triplestore` is not given.  Defaults to `iri`.
-    """
-    NO_CACHE = 0
-    USE_CACHE = 1
-    ONLY_CACHE = 2
-
-    __slots__ = (
-        "_iri", "_label_annotations", "_check", "_cache", "_triplestore",
-    )
-
-    def __init__(self, iri, label_annotations=(), check=False, cachemode=-1,
-                 triplestore=None, triplestore_url=None):
-        if label_annotations is True:
-            label_annotations = (SKOS.prefLabel, RDF.label, SKOS.altLabel)
-
-        self._iri = str(iri)
-        self._label_annotations = tuple(label_annotations)
-        self._check = bool(check)
-
-        need_triplestore = True if check or label_annotations else False
-        if cachemode == -1:
-            cachemode = (
-                Namespace.ONLY_CACHE if need_triplestore else Namespace.NO_CACHE
-            )
-
-        if need_triplestore and triplestore is None:
-            url = triplestore_url if triplestore_url else iri
-            triplestore = Triplestore("rdflib", base_iri=iri)
-            triplestore.parse(url)
-
-        self._cache = {} if cachemode != Namespace.NO_CACHE else None
-        #
-        # FIXME:
-        # Change this to only assigning the triplestore if cachemode is
-        # ONLY_CACHE when we figure out a good way to pre-populate the
-        # cache with IRIs from the triplestore.
-        #
-        #self._triplestore = (
-        #    triplestore if cachemode != Namespace.ONLY_CACHE else None
-        #)
-        self._triplestore = triplestore if need_triplestore else None
-
-        if cachemode != Namespace.NO_CACHE:
-            self._update_cache(triplestore)
-
-    def _update_cache(self, triplestore=None):
-        """Update the internal cache from `triplestore`."""
-        if not triplestore:
-            triplestore = self._triplestore
-        if not triplestore:
-            raise NamespaceError(
-                "`triplestore` argument needed for updating the cache"
-            )
-        if self._cache is None:
-            self._cache = {}
-
-        # Add (label, full_iri) pairs to cache
-        for la in reversed(self._label_annotations):
-            self._cache.update(
-                (o, s) for s, o in triplestore.subject_objects(la)
-                if s.startswith(self._iri)
-            )
-
-        # Add (name, full_iri) pairs to cache
-        # Currently we only check concepts that defines RDFS.isDefinedBy
-        # relations.
-        # Is there an efficient way to loop over all IRIs in this namespace?
-        n = len(self._iri)
-        self._cache.update(
-            (s[n:], s) for s in triplestore.subjects(
-                RDFS.isDefinedBy, self._iri)
-            if s.startswith(self._iri)
-        )
-
-    def __getattr__(self, name):
-        if self._cache is not None and name in self._cache:
-            return self._cache[name]
-
-        if self._triplestore:
-
-            # Check if ``iri = self._iri + name`` is in the triplestore.
-            # If so, add it to the cache.
-            # We only need to check that generator returned by
-            # `self._triplestore.predicate_objects(iri)` is non-empty.
-            iri = self._iri + name
-            g = self._triplestore.predicate_objects(iri)
-            try:
-                g.__next__()
-            except StopIteration:
-                pass
-            else:
-                if self._cache is not None:
-                    self._cache[name] = iri
-                return iri
-
-            # Check for label annotations matching `name`.
-            for la in self._label_annotations:
-                for s, o in self._triplestore.subject_objects(la):
-                    if o == name and s.startswith(self._iri):
-                        if self._cache is not None:
-                            self._cache[name] = s
-                        return s
-
-        if self._check:
-            raise NoSuchIRIError(self._iri + name)
-        else:
-            return self._iri + name
-
-    def __getitem__(self, key):
-        return self.__getattr__(key)
-
-    def __repr__(self):
-        return f"Namespace({self._iri})"
-
-    def __str__(self):
-        return self._iri
-
-    def __add__(self, other):
-        return self._iri + str(other)
-
-
-# Pre-defined namespaces
-XML = Namespace("http://www.w3.org/XML/1998/namespace")
-RDF = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
-RDFS = Namespace("http://www.w3.org/2000/01/rdf-schema#")
-XSD = Namespace("http://www.w3.org/2001/XMLSchema#")
-OWL = Namespace("http://www.w3.org/2002/07/owl#")
-SKOS = Namespace("http://www.w3.org/2004/02/skos/core#")
-DC = Namespace("http://purl.org/dc/elements/1.1/")
-DCTERMS = Namespace("http://purl.org/dc/terms/")
-FOAF = Namespace("http://xmlns.com/foaf/0.1/")
-DOAP = Namespace("http://usefulinc.com/ns/doap#")
-PROV = Namespace("http://www.w3.org/ns/prov#")
-DCAT = Namespace("http://www.w3.org/ns/dcat#")
-TIME = Namespace("http://www.w3.org/2006/time#")
-FNO = Namespace("https://w3id.org/function/ontology#")
-QUDTU = Namespace("http://qudt.org/vocab/unit/")
-OM = Namespace("http://www.ontology-of-units-of-measure.org/resource/om-2/")
-
-EMMO = Namespace("http://emmo.info/emmo#")
-MAP = Namespace("http://emmo.info/domain-mappings#")
-DM = Namespace("http://emmo.info/datamodel#")
-
-
-class Literal(str):
-    """A literal RDF value."""
-    def __new__(cls, value, lang=None, datatype=None):
-        string = super().__new__(cls, value)
-        if lang:
-            if datatype:
-                raise TypeError("A literal can only have one of `lang` or "
-                                "`datatype`.")
-            string.lang = str(lang)
-            string.datatype = None
-        else:
-            string.lang = None
-            if datatype:
-                d = {
-                    str: XSD.string,
-                    bool: XSD.boolean,
-                    int: XSD.integer,
-                    float: XSD.double,
-                    bytes: XSD.hexBinary,
-                    bytearray: XSD.hexBinary,
-                    datetime: XSD.dateTime,
-                }
-                string.datatype = d.get(datatype, datatype)
-            elif isinstance(value, str):
-                string.datatype = None
-            elif isinstance(value, bool):
-                string.datatype = XSD.boolean
-            elif isinstance(value, int):
-                string.datatype = XSD.integer
-            elif isinstance(value, float):
-                string.datatype = XSD.double
-            elif isinstance(value, (bytes, bytearray)):
-                string = value.hex()
-                string.datatype = XSD.hexBinary
-            elif isinstance(value, datetime):
-                string.datatype = XSD.dateTime
-                # TODO:
-                #   - XSD.base64Binary
-                #   - XSD.byte, XSD.unsignedByte
-            else:
-                string.datatype = None
-        return string
-
-    def __repr__(self):
-        lang = f", lang='{self.lang}'" if self.lang else ""
-        datatype = f", datatype='{self.datatype}'" if self.datatype else ""
-        return f"Literal('{self}'{lang}{datatype})"
-
-    value = property(
-        fget=lambda self: self.to_python(),
-        doc="Appropriate python datatype derived from this RDF literal.",
-    )
-
-    def to_python(self):
-        """Returns an appropriate python datatype derived from this RDF
-        literal."""
-        v = str(self)
-
-        if self.datatype == XSD.boolean:
-            v = bool(self)
-        elif self.datatype in (XSD.integer, XSD.int, XSD.short, XSD.long,
-                               XSD.nonPositiveInteger,
-                               XSD.negativeInteger, XSD.nonNegativeInteger,
-                               XSD.unsignedInt, XSD.unsignedShort,
-                               XSD.unsignedLong,
-                               XSD.byte, XSD.unsignedByte,
-                               ):
-            v = int(self)
-        elif self.datatype in (XSD.double, XSD.decimal, XSD.dataTimeStamp,
-                               OWL.real, OWL.rational):
-            v = float(self)
-        elif self.datatype == XSD.hexBinary:
-            v = self.encode()
-        elif self.datatype == XSD.dateTime:
-            v = datetime.fromisoformat(self)
-        elif self.datatype and self.datatype not in (
-                RDF.PlainLiteral, RDF.XMLLiteral, RDFS.Literal,
-                XSD.anyURI, XSD.language, XSD.Name, XSD.NMName,
-                XSD.normalizedString, XSD.string, XSD.token, XSD.NMTOKEN,
-        ):
-            warnings.warn(
-                f"unknown datatype: {self.datatype} - assuming string"
-            )
-        return v
-
-    def n3(self):
-        """Returns a representation in n3 format."""
-        if self.lang:
-            return f'"{self}"@{self.lang}'
-        elif self.datatype:
-            return f'"{self}"^^{self.datatype}'
-        else:
-            return f'"{self}"'
-
-
-def en(value):
-    """Convenience function that returns value as a plain english literal.
-
-    Equivalent to``Literal(value, lang="en")``.
-    """
-    return Literal(value, lang="en")
-
-
-class Triplestore:
-    """Provides a common frontend to a range of triplestore backends."""
-
-    default_namespaces = {
-        "xml": XML,
-        "rdf": RDF,
-        "rdfs": RDFS,
-        "xsd": XSD,
-        "owl": OWL,
-        # "skos": SKOS,
-        # "dc": DC,
-        # "dcterms": DCTERMS,
-        # "foaf": FOAF,
-        # "doap": DOAP,
-        # "fno": FNO,
-        # "emmo": EMMO,
-        # "map": MAP,
-        # "dm": DM,
-    }
-
-    def __init__(self, backend: str, base_iri: str = None, **kwargs):
-        """Initialise triplestore using the backend with the given name.
-
-        Parameters:
-            backend: Name of the backend module.
-            base_iri: Base IRI used by the add_function() method when adding
-                new triples.
-            kwargs: Keyword arguments passed to the backend's __init__()
-                method.
-        """
-        module = import_module(backend if "." in backend
-                      else "dlite.triplestore.backends." + backend)
-        cls = getattr(module, backend.title() + "Strategy")
-        self.base_iri = base_iri
-        self.namespaces = {}
-        self.backend_name = backend
-        self.backend = cls(base_iri=base_iri, **kwargs)
-        # Keep functions in the triplestore for convienence even though
-        # they usually do not belong to the triplestore per se.
-        self.function_repo = {}
-        for prefix, ns in self.default_namespaces.items():
-            self.bind(prefix, ns)
-
-    # Methods implemented by backend
-    # ------------------------------
-    def triples(self, triple: "Triple") -> "Generator":
-        """Returns a generator over matching triples."""
-        return self.backend.triples(triple)
-
-    def add_triples(self, triples: "Sequence[Triple]"):
-        """Add a sequence of triples."""
-        self.backend.add_triples(triples)
-
-    def remove(self, triple: "Triple"):
-        """Remove all matching triples from the backend."""
-        self.backend.remove(triple)
-
-    # Methods optionally implemented by backend
-    # -----------------------------------------
-    def parse(self, source=None, format=None, **kwargs):
-        """Parse source and add the resulting triples to triplestore.
-
-        Parameters:
-            source: File-like object or file name.
-            format: Needed if format can not be inferred from source.
-            kwargs: Keyword arguments passed to the backend.
-                The rdflib and ontopy backends support e.g. `location`
-                (absolute or relative URL) and `data` (string
-                containing the data to be parsed) arguments.
-        """
-        self._check_method("parse")
-        self.backend.parse(source=source, format=format, **kwargs)
-
-        if hasattr(self.backend, "namespaces"):
-            for prefix, ns in self.backend.namespaces().items():
-                if prefix and prefix not in self.namespaces:
-                    self.namespaces[prefix] = Namespace(ns)
-
-    def serialize(self, destination=None, format="turtle", **kwargs):
-        """Serialise triplestore.
-
-        Parameters:
-            destination: File name or object to write to.  If None, the
-                serialisation is returned.
-            format: Format to serialise as.  Supported formats, depends on
-                the backend.
-            kwargs: Passed to the backend serialize() method.
-
-        Returns:
-            Serialized string if `destination` is None.
-        """
-        self._check_method("serialize")
-        return self.backend.serialize(destination=destination,
-                                      format=format, **kwargs)
-
-    def query(self, query_object, **kwargs):
-        """SPARQL query.
-
-        Parameters:
-            query_object: String with the SPARQL query.
-            kwargs: Keyword arguments passed to rdflib.Graph.query().
-
-        Returns:
-            List of tuples of IRIs for each matching row.
-
-        Note:
-            This method is intended for SELECT queries.  Use
-            the update() method for INSERT and DELETE  queries.
-
-        """
-        self._check_method("query")
-        return self.backend.query(query_object=query_object, **kwargs)
-
-    def update(self, update_object, **kwargs):
-        """Update triplestore with SPARQL.
-
-        Parameters:
-            query_object: String with the SPARQL query.
-            kwargs: Keyword arguments passed to rdflib.Graph.query().
-
-        Note:
-            This method is intended for INSERT and DELETE queries.  Use
-            the query() method for SELECT queries.
-        """
-        self._check_method("update")
-        return self.backend.update(update_object=update_object, **kwargs)
-
-    def bind(self, prefix: str, namespace: "Union[str, Namespace]", **kwargs):
-        """Bind prefix to namespace and return the new Namespace object.
-
-        The new Namespace is created with `namespace` as IRI.
-        Keyword arguments are passed to the Namespace() constructor.
-
-        If `namespace` is None, the corresponding prefix is removed.
-        """
-        if namespace is None:
-            del self.namespaces[prefix]
-            ns = None
-        else:
-            ns = namespace if isinstance(namespace, Namespace) else Namespace(
-                namespace, **kwargs)
-            self.namespaces[prefix] = ns
-
-        if hasattr(self.backend, "bind"):
-            self.backend.bind(prefix, namespace)
-
-        return ns
-
-    # Convenient methods
-    # ------------------
-    # These methods are modelled after rdflib and provide some convinient
-    # interfaces to the triples(), add_triples() and remove() methods
-    # implemented by all backends.
-    def _check_method(self, name):
-        """Check that backend implements the given method."""
-        if not hasattr(self.backend, name):
-            raise NotImplementedError(
-                f'Triplestore backend "{self.backend_name}" do not '
-                f'implement a "{name}()" method.')
-
-    def add(self, triple: "Triple"):
-        """Add `triple` to triplestore."""
-        self.add_triples([triple])
-
-    def value(self, subject=None, predicate=None, object=None, default=None,
-              any=False):
-        """Return the value for a pair of two criteria.
-
-        Useful if one knows that there may only be one value.
-
-        Parameters:
-            subject, predicate, object: Triple to match.
-            default: Value to return if no matches are found.
-            any: If true, return any matching value, otherwise raise
-                UniquenessError.
-        """
-        g = self.triples((subject, predicate, object))
-        try:
-            value = next(g)
-        except StopIteration:
-            return default
-
-        if any:
-            return value
-
-        try:
-            next(g)
-        except StopIteration:
-            return value
-        else:
-            raise UniquenessError("More than one match")
-
-    def subjects(self, predicate=None, object=None):
-        """Returns a generator of subjects for given predicate and object."""
-        for s, _, _ in self.triples((None, predicate, object)):
-            yield s
-
-    def predicates(self, subject=None, object=None):
-        """Returns a generator of predicates for given subject and object."""
-        for _, p, _ in self.triples((subject, None, object)):
-            yield p
-
-    def objects(self, subject=None, predicate=None):
-        """Returns a generator of objects for given subject and predicate."""
-        for _, _, o in self.triples((subject, predicate, None)):
-            yield o
-
-    def subject_predicates(self, object=None):
-        """Returns a generator of (subject, predicate) tuples for given
-        object."""
-        for s, p, _ in self.triples((None, None, object)):
-            yield s, p
-
-    def subject_objects(self, predicate=None):
-        """Returns a generator of (subject, object) tuples for given
-        predicate."""
-        for s, _, o in self.triples((None, predicate, None)):
-            yield s, o
-
-    def predicate_objects(self, subject=None):
-        """Returns a generator of (predicate, object) tuples for given
-        subject."""
-        for _, p, o in self.triples((subject, None, None)):
-            yield p, o
-
-    def set(self, triple):
-        """Convenience method to update the value of object.
-
-        Removes any existing triples for subject and predicate before adding
-        the given `triple`.
-        """
-        s, p, _ = triple
-        self.remove((s, p, None))
-        self.add(triple)
-
-    def has(self, subject=None, predicate=None, object=None):
-        """Returns true if the triplestore has any triple matching
-        the give subject, predicate and/or object."""
-        g = self.triples((subject, predicate, object))
-        try:
-            g.__next__()
-        except StopIteration:
-            return False
-        return True
-
-
-    # Methods providing additional functionality
-    # ------------------------------------------
-    def expand_iri(self, iri: str):
-        """Return the full IRI if `iri` is prefixed.  Otherwise `iri` is
-        returned."""
-        match = re.match(_MATCH_PREFIXED_IRI, iri)
-        if match:
-            prefix, name = match.groups()
-            if prefix not in self.namespaces:
-                raise NamespaceError(f"unknown namespace: {prefix}")
-            return f"{self.namespaces[prefix]}{name}"
-        return iri
-
-    def prefix_iri(self, iri: str, require_prefixed: bool = False):
-        """Return prefixed IRI.
-
-        This is the reverse of expand_iri().
-
-        If `require_prefixed` is true, a NamespaceError exception is raised
-        if no prefix can be found.
-        """
-        if not re.match(_MATCH_PREFIXED_IRI, iri):
-            for prefix, namespace in self.namespaces.items():
-                if iri.startswith(str(namespace)):
-                    return f"{prefix}:{iri[len(str(namespace)):]}"
-            if require_prefixed:
-                raise NamespaceError(f"No prefix defined for IRI: {iri}")
-        return iri
-
-    def add_mapsTo(
-            self,
-            target: str,
-            source: "Union[str, dlite.Instance, dataclass]",
-            property_name: str = None,
-            cost: "Union[float, Callable]" = None,
-            target_cost: bool = True,
-    ):
-        """Add 'mapsTo' relation to triplestore.
-
-        Parameters:
-            target: IRI of target ontological concept.
-            source: Source IRI or entity object.
-            property_name: Name of property if `source` is an entity or
-                an entity IRI.
-            cost: User-defined cost of following this mapping relation
-                represented as a float.  It may be given either as a
-                float or as a callable taking the value of the mapped
-                quantity as input and returning the cost as a float.
-            target_cost: Whether the cost is assigned to mapping steps
-                that have `target` as output.
-        """
-        self.bind("map", MAP)
-
-        if not property_name and not isinstance(source, str):
-            raise TriplestoreError(
-                "`property_name` is required when `target` is not a string.")
-
-        target = self.expand_iri(target)
-        source = self.expand_iri(infer_iri(source))
-        if property_name:
-            source = f"{source}#{property_name}"
-            self.add((source, MAP.mapsTo, target))
-        if cost is not None:
-            dest = target if target_cost else source
-            self._add_cost(cost, dest)
-
-    def add_function(
-            self,
-            func: Callable,
-            expects: "Union[str, Sequence, Mapping]" = (),
-            returns: "Union[str, Sequence]" = (),
-            base_iri: str = None,
-            standard: str = 'fno',
-            cost: "Union[float, Callable]" = None,
-    ):
-        """Inspect function and add triples describing it to the triplestore.
-
-        Parameters:
-            func: Function to describe.
-            expects: Sequence of IRIs to ontological concepts corresponding
-                to positional arguments of `func`.  May also be given as a
-                dict mapping argument names to corresponding ontological IRIs.
-            returns: IRI of return value.  May also be given as a sequence
-                of IRIs, if multiple values are returned.
-            base_iri: Base of the IRI representing the function in the
-                knowledge base.  Defaults to the base IRI of the triplestore.
-            standard: Name of ontology to use when describing the function.
-                Defaults to the Function Ontology (FnO).
-            cost: User-defined cost of following this mapping relation
-                represented as a float.  It may be given either as a
-                float or as a callable taking the same arguments as `func`
-                returning the cost as a float.
-
-        Returns:
-            func_iri: IRI of the added function.
-        """
-        if isinstance(expects, str):
-            expects = [expects]
-        if isinstance(returns, str):
-            returns = [returns]
-
-        method = getattr(self, f"_add_function_{standard}")
-        func_iri = method(func, expects, returns, base_iri)
-        self.function_repo[func_iri] = func
-
-        if cost is not None:
-            for dest_iri in returns:
-                self._add_cost(cost, dest_iri)
-
-        return func_iri
-
-    def _add_cost(self, cost, dest_iri):
-        """Help function that adds `cost` to destination IRI `dest_iri`.
-
-        `cost` should be either a float or a Callable returning a float.
-
-        If `cost` is a callable it is just referred to with a literal
-        id and is not ontologically described as a function.  The
-        expected input arguments depends on the context, which is why
-        this function is not part of the public API.  Use the add_mapsTo()
-        and add_function() methods instead.
-        """
-        if self.has(dest_iri, DM.hasCost):
-            warnings.warn(f"A cost is already assigned to IRI: {dest_iri}")
-        elif callable(cost):
-            cost_id = f"cost_function{function_id(cost)}"
-            self.add((dest_iri, DM.hasCost, Literal(cost_id)))
-            self.function_repo[cost_id] = cost
-        else:
-            self.add((dest_iri, DM.hasCost, Literal(cost)))
-
-    def _add_function_fno(self, func, expects, returns, base_iri):
-        """Implementing add_function() for FnO."""
-        self.bind("fno", FNO)
-        self.bind("dcterms", DCTERMS)
-        self.bind("map", MAP)
-
-        if base_iri is None:
-            base_iri = self.base_iri if self.base_iri else ":"
-        fid = function_id(func)  # Function id
-        doc = inspect.getdoc(func)
-        name = func.__name__
-        signature = inspect.signature(func)
-        func_iri = f"{base_iri}{name}_{fid}"
-        parlist = f"_:{name}{fid}parlist"
-        outlist = f"_:{name}{fid}outlist"
-        self.add((func_iri, RDF.type, FNO.Function))
-        self.add((func_iri, FNO.expects, parlist))
-        self.add((func_iri, FNO.returns, outlist))
-        if doc:
-            self.add((func_iri, DCTERMS.description, en(doc)))
-
-        if isinstance(expects, Sequence):
-            items = list(zip(expects, signature.parameters))
-        else:
-            items = [(expects[par], par)
-                     for par in signature.parameters.keys()]
-        lst = parlist
-        for i, (iri, parname) in enumerate(items):
-            lst_next = f"{parlist}{i+2}" if i < len(items) - 1 else RDF.nil
-            par = f"{func_iri}_parameter{i+1}_{parname}"
-            self.add((par, RDF.type, FNO.Parameter))
-            self.add((par, RDFS.label, en(parname)))
-            self.add((par, MAP.mapsTo, iri))
-            self.add((lst, RDF.first, par))
-            self.add((lst, RDF.rest, lst_next))
-            lst = lst_next
-
-        lst = outlist
-        for i, iri in enumerate(returns):
-            lst_next = f"{outlist}{i+2}" if i < len(returns) - 1 else RDF.nil
-            val = f"{func_iri}_output{i+1}"
-            self.add((val, RDF.type, FNO.Output))
-            self.add((val, MAP.mapsTo, iri))
-            self.add((lst, RDF.first, val))
-            self.add((lst, RDF.rest, lst_next))
-            lst = lst_next
-
-        return func_iri
-
-
-def infer_iri(obj):
-    """Return IRI of the individual that stands for object `obj`."""
-    if isinstance(obj, str):
-        return obj
-    if hasattr(obj, "uri") and obj.uri:
-        # dlite.Metadata or dataclass (or instance with uri)
-        return obj.uri
-    if hasattr(obj, "uuid") and obj.uuid:
-        # dlite.Instance or dataclass
-        return obj.uuid
-    if hasattr(obj, "schema") and callable(obj.schema):
-        # pydantic.BaseModel
-        schema = obj.schema()
-        properties = schema['properties']
-        if "uri" in properties and properties["uri"]:
-            return properties["uri"]
-        if "uuid" in properties and properties["uuid"]:
-            return properties["uuid"]
-    raise TypeError("cannot infer IRI from object {obj!r}")
-
-
-def function_id(func, length=4):
-    """Return a checksum for function `func`.
-
-    The returned object is a string of hexadecimal digits.
-
-    `length` is the number of bytes in the returned checksum.  Since
-    the current implementation is based on the shake_128 algorithm,
-    it make no sense to set `length` larger than 32 bytes.
-    """
-    #return hex(crc32(inspect.getsource(func).encode())).lstrip('0x')
-    return hashlib.shake_128(
-        inspect.getsource(func).encode()).hexdigest(length)
diff --git a/bindings/python/triplestore/units.py b/bindings/python/triplestore/units.py
index 9c9ced9fc..92b262b24 100644
--- a/bindings/python/triplestore/units.py
+++ b/bindings/python/triplestore/units.py
@@ -3,8 +3,7 @@
 import os
 import re
 import logging
-import warnings
-from pint import UnitRegistry, Quantity
+from pint import UnitRegistry
 from tripper import Triplestore, RDFS
 from appdirs import user_cache_dir
 
diff --git a/bindings/python/utils.py b/bindings/python/utils.py
index 01a0bc52c..583bc5336 100644
--- a/bindings/python/utils.py
+++ b/bindings/python/utils.py
@@ -1,5 +1,3 @@
-import binascii
-import os
 from typing import Sequence, Mapping
 import json
 from typing import Dict, List, Optional
diff --git a/examples/mappings/mappingfunc.py b/examples/mappings/mappingfunc.py
index 5efdc0e7c..c3ea18f4a 100644
--- a/examples/mappings/mappingfunc.py
+++ b/examples/mappings/mappingfunc.py
@@ -3,10 +3,9 @@
 
 import numpy as np
 
-from tripper import DM, EMMO, RDFS, Triplestore
+from tripper import EMMO, RDFS, Triplestore
 
 import dlite
-from dlite.mappings import instantiate
 
 
 # Paths
diff --git a/examples/mappings/simple.py b/examples/mappings/simple.py
index 9f1e17965..3bd35a550 100644
--- a/examples/mappings/simple.py
+++ b/examples/mappings/simple.py
@@ -1,7 +1,7 @@
 """Mapping example using a collection - without mapping functions."""
 from pathlib import Path
 
-from tripper import EMMO, MAP, RDFS, Triplestore
+from tripper import EMMO, Triplestore
 
 import dlite
 
diff --git a/pydoc/_static/logo.jpg b/pydoc/_static/logo.jpg
deleted file mode 100644
index a99fda71b..000000000
Binary files a/pydoc/_static/logo.jpg and /dev/null differ
diff --git a/pydoc/_templates/autoapi/base/base.rst b/pydoc/_templates/autoapi/base/base.rst
new file mode 100644
index 000000000..45c8dd7f8
--- /dev/null
+++ b/pydoc/_templates/autoapi/base/base.rst
@@ -0,0 +1,7 @@
+.. {{ obj.type }}:: {{ obj.name }}
+
+	{% if summary %}
+
+	{{ obj.summary }}
+
+	{% endif %}
diff --git a/pydoc/_templates/autoapi/index.rst b/pydoc/_templates/autoapi/index.rst
new file mode 100644
index 000000000..95d0ad891
--- /dev/null
+++ b/pydoc/_templates/autoapi/index.rst
@@ -0,0 +1,15 @@
+API Reference
+=============
+
+This page contains auto-generated API reference documentation [#f1]_.
+
+.. toctree::
+   :titlesonly:
+
+   {% for page in pages %}
+   {% if page.top_level_object and page.display %}
+   {{ page.include_path }}
+   {% endif %}
+   {% endfor %}
+
+.. [#f1] Created with `sphinx-autoapi <https://github.com/readthedocs/sphinx-autoapi>`_
diff --git a/pydoc/_templates/autoapi/python/attribute.rst b/pydoc/_templates/autoapi/python/attribute.rst
new file mode 100644
index 000000000..ebaba555a
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/attribute.rst
@@ -0,0 +1,1 @@
+{% extends "python/data.rst" %}
diff --git a/pydoc/_templates/autoapi/python/class.rst b/pydoc/_templates/autoapi/python/class.rst
new file mode 100644
index 000000000..df5edffb6
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/class.rst
@@ -0,0 +1,58 @@
+{% if obj.display %}
+.. py:{{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %}
+{% for (args, return_annotation) in obj.overloads %}
+   {{ " " * (obj.type | length) }}   {{ obj.short_name }}{% if args %}({{ args }}){% endif %}
+{% endfor %}
+
+
+   {% if obj.bases %}
+   {% if "show-inheritance" in autoapi_options %}
+   Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %}
+   {% endif %}
+
+
+   {% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %}
+   .. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }}
+      :parts: 1
+      {% if "private-members" in autoapi_options %}
+      :private-bases:
+      {% endif %}
+
+   {% endif %}
+   {% endif %}
+   {% if obj.docstring %}
+   {{ obj.docstring|indent(3) }}
+   {% endif %}
+   {% if "inherited-members" in autoapi_options %}
+   {% set visible_classes = obj.classes|selectattr("display")|list %}
+   {% else %}
+   {% set visible_classes = obj.classes|rejectattr("inherited")|selectattr("display")|list %}
+   {% endif %}
+   {% for klass in visible_classes %}
+   {{ klass.render()|indent(3) }}
+   {% endfor %}
+   {% if "inherited-members" in autoapi_options %}
+   {% set visible_properties = obj.properties|selectattr("display")|list %}
+   {% else %}
+   {% set visible_properties = obj.properties|rejectattr("inherited")|selectattr("display")|list %}
+   {% endif %}
+   {% for property in visible_properties %}
+   {{ property.render()|indent(3) }}
+   {% endfor %}
+   {% if "inherited-members" in autoapi_options %}
+   {% set visible_attributes = obj.attributes|selectattr("display")|list %}
+   {% else %}
+   {% set visible_attributes = obj.attributes|rejectattr("inherited")|selectattr("display")|list %}
+   {% endif %}
+   {% for attribute in visible_attributes %}
+   {{ attribute.render()|indent(3) }}
+   {% endfor %}
+   {% if "inherited-members" in autoapi_options %}
+   {% set visible_methods = obj.methods|selectattr("display")|list %}
+   {% else %}
+   {% set visible_methods = obj.methods|rejectattr("inherited")|selectattr("display")|list %}
+   {% endif %}
+   {% for method in visible_methods %}
+   {{ method.render()|indent(3) }}
+   {% endfor %}
+{% endif %}
diff --git a/pydoc/_templates/autoapi/python/data.rst b/pydoc/_templates/autoapi/python/data.rst
new file mode 100644
index 000000000..4c7da4ae8
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/data.rst
@@ -0,0 +1,32 @@
+{% if obj.display and obj.name not in ("__repr__",) %}
+.. py:{{ obj.type }}:: {{ obj.name }}
+   {%+ if obj.value is not none or obj.annotation is not none -%}
+   :annotation:
+        {%- if obj.annotation %} :{{ obj.annotation }}
+        {%- endif %}
+        {%- if obj.value is not none %} = {%
+            if obj.value is string and obj.value.splitlines()|count > 1 -%}
+                Multiline-String
+
+    .. raw:: html
+
+        <details><summary>Show Value</summary>
+
+    .. code-block:: text
+        :linenos:
+
+        {{ obj.value|indent(width=8) }}
+
+    .. raw:: html
+
+        </details>
+
+            {%- else -%}
+                {{ obj.value|string|truncate(100) }}
+            {%- endif %}
+        {%- endif %}
+    {% endif %}
+
+
+   {{ obj.docstring|indent(3) }}
+{% endif %}
diff --git a/pydoc/_templates/autoapi/python/exception.rst b/pydoc/_templates/autoapi/python/exception.rst
new file mode 100644
index 000000000..92f3d38fd
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/exception.rst
@@ -0,0 +1,1 @@
+{% extends "python/class.rst" %}
diff --git a/pydoc/_templates/autoapi/python/function.rst b/pydoc/_templates/autoapi/python/function.rst
new file mode 100644
index 000000000..b00d5c244
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/function.rst
@@ -0,0 +1,15 @@
+{% if obj.display %}
+.. py:function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}
+
+{% for (args, return_annotation) in obj.overloads %}
+              {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}
+
+{% endfor %}
+   {% for property in obj.properties %}
+   :{{ property }}:
+   {% endfor %}
+
+   {% if obj.docstring %}
+   {{ obj.docstring|indent(3) }}
+   {% endif %}
+{% endif %}
diff --git a/pydoc/_templates/autoapi/python/method.rst b/pydoc/_templates/autoapi/python/method.rst
new file mode 100644
index 000000000..723cb7bbe
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/method.rst
@@ -0,0 +1,19 @@
+{%- if obj.display %}
+.. py:method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}
+
+{% for (args, return_annotation) in obj.overloads %}
+            {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}
+
+{% endfor %}
+   {% if obj.properties %}
+   {% for property in obj.properties %}
+   :{{ property }}:
+   {% endfor %}
+
+   {% else %}
+
+   {% endif %}
+   {% if obj.docstring %}
+   {{ obj.docstring|indent(3) }}
+   {% endif %}
+{% endif %}
diff --git a/pydoc/_templates/autoapi/python/module.rst b/pydoc/_templates/autoapi/python/module.rst
new file mode 100644
index 000000000..1000e2c3e
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/module.rst
@@ -0,0 +1,114 @@
+{% if not obj.display %}
+:orphan:
+
+{% endif %}
+:py:mod:`{{ obj.name|remove_plugin_path }}`
+=========={{ "=" * obj.name|remove_plugin_path|length }}
+
+.. py:module:: {{ obj.name|remove_plugin_path }}
+
+{% if obj.docstring %}
+.. autoapi-nested-parse::
+
+   {{ obj.docstring|indent(3) }}
+
+{% endif %}
+
+{% block subpackages %}
+{% set visible_subpackages = obj.subpackages|selectattr("display")|list %}
+{% if visible_subpackages %}
+Subpackages
+-----------
+.. toctree::
+   :titlesonly:
+   :maxdepth: 3
+
+{% for subpackage in visible_subpackages %}
+   {{ subpackage.short_name }}/index.rst
+{% endfor %}
+
+
+{% endif %}
+{% endblock %}
+{% block submodules %}
+{% set visible_submodules = obj.submodules|selectattr("display")|list %}
+{% if visible_submodules %}
+Submodules
+----------
+.. toctree::
+   :titlesonly:
+   :maxdepth: 1
+
+{% for submodule in visible_submodules %}
+   {{ submodule.short_name }}/index.rst
+{% endfor %}
+
+
+{% endif %}
+{% endblock %}
+{% block content %}
+{% if obj.all is not none %}
+{% set visible_children = obj.children|selectattr("short_name", "in", obj.all)|list %}
+{% elif obj.type is equalto("package") %}
+{% set visible_children = obj.children|selectattr("display")|list %}
+{% else %}
+{% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %}
+{% endif %}
+{% if visible_children %}
+{{ obj.type|title }} Contents
+{{ "-" * obj.type|length }}---------
+
+{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %}
+{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %}
+{% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %}
+{% if "show-module-summary" in autoapi_options and (visible_classes or visible_functions) %}
+{% block classes scoped %}
+{% if visible_classes %}
+Classes
+~~~~~~~
+
+.. autoapisummary::
+
+{% for klass in visible_classes %}
+   {{ klass.id }}
+{% endfor %}
+
+
+{% endif %}
+{% endblock %}
+
+{% block functions scoped %}
+{% if visible_functions %}
+Functions
+~~~~~~~~~
+
+.. autoapisummary::
+
+{% for function in visible_functions %}
+   {{ function.id }}
+{% endfor %}
+
+
+{% endif %}
+{% endblock %}
+
+{% block attributes scoped %}
+{% if visible_attributes %}
+Attributes
+~~~~~~~~~~
+
+.. autoapisummary::
+
+{% for attribute in visible_attributes %}
+   {{ attribute.id }}
+{% endfor %}
+
+
+{% endif %}
+{% endblock %}
+{% endif %}
+{% for obj_item in visible_children %}
+{{ obj_item.render()|indent(0) }}
+{% endfor %}
+{% endif %}
+{% endblock %}
diff --git a/pydoc/_templates/autoapi/python/package.rst b/pydoc/_templates/autoapi/python/package.rst
new file mode 100644
index 000000000..fb9a64965
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/package.rst
@@ -0,0 +1,1 @@
+{% extends "python/module.rst" %}
diff --git a/pydoc/_templates/autoapi/python/property.rst b/pydoc/_templates/autoapi/python/property.rst
new file mode 100644
index 000000000..70af24236
--- /dev/null
+++ b/pydoc/_templates/autoapi/python/property.rst
@@ -0,0 +1,15 @@
+{%- if obj.display %}
+.. py:property:: {{ obj.short_name }}
+   {% if obj.annotation %}
+   :type: {{ obj.annotation }}
+   {% endif %}
+   {% if obj.properties %}
+   {% for property in obj.properties %}
+   :{{ property }}:
+   {% endfor %}
+   {% endif %}
+
+   {% if obj.docstring %}
+   {{ obj.docstring|indent(3) }}
+   {% endif %}
+{% endif %}
diff --git a/pydoc/conf.py b/pydoc/conf.py
index c922a4835..cc15ab5b5 100644
--- a/pydoc/conf.py
+++ b/pydoc/conf.py
@@ -1,65 +1,164 @@
-# Configuration file for the Sphinx documentation builder.
-#
-# For the full list of built-in configuration values, see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
+"""Configuration file for the Sphinx documentation builder.
+
+For the full list of built-in configuration values, see the documentation:
+https://www.sphinx-doc.org/en/master/usage/configuration.html
+"""
+import os
+from pathlib import Path
+import re
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover
+    from jinja2 import Environment
+
+dlite_version_file = Path(__file__).resolve().parent.parent / "CMakeLists.txt"
+if not dlite_version_file.exists():
+    raise FileNotFoundError(
+        f"Could not find {dlite_version_file} necessary to read DLite version."
+    )
+for line in dlite_version_file.read_text(encoding="utf8").splitlines():
+    match = re.match(
+        r"^\s+VERSION\s+(?P<version>[0-9]+(\.[0-9]+){2})$",
+        line,
+    )
+    if match:
+        dlite_version = match.group("version")
+        break
+else:
+    raise ValueError(f"Could not determine DLite version from {dlite_version_file}")
+
+dlite_share_plugins = [
+    plugin_dir.name
+    for plugin_dir in (
+        Path(__file__).resolve().parent.parent
+        / "build" / "bindings" / "python" / "dlite" / "share" / "dlite"
+    ).iterdir()
+    if plugin_dir.is_dir()
+]
 
 # -- Project information -----------------------------------------------------
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
 
-project = 'DLite'
-copyright = '© SINTEF 2022'
-author = 'SINTEF'
-release = '0.3'
+project = "DLite"
+copyright = "© SINTEF 2022"
+author = "SINTEF"
+version = ".".join(dlite_version.split(".")[2:])
+release = dlite_version
+
 # -- General configuration ---------------------------------------------------
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 
-
 # General configuration
+exclude_patterns = [
+    "_build",
+    "Thumbs.db",
+    ".DS_Store",
+    "**.ipynb_checkpoints",
+    "_templates",
+]
+
 extensions = [
-    "breathe", # Doxygen bridge
-    "myst_parser",  # markdown source support
     "autoapi.extension",
-    "sphinx.ext.napoleon",  # API ref Google and NumPy style
+    "breathe", # Doxygen bridge
+    "myst_nb",  # markdown source support & support for Jupyter notebooks
     "sphinx.ext.graphviz",  # Graphviz
+    "sphinx.ext.intersphinx",  # Connect to external (Sphinx) API documentation
+    "sphinx.ext.napoleon",  # API ref Google and NumPy style
+    "sphinx.ext.viewcode",
+    "sphinxcontrib.plantuml",  # PlantUml
     "sphinx_copybutton",  # Copy button for codeblocks
-    "nbsphinx",  # Jupyter
-    "IPython.sphinxext.ipython_console_highlighting",  # nb syntax highlight
-    "sphinx.ext.autosectionlabel",  # Auto-generate section labels.
-    "sphinx_panels",  # Create panels in a grid layout or as drop-downs
-    "sphinx_markdown_tables",
+    "sphinx_design",  # Create panels in a grid layout or as drop-downs and more
+    # "sphinx.ext.autosectionlabel",  # Auto-generate section labels.
 ]
 
-# Breathe Configuration
-breathe_projects = {"dlite": "../build/pydoc/doxygen/xml/"}
-breathe_default_project = "dlite"
+root_doc = "index"
 
-autoapi_dirs = ['../build/bindings/python/dlite']
-autoapi_type = 'python'
-autoapi_file_patterns=['*.py', '*.pyi']
-autoapi_add_toctree_entry = False
+suppress_warnings = ["myst.mathjax"]
 
-master_doc = "index"
+# Extension configuration
+breathe_projects = {"dlite": "../build/pydoc/doxygen/xml/"}
+breathe_default_project = "dlite"
 
-myst_heading_anchors = 5
+autoapi_dirs = [
+    "../build/bindings/python/dlite"
+] + [
+    f"../build/bindings/python/dlite/share/dlite/{plugin_dir}"
+    for plugin_dir in dlite_share_plugins
+]
+autoapi_type = "python"
+autoapi_file_patterns = ["*.py", "*.pyi"]
+autoapi_template_dir = "_templates/autoapi"
+autoapi_add_toctree_entry = True  # Toggle the most top-level index file
+autoapi_options = [
+    "members",
+    "undoc-members",
+    "private-members",
+    "show-inheritance",
+    "show-module-summary",
+    "special-members",
+    "imported-members",
+    # "show-inheritance-diagram",
+    # "inherited-members",
+]
+autoapi_keep_files = True  # Should be False in production
+autoapi_python_use_implicit_namespaces = True  # True to avoid namespace being `python.dlite`
 
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"]
+autodoc_typehints = "description"
+autodoc_typehints_format = "short"
+autodoc_inherit_docstrings = True
 
 # HTML output
 html_theme = "sphinx_book_theme"
 html_logo = "_static/logo.svg"
 html_favicon = "_static/favicon.ico"
 html_theme_options = {
-    "repository_url": "https://github.com/sintef/dlite",
+    "path_to_docs": "pydoc",
+    "repository_url": "https://github.com/SINTEF/dlite",
+    "repository_branch": "master",
+    "use_issues_button": True,
+    "use_fullscreen_button": True,
     "use_repository_button": True,
-    "repository_branch": "main",
-    "path_to_docs": "docs",
     "logo_only": True,
-    "show_navbar_depth": 2,
+    "show_navbar_depth": 1,
+    "announcement": "This documentation is under development!",
 }
-
 html_static_path = ["_static"]
-html_css_files = ["custom.css"]
+# html_css_files = ["custom.css"]
 
-nbsphinx_allow_errors = True
+intersphinx_mapping = {
+    "numpy": ("https://numpy.org/doc/stable/", None),
+    "openpyxl": ("https://openpyxl.readthedocs.io/en/stable/", None),
+    "pandas": ("https://pandas.pydata.org/docs/", None),
+    "pint": ("https://pint.readthedocs.io/en/stable/", None),
+    "pymongo": ("https://pymongo.readthedocs.io/en/stable/", None),
+    "python": ("https://docs.python.org/3", None),
+    "rdflib": ("https://rdflib.readthedocs.io/en/stable/", None),
+    "tripper": ("https://emmc-asbl.github.io/tripper/latest/", None),
+}
 
-suppress_warnings = ["myst.mathjax"]
+myst_heading_anchors = 5
+myst_enable_extensions = ["colon_fence"]
+
+napoleon_use_admonition_for_examples = True
+napoleon_use_admonition_for_notes = True
+napoleon_use_admonition_for_references = True
+napoleon_preprocess_types = True
+napoleon_attr_annotations = True
+
+nb_execution_allow_errors = False
+nb_execution_mode = "cache"
+if os.getenv("CI"):
+    nb_kernel_rgx_aliases = {".*": "python"}
+
+plantuml = "java -jar lib/plantuml.jar"
+plantuml_output_format = "svg_img"
+
+# Jinja2 custom filters
+def autoapi_prepare_jinja_env(jinja_env: "Environment") -> None:
+    """Add custom Jinja2 filters for use with AutoAPI templates."""
+
+    # Remove plugin path, removes the initial part of the module name if it is equal to
+    # any of the folder names under `dlite/share/dlite`.
+    jinja_env.filters["remove_plugin_path"] = lambda name: ".".join(
+        name.split(".")[1:]
+    ) if name.split(".")[0] in dlite_share_plugins else name
diff --git a/pydoc/dlite.rst b/pydoc/dlite.rst
index 1a0535f29..8de758984 100644
--- a/pydoc/dlite.rst
+++ b/pydoc/dlite.rst
@@ -1,7 +1,7 @@
 DLite Core
 ==========
 
-Core DLite C-API modules
+Core DLite C-API modules.
 
 .. toctree::
    :caption: Modules:
diff --git a/pydoc/index.rst b/pydoc/index.rst
index 6857c18db..fc25366bf 100644
--- a/pydoc/index.rst
+++ b/pydoc/index.rst
@@ -1,22 +1,60 @@
-DLite Python API
-================
+DLite
+=====
 
+.. Comment back in the toctrees below if ever Python files are generated in these folders.
 .. toctree::
    :maxdepth: 3
-   :caption: Contents:
+   :caption: API Reference
    :glob:
+   :hidden:
 
-   autoapi/python/dlite/index
-   autoapi/yaml/index
-   autoapi/postgresql/index
-   autoapi/blob/index
-   autoapi/mongodb/index
-   autoapi/pyrdf/index
-   autoapi/bson/index
-   dlite
-   extras
-   utils
- 
+   Python <autoapi/index>
+   C <dlite>
+   Extras <extras>
+   Utils <utils>
 
+.. toctree::
+   :maxdepth: 3
+   :caption: Mapping Plugins
+   :glob:
+   :hidden:
+
+   autoapi/mappingplugins/**/index
+
+.. .. toctree::
+..    :maxdepth: 3
+..    :caption: Storage Plugins
+..    :glob:
+..    :hidden:
+
+..    autoapi/storageplugins/**/index
+
+.. .. toctree::
+..    :maxdepth: 3
+..    :caption: Python Mapping Plugins
+..    :glob:
+..    :hidden:
+
+..    autoapi/pythonmappingplugins/**/index
+
+.. toctree::
+   :maxdepth: 3
+   :caption: Python Storage Plugins
+   :glob:
+   :hidden:
+
+   autoapi/pythonstorageplugins/**/index
+
+.. .. toctree::
+..    :maxdepth: 3
+..    :caption: Storages
+..    :glob:
+..    :hidden:
+
+..    autoapi/storages/**/index
 
+Indices and tables
+------------------
 
+* :ref:`genindex`
+* :ref:`modindex`
diff --git a/pydoc/utils.rst b/pydoc/utils.rst
index 38949cc5a..c10a65945 100644
--- a/pydoc/utils.rst
+++ b/pydoc/utils.rst
@@ -1,8 +1,7 @@
 Utils
 =====
 
-A collection of utility functions used by DLite, but generic enough
-to be separate.
+A collection of utility functions used by DLite, but generic enough to be separate.
 
 .. toctree::
    :caption: Modules:
@@ -12,7 +11,6 @@ to be separate.
 
    utils/bson
    utils/execprocess
-   utils/infixcalc
    utils/map
    utils/sha1
    utils/tgen
diff --git a/requirements.txt b/requirements.txt
index a229913a8..cbeda11fb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,6 @@ numpy
 PyYAML
 psycopg2-binary
 pandas
-pymongo
 rdflib
 pint
 openpyxl
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 2679950f9..11bb96574 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,4 +1,5 @@
-ipython==8.6.0
-ipykernel==6.0.1
-ipython_genutils==0.2.0
-mongomock==4.1.2
+# Keep ipython at min. v7.34.0 for Python 3.7 support.
+ipython>=7.34.0,<9
+ipykernel>=6.0.1,<7
+ipython_genutils~=0.2.0
+mongomock>=4.1.2,<5
diff --git a/requirements_doc.txt b/requirements_doc.txt
index 6415f608b..990a24c20 100644
--- a/requirements_doc.txt
+++ b/requirements_doc.txt
@@ -1,22 +1,10 @@
-markupsafe==2.0.1
 breathe==4.34.0
-docutils==0.17.1
-ipython==8.6.0
-myst-parser==0.18.0
-nbsphinx==0.8.9
-Jinja2==3.1.2
-Sphinx==4.5.0
-sphinx-autoapi==2.0.0
+importlib-metadata==4.13.0; python_version<'3.8'
+myst-nb~=0.17.1
+Sphinx>=4.5.0,<6
+sphinx-autoapi~=2.0
 sphinx-autobuild==2021.3.14
-sphinx-book-theme==0.3.3
-sphinx-copybutton==0.5.0
-sphinx-markdown-tables==0.0.17
-sphinx-panels==0.6.0
-sphinxawesome-theme==3.3.5
-sphinxcontrib-applehelp==1.0.2
-sphinxcontrib-devhelp==1.0.2
-sphinxcontrib-htmlhelp==2.0.0
-sphinxcontrib-jsmath==1.0.1
+sphinx-book-theme~=0.3.3
+sphinx-copybutton~=0.5.1
+sphinx-design~=0.3.0
 sphinxcontrib-plantuml==0.24
-sphinxcontrib-qthelp==1.0.3
-sphinxcontrib-serializinghtml==1.1.5
diff --git a/storages/python/python-storage-plugins/bson.py b/storages/python/python-storage-plugins/bson.py
index f7016f475..88ba031b7 100644
--- a/storages/python/python-storage-plugins/bson.py
+++ b/storages/python/python-storage-plugins/bson.py
@@ -1,5 +1,6 @@
 """A DLite storage plugin for BSON written in Python."""
 import os
+from typing import TYPE_CHECKING
 
 import bson as pybson # Must be pymongo.bson
 
@@ -7,86 +8,115 @@
 from dlite.options import Options
 from dlite.utils import instance_from_dict
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Generator, Optional
+
 
 class bson(dlite.DLiteStorageBase):
     """DLite storage plugin for BSON."""
 
-    def open(self, uri, options=None):
+    def open(self, uri: str, options: "Optional[str]" = None) -> None:
         """Open `uri`.
 
-        Supported options:
-        - mode : a | r | w
-            Valid values are:
-            - a   Append to existing file or create new file (default)
-            - r   Open existing file for read-only
-            - w   Truncate existing file or create new file
-        - soft7 : bool
-            Whether to save using SOFT7 format.
+        Parameters:
+            uri: A fully resolved URI to the BSON file.
+            options: Supported options:
+
+                - `mode`: Mode for opening.
+                  Valid values are:
+
+                  - `a`: Append to existing file or create new file (default).
+                  - `r`: Open existing file for read-only.
+                  - `w`: Truncate existing file or create new file.
+
+                - `soft7`: Whether to save using the SOFT7 format.
 
         After the options are passed, this method may set attribute
-        `writable` to True if it is writable and to False otherwise.
-        If `writable` is not set, it is assumed to be True.
+        ``writable`` to ``True`` if it is writable and to ``False`` otherwise.
+        If ``writable`` is not set, it is assumed to be ``True``.
 
         The BSON data is translated to JSON.
         """
-        self.options = Options(options, defaults='mode=a;soft7=true')
-        self.mode = dict(r='rb', w='wb', a='rb+', append='rb+')[self.options.mode]
-        if self.mode == 'rb' and not os.path.exists(uri):
-            raise FileNotFoundError(f"Did not find URI '{uri}'")
-        self.readable = True  if 'rb' in self.mode else False
-        self.writable = False if 'rb' == self.mode else True
+        self.options = Options(options, defaults="mode=a;soft7=true")
+        self.mode = dict(r="rb", w="wb", a="rb+", append="rb+")[self.options.mode]
+        if self.mode == "rb" and not os.path.exists(uri):
+            raise FileNotFoundError(f"Did not find URI {uri!r}")
+
+        self.readable = "rb" in self.mode
+        self.writable = "rb" != self.mode
         self.generic = True
         self.uri = uri
-        self.d = {}
-        if self.mode in ('rb', 'rb+'):
-            with open(uri, self.mode) as f:
-                bson_data = f.read()
+        self._data = {}
+        if self.mode in ("rb", "rb+"):
+            with open(uri, self.mode) as handle:
+                bson_data = handle.read()
+
             if pybson.is_valid(bson_data):
-                self.d = pybson.decode(bson_data)
-                if not self.d:
-                    raise EOFError(f"Failed to read BSON data from '{uri}'")
+                self._data = pybson.decode(bson_data)
+                if not self._data:
+                    raise EOFError(f"Failed to read BSON data from {uri!r}")
             else:
-                raise EOFError(f"Invalid BSON data in source '{uri}'")
+                raise EOFError(f"Invalid BSON data in source {uri!r}")
 
-    def close(self):
+    def close(self) -> None:
         """Close this storage and write the data to file.
 
         Assumes the data to store is in JSON format.
         """
         if self.writable:
-            if self.mode == 'rb+' and not os.path.exists(self.uri):
-                mode = 'wb'
+            if self.mode == "rb+" and not os.path.exists(self.uri):
+                mode = "wb"
             else:
                 mode = self.mode
+
             for uuid in self.queue():
-                props = self.d[uuid]['properties']
-                if type(props) == dict: # Metadata props is list
-                    for key in props.keys():
-                        if type(props[key]) in (bytearray, bytes):
+                props = self._data[uuid]["properties"]
+                if isinstance(props, dict): # Metadata props is list
+                    for key in props:
+                        if isinstance(props[key], (bytearray, bytes)):
                             props[key] = props[key].hex()
-                    self.d[uuid]['properties'] = props
-            with open(self.uri, mode) as f:
-                f.write(pybson.encode(self.d))
+                    self._data[uuid]["properties"] = props
+
+            with open(self.uri, mode) as handle:
+                handle.write(pybson.encode(self._data))
+
+    def load(self, uuid: str) -> dlite.Instance:
+        """Load `uuid` from current storage and return it as a new instance.
+
+        Parameters:
+            uuid: A UUID representing a DLite Instance to return from the RDF storage.
+
+        Returns:
+            A DLite Instance corresponding to the given UUID.
+
+        """
+        if uuid in self._data:
+            return instance_from_dict(self._data[uuid])
+        raise KeyError(f"Instance with ID {uuid!r} not found")
+
+    def save(self, inst: dlite.Instance) -> None:
+        """Store `inst` in the current storage.
+
+        Parameters:
+            inst: A DLite Instance to store in the BSON storage.
 
-    def load(self, uuid):
-        """Load `uuid` from current storage and return it
-        as a new instance.
         """
-        if uuid in self.d.keys():
-            return instance_from_dict(self.d[uuid])
-        else:
-            raise KeyError(f"Instance with id '{uuid}' not found")
-
-    def save(self, inst):
-        """Store `inst` in the current storage."""
-        self.d[inst.uuid] = inst.asdict(soft7=dlite.asbool(self.options.soft7))
-
-    def queue(self, pattern=None):
-        """Generator method that iterates over all UUIDs in
-        the storage who's metadata URI matches global pattern
-        `pattern`.
+        self._data[inst.uuid] = inst.asdict(soft7=dlite.asbool(self.options.soft7))
+
+    def queue(self, pattern: "Optional[str]" = None) -> "Generator[str, None, None]":
+        """Generator method that iterates over all UUIDs in the storage whose metadata
+        URI matches global pattern.
+
+        Parameters:
+            pattern: A regular expression to filter the yielded UUIDs.
+
+        Yields:
+            DLite Instance UUIDs based on the `pattern` regular expression.
+            If no `pattern` is given, all UUIDs are yielded from within the RDF
+            storage.
+
         """
-        for uuid, d in self.d.items():
-            if pattern and dlite.globmatch(pattern, d['meta']):
+        for uuid, data in self._data.items():
+            if pattern and dlite.globmatch(pattern, data["meta"]):
                 continue
             yield uuid
diff --git a/storages/python/python-storage-plugins/csv.py b/storages/python/python-storage-plugins/csv.py
index c2a6fcfa2..63f87db32 100644
--- a/storages/python/python-storage-plugins/csv.py
+++ b/storages/python/python-storage-plugins/csv.py
@@ -1,166 +1,237 @@
 """Storage plugin that reading/writing CSV files."""
-import sys
-import re
+from __future__ import annotations
+
 import warnings
 import hashlib
 import ast
 from pathlib import Path
+from typing import TYPE_CHECKING
 
 import pandas as pd
 
 import dlite
 from dlite.options import Options
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Any, Optional
+
 
-class csv(dlite.DLiteStorageBase):  # noqa: F821
+class csv(dlite.DLiteStorageBase):
     """DLite storage plugin for CSV files."""
 
-    def open(self, uri, options=None):
+    def open(self, uri: str, options: "Optional[str]" = None) -> None:
         """Opens `uri`, which should be a valid path to a local file.
 
-        Options
-        -------
-        mode: "r" | "w"
-            Whether to read or write data.  Required
-        meta: URI
-            URI to metadata describing the table to read.  Required if
-            `infer` is false.
-        infer: bool
-            Whether to infer metadata from data source.  Defaults to True
-        path: directories
-            Additional search directories to add to the search path for
-            `meta`.  Optional
-        pandas_opts: string
-            Comma-separated string of "key"=value options sent to pandas
-            read_<format> or save_<format> function.  String values should
-            be quoted.
-        format: "csv" | "excel" | "json" | "clipboard", ...
-            Any format supported by pandas.  The default is inferred from
-            the extension of `uri`.
-        id: string
-            Explicit id of returned instance if reading.  Optional
+        Parameters:
+            uri: A fully resolved URI to the CSV.
+            options: Supported options:
+
+                - mode: Whether to read or write data. Required.
+                  Valid values are:
+
+                  - `r`: Read data.
+                  - `w`: Write data.
+
+                - meta: URI to metadata describing the table to read.
+                  Required if `infer` is ``False``.
+                - infer: Whether to infer metadata from data source.
+                  Defaults to ``True``.
+                - path: Additional search directories to add to the search path for
+                  `meta`. Optional.
+                - pandas_opts: Comma-separated string of "key"=value options sent to
+                  pandas read_<format> or save_<format> function. String values should
+                  be quoted.
+                - format: Any format supported by pandas. The default is inferred from
+                  the extension of `uri`.
+                - id: Explicit id of returned instance if reading. Optional.
+
         """
-        self.options = Options(options, defaults='mode=r')
-        self.mode = dict(r='rt', w='wt')[self.options.mode]
-        self.readable = True  if 'rt' in self.mode else False
-        self.writable = False if 'rt' == self.mode else True
+        self.options = Options(options, defaults="mode=r")
+        self.mode = {"r": "rt", "w": "wt"}[self.options.mode]
+        self.readable = "rt" in self.mode
+        self.writable = "rt" != self.mode
         self.generic = False
         self.uri = uri
-        self.format = get_pandas_format_name(uri, self.options.get('format'))
+        self.format = get_pandas_format_name(uri, self.options.get("format"))
 
-    def close(self):
+    def close(self) -> None:
         """Closes this storage."""
-        pass
 
-    def load(self, id=None):
-        """Loads `id` from current storage and return it as a new
-        instance."""
+    def load(self, id: "Optional[str]" = None) -> dlite.Instance:
+        """Loads `id` from current storage and return it as a new instance.
+
+        Parameters:
+            id: A UUID representing a DLite Instance to return from the CSV storage.
+
+        Returns:
+            A DLite Instance corresponding to the given `id` (UUID).
+
+        """
         # This will break recursive search for metadata using this plugin
         if id:
-            raise dlite.DLiteError('csv plugin does support loading an '
-                                   'instance with a given id')
+            raise dlite.DLiteError(
+                "csv plugin does support loading an instance with a given id"
+            )
 
-        reader = getattr(pd, 'read_' + self.format)
-        pdopts = optstring2keywords(self.options.get('pandas_opts', ''))
-        metaid = self.options.meta if 'meta' in self.options else None
+        reader = getattr(pd, f"read_{self.format}")
+        pdopts = optstring2keywords(self.options.get("pandas_opts", ""))
+        metaid = self.options.meta if "meta" in self.options else None
         data = reader(self.uri, **pdopts)
-        rows, columns = data.shape
+        rows, _ = data.shape
 
-        if 'infer' not in self.options or dlite.asbool(self.options.infer):
+        if "infer" not in self.options or dlite.asbool(self.options.infer):
             Meta = infer_meta(data, metaid, self.uri)
         elif metaid:
             Meta = dlite.get_instance(metaid)
         else:
             raise ValueError(
-                'csv option `meta` must be provided if `infer` if false')
+                "csv option `meta` must be provided if `infer` if false"
+            )
 
-        inst = Meta(dims=(rows, ), id=self.options.get('id'))
-        for i, name in enumerate(inst.properties):
+        inst = Meta(dims=(rows,), id=self.options.get("id"))
+        for i in range(len(inst.properties)):
             inst[i] = data.iloc[:, i]
 
         return inst
 
-    def save(self, inst):
-        """Stores `inst` in current storage."""
-        d = inst.asdict()
-        data = pd.DataFrame(d['properties'])
+    def save(self, inst: dlite.Instance) -> None:
+        """Stores `inst` in current storage.
+
+        Parameters:
+            inst: A DLite Instance to store in the CSV storage.
 
-        writer = getattr(data, 'to_' + self.format)
-        pdopts = optstring2keywords(self.options.get('pandas_opts', ''))
+        """
+        inst_as_dict = inst.asdict()
+        data = pd.DataFrame(inst_as_dict["properties"])
+
+        writer = getattr(data, f"to_{self.format}")
+        pdopts = optstring2keywords(self.options.get("pandas_opts", ""))
         writer(self.uri, **pdopts)
 
 
-def get_pandas_format_name(uri, format):
-    """Return Pandas format name corresponding to `format`.  If `format`
-     is None, the name is inferred from the extension of `uri`.
+def get_pandas_format_name(uri: str, format: str) -> str:
+    """Return Pandas format name corresponding to `format`.
+
+    If `format` is ``None``, the name is inferred from the extension of `uri`.
+
+    Parameters:
+            uri: A fully resolved URI to the CSV.
+            format: A format to be mapped to Pandas format.
+
+    Returns:
+        A Pandas format matching `format`.
+
+    """
+    fmt = format.lower() if format else Path(uri).suffix.lstrip(".").lower()
+    return {
+        "xls": "excel",
+        "xlsx": "excel",
+        "h5": "hdf",
+        "hdf5": "hdf",
+    }.get(fmt, fmt)
+
+
+def infer_prop_name(name: str) -> str:
+    """Return inferred property name from pandas column name.
+
+    Parameters:
+        name: Pandas column name.
+
+    Returns:
+        Inferred property name.
+
     """
-    fmt = format.lower() if format else Path(uri).suffix.lstrip('.').lower()
-    d = {
-        'xls': 'excel',
-        'xlsx': 'excel',
-        'h5': 'hdf',
-        'hdf5': 'hdf',
-    }
-    return d.get(fmt, fmt)
+    return name.strip(' "').rsplit("(", 1)[0].rsplit("[", 1)[0].strip().replace(
+        " ", "_"
+    )
 
 
-def infer_prop_name(name):
-    """Return inferred property name from pandas column name."""
-    return name.strip(' "').rsplit('(', 1)[0].rsplit(
-        '[', 1)[0].strip().replace(' ', '_')
+def infer_prop_unit(name: str) -> "Optional[str]":
+    """Return inferred property unit from pandas column name.
+
+    Parameters:
+        name: Pandas column name.
+
+    Returns:
+        Inferred property unit.
+
+    """
+    if "(" in name:
+        return name.strip(' "').rsplit("(", 1)[1].strip().rstrip(")").rstrip()
 
+    if "[" in name:
+        return name.strip(' "').rsplit("[", 1)[1].strip().rstrip("]").rstrip()
 
-def infer_prop_unit(name):
-    """Return inferred property unit from pandas column name."""
-    if '(' in name:
-        return name.strip(' "').rsplit('(', 1)[1].strip().rstrip(')').rstrip()
-    elif '[' in name:
-        return name.strip(' "').rsplit('[', 1)[1].strip().rstrip(']').rstrip()
-    else:
-        return None
+    return None
 
 
-def infer_meta(data, metauri, uri):
+def infer_meta(data: pd.DataFrame, metauri: str, uri: str) -> dlite.Metadata:
     """Infer dlite metadata from Pandas dataframe `data`.
-    `metauri` is a namespace/version/name URI for the metadata.
-    `uri` is the location of the input storage.
+
+    Parameters:
+        data: A Pandas DataFrame.
+        metauri: A namespace/version/name URI for the metadata.
+        uri: The location of the input storage.
+
+    Returns:
+        A DLite Metadata based on the Pandas DataFrame.
+
     """
     if not metauri:
-        ext = Path(uri).suffix.lstrip('.')
-        fmt = ext if ext else 'csv'
-        with open(uri, 'rb') as f:
-            hash = hashlib.sha256(f.read()).hexdigest()
-        metauri = f'http://onto-ns.com/meta/1.0/generated_from_{fmt}_{hash}'
+        ext = Path(uri).suffix.lstrip(".")
+        fmt = ext if ext else "csv"
+
+        with open(uri, "rb") as handle:
+            hash = hashlib.sha256(handle.read()).hexdigest()
+
+        metauri = f"http://onto-ns.com/meta/1.0/generated_from_{fmt}_{hash}"
     elif dlite.has_instance(metauri):
-        warnings.warn(f'csv option infer is true, but explicit instance id '
-                      f'"{metauri}" already exists')
+        warnings.warn(
+            f"csv option infer is true, but explicit instance id {metauri!r} already "
+            "exists"
+        )
 
-    dims_ = [dlite.Dimension('rows', 'Number of rows.')]
+    dims = [dlite.Dimension("rows", "Number of rows.")]
     props = []
     for i, col in enumerate(data.columns):
         name = infer_prop_name(col)
         type = data.dtypes[i].name
-        if type == 'object':
-            type = 'string'
-        dims = ['rows']
+        if type == "object":
+            type = "string"
+        col_dims = ["rows"]
         unit = infer_prop_unit(col)
-        props.append(dlite.Property(name=name, type=type, dims=dims,
-                                    unit=unit, description=None))
-    descr = f'Inferred metadata for {uri}'
-
-    return dlite.Instance.create_metadata(metauri, dims_, props, descr)
+        props.append(
+            dlite.Property(
+                name=name,
+                type=type,
+                dims=col_dims,
+                unit=unit,
+                description=None,
+            )
+        )
+
+    return dlite.Instance.create_metadata(
+        metauri, dims, props, f"Inferred metadata for {uri}"
+    )
+
+
+def optstring2keywords(optstring: str) -> "dict[str, Any]":
+    """Converts comma-separated ``"key":value`` options string `optstring`
+    to a keyword dict and returns it.
+
+    The values should be valid Python expressions. They are parsed with
+    ast.literal_eval().
 
+    Parameters:
+        optstring: Commma-separated options string.
 
-def optstring2keywords(optstring):
-    """Converts comma-separated ``"key":value`` option string `optstring`
-    to a keyword dict and return it.
+    Returns:
+        Parsed options as a dictionary.
 
-    The values should be valid Python expressions.  They are parsed with
-    ast.literal_eval().
     """
-    s = '{%s}' % (optstring, )
     try:
-        return ast.literal_eval(s)
-    except Exception as e:
+        return ast.literal_eval("{%s}" % (optstring,))
+    except Exception as exc:
         raise ValueError(
-            f'invalid in option string ({e.__name__}): {optstring!r}')
+            f"invalid in option string ({exc.__name__}): {optstring!r}"
+        ) from exc
diff --git a/storages/python/python-storage-plugins/postgresql.py b/storages/python/python-storage-plugins/postgresql.py
index a6e96efaa..41f30e7a0 100644
--- a/storages/python/python-storage-plugins/postgresql.py
+++ b/storages/python/python-storage-plugins/postgresql.py
@@ -1,7 +1,6 @@
-import os
-import sys
-import warnings
+"""PostgreSQL storage"""
 import fnmatch
+from typing import TYPE_CHECKING
 
 import psycopg2
 from psycopg2 import sql
@@ -10,204 +9,266 @@
 from dlite.options import Options
 from dlite.utils import instance_from_dict
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Generator, Optional
 
-# Translation table from dlite types to postgresql types
+
+# Mapping from DLite types to PostgreSQL types
 pgtypes = {
-    'blob': 'bytea',
-    'bool': 'bool',
-    'int': 'integer',
-    'int8': 'bytea',
-    'int16': 'smallint',
-    'int32': 'integer',
-    'int64': 'bigint',
-    'uint16': 'integer',
-    'uint32': 'bigint',
-    'float': 'real',
-    'double': 'float8',
-    'float32': 'real',
-    'float64': 'float8',
-    'string': 'varchar',
-    'dimension': 'varchar[2]',
-    'property': 'varchar[5]',
-    'relation': 'varchar[3]',
+    "blob": "bytea",
+    "bool": "bool",
+    "int": "integer",
+    "int8": "bytea",
+    "int16": "smallint",
+    "int32": "integer",
+    "int64": "bigint",
+    "uint16": "integer",
+    "uint32": "bigint",
+    "float": "real",
+    "double": "float8",
+    "float32": "real",
+    "float64": "float8",
+    "string": "varchar",
+    "dimension": "varchar[2]",
+    "property": "varchar[5]",
+    "relation": "varchar[3]",
 }
 
-def to_pgtype(typename):
-    """Returns PostGreSQL type corresponding to dlite typename."""
-    if typename in pgtypes:
-        return pgtypes[typename]
-    else:
-        t = typename.rstrip('0123456789')
-        return pgtypes[t]
+def to_pgtype(typename: str) -> str:
+    """Returns PostgreSQL type corresponding to dlite typename."""
+    return pgtypes.get(
+        typename,
+        pgtypes[typename.rstrip("0123456789")]
+    )
 
 
 class postgresql(dlite.DLiteStorageBase):
     """DLite storage plugin for PostgreSQL."""
-    def open(self, uri, options=None):
+
+    def open(self, uri: str, options: "Optional[str]" = None) -> None:
         """Opens `uri`.
 
-        The `options` argument provies additional input to the driver.
-        Which options that are supported varies between the plugins.  It
-        should be a valid URL query string of the form:
+        After the options are passed, this method may set attribute
+        `writable` to `True` if it is writable and to `False` otherwise.
+        If `writable` is not set, it is assumed to be true.
 
-            key1=value1;key2=value2...
+        Parameters:
+            uri: A fully resolved URI to the PostgreSQL database.
+            options: This argument provides additional input to the driver.
+                Which options that are supported varies between the plugins. It
+                should be a valid URL query string of the form:
 
-        An ampersand (&) may be used instead of the semicolon (;).
+                ```python
+                options = "key1=value1;key2=value2;..."
+                ```
 
-        Typical options supported by most drivers include:
-        - database : Name of database to connect to (default: dlite)
-        - user : User name.
-        - password : Password.
-        - mode : append | r
-            Valid values are:
-            - append   Append to existing file or create new file (default)
-            - r        Open existing file for read-only
+                An ampersand (`&`) may be used instead of the semicolon (`;`) as a separator
+                between the key/value-pairs.
+
+                Typical options supported by most drivers include:
+
+                - `database`: Name of database to connect to (default: dlite).
+                - `user`: User name.
+                - `password`: User password.
+                - `mode`: Mode for opening.
+                  Valid values are:
+
+                  - `append`: Append to existing file or create new file (default).
+                  - `r`: Open existing file for read-only.
 
-        After the options are passed, this method may set attribute
-        `writable` to true if it is writable and to false otherwise.
-        If `writable` is not set, it is assumed to be true.
         """
-        self.options = Options(options, defaults='database=dlite;mode=append')
-        opts = self.options
-        opts.setdefault('password', None)
-        self.writable = False if opts.mode == 'r' else True
+        self.options = Options(options, defaults="database=dlite;mode=append")
+        self.options.setdefault("password", None)
+        self.writable = self.options.mode != "r"
 
         # Connect to existing database
-        print('  host:', uri)
-        print('  user:', opts.user)
-        print('  database:', opts.database)
-        #print('  password:', opts.password)
-        self.conn = psycopg2.connect(host=uri, database=opts.database,
-                                     user=opts.user, password=opts.password)
+        self.connection = psycopg2.connect(
+            host=uri,
+            database=self.options.database,
+            user=self.options.user,
+            password=self.options.password,
+        )
 
         # Open a cursor to perform database operations
-        self.cur = self.conn.cursor()
+        self.cursor = self.connection.cursor()
 
-    def close(self):
+    def close(self) -> None:
         """Closes this storage."""
-        self.cur.close()
-        self.conn.close()
+        self.cursor.close()
+        self.connection.close()
+
+    def load(self, uuid: str) -> dlite.Instance:
+        """Loads `uuid` from current storage and returns it as a new instance.
+
+        Parameters:
+            uuid: The DLite Instance UUID.
+
+        Returns:
+            The DLite Instance corresponding to the UUID given as it exists in the
+            PostgreSQL database.
 
-    def load(self, uuid):
-        """Loads `uuid` from current storage and return it as a new instance."""
+        """
         uuid = dlite.get_uuid(uuid)
-        q = sql.SQL('SELECT meta FROM uuidtable WHERE uuid = %s')
-        self.cur.execute(q, [uuid])
-        metaid, = self.cur.fetchone()
-        q = sql.SQL('SELECT * FROM {} WHERE uuid = %s').format(
-            sql.Identifier(metaid))
-        self.cur.execute(q, [uuid])
-        tokens = self.cur.fetchone()
+
+        sql_query = sql.SQL("SELECT meta FROM uuidtable WHERE uuid = %s")
+        self.cursor.execute(sql_query, [uuid])
+        metaid = self.cursor.fetchone()
+
+        if metaid is None or len(metaid) != 1:
+            raise RuntimeError(f"Could not retrieve meta ID for UUID {uuid}")
+
+        sql_query = sql.SQL("SELECT * FROM {} WHERE uuid = %s").format(
+            sql.Identifier(metaid[0])
+        )
+        self.cursor.execute(sql_query, [uuid])
+        tokens = self.cursor.fetchone()
         uuid_, uri, metaid_, dims = tokens[:4]
         values = tokens[4:]
-        assert uuid_ == uuid
-        assert metaid_ == metaid
 
-        # Make sure we have metadata object correcponding to metaid
+        if uuid_ != uuid:
+            raise RuntimeError(
+                f"Fetching {uuid} from PostgreSQL database results in a different "
+                f"UUID to be returned: {uuid_}"
+            )
+        if metaid_ != metaid:
+            raise RuntimeError(
+                f"Fetching {metaid} from PostgreSQL database results in a different "
+                f"meta ID to be returned: {metaid_}"
+            )
+
+        # Make sure we have a metadata object corresponding to metaid
         try:
             with dlite.err():
-                meta = dlite.get_instance(metaid)
+                dlite.get_instance(metaid)
         except RuntimeError:
             dlite.errclr()
-            meta = self.load(metaid)
+            self.load(metaid)
 
-        inst = dlite.Instance.from_metaid(metaid, dims, uri)
+        inst: dlite.Instance = dlite.Instance.from_metaid(metaid, dims, uri)
 
-        for i, p in enumerate(inst.meta['properties']):
-            inst.set_property(p.name, values[i])
+        for index, meta_property in enumerate(inst.meta["properties"]):
+            inst.set_property(meta_property.name, values[index])
 
-        # The uuid will be wrong for data instances, so override it
+        # The UUID will be wrong for data instances, so override it
         if not inst.is_metameta:
-            d = inst.asdict()
-            d['uuid'] = uuid
-            inst = instance_from_dict(d)
+            inst_dict = inst.asdict()
+            inst_dict["uuid"] = uuid
+            inst = instance_from_dict(inst_dict)
+
         return inst
 
-    def save(self, inst):
-        """Stores `inst` in current storage."""
+    def save(self, inst: dlite.Instance) -> None:
+        """Stores `inst` in current storage.
 
+        Parameters:
+            inst: The DLite Instance to store in the PostgreSQL database.
+
+        """
         # Save to metadata table
-        if not self.table_exists(inst.meta.uri):
-            self.table_create(inst.meta, inst.dimensions.values())
-        colnames = ['uuid', 'uri', 'meta', 'dims'] +  [
-            p.name for p in inst.meta['properties']]
-        q = sql.SQL('INSERT INTO {0} ({1}) VALUES ({2});').format(
+        if not self._table_exists(inst.meta.uri):
+            self._table_create(inst.meta)
+        colnames = ["uuid", "uri", "meta", "dims"] + [
+            meta_property.name for meta_property in inst.meta["properties"]
+        ]
+        sql_query = sql.SQL("INSERT INTO {0} ({1}) VALUES ({2});").format(
             sql.Identifier(inst.meta.uri),
-            sql.SQL(', ').join(map(sql.Identifier, colnames)),
-            (sql.Placeholder() * len(colnames)).join(', '))
-        values = [inst.uuid,
-                  inst.uri,
-                  inst.meta.uri,
-                  list(inst.dimensions.values()),
-        ] + [dlite.standardise(v, inst.get_property_descr(k), asdict=False)
-             for k, v in inst.properties.items()]
+            sql.SQL(", ").join(map(sql.Identifier, colnames)),
+            (sql.Placeholder() * len(colnames)).join(", ")
+        )
+        values = [
+            inst.uuid,
+            inst.uri,
+            inst.meta.uri,
+            list(inst.dimensions.values()),
+        ] + [
+            dlite.standardise(value, inst.get_property_descr(key), asdict=False)
+            for key, value in inst.properties.items()
+        ]
         try:
-            self.cur.execute(q, values)
+            self.cursor.execute(sql_query, values)
         except psycopg2.IntegrityError:
-            self.conn.rollback()  # Instance already in database
+            self.connection.rollback()  # Instance already in database
             return
 
         # Save to uuidtable
-        if not self.table_exists('uuidtable'):
-            self.uuidtable_create()
-        q = sql.SQL('INSERT INTO uuidtable (uuid, meta) VALUES (%s, %s);')
-        self.cur.execute(q, [inst.uuid, inst.meta.uri])
-        self.conn.commit()
+        if not self._table_exists("uuidtable"):
+            self._uuidtable_create()
+        sql_query = sql.SQL("INSERT INTO uuidtable (uuid, meta) VALUES (%s, %s);")
+        self.cursor.execute(sql_query, [inst.uuid, inst.meta.uri])
+        self.connection.commit()
+
+    def instances(self, pattern: str) -> "Generator[str, None, None]":
+        """Generator method that iterates over all UUIDs in the storage
+        whose metadata URI matches glob pattern `pattern`.
 
-    def table_exists(self, table_name):
+        Parameters:
+            pattern: Regular expression to identify DLite Instance UUIDs.
+
+        Yields:
+            DLite Instance UUIDs matching the regular expression UUID pattern or all
+            instance UUIDs in the storage if no pattern is given.
+
+        """
+        if pattern:
+            # Convert glob patter to PostgreSQL regular expression
+            regex = "^{}".format(
+                fnmatch.translate(pattern).replace("\\Z(?ms)", "$")
+            )
+            sql_query = sql.SQL("SELECT uuid from uuidtable WHERE uuid ~ %s;")
+            self.cursor.execute(sql_query, [regex])
+        else:
+            sql_query = sql.SQL("SELECT uuid from uuidtable;")
+            self.cursor.execute(sql_query)
+
+        tokens = self.cursor.fetchone()
+
+        while tokens:
+            uuid, = tokens
+            yield uuid
+            tokens = self.cursor.fetchone()
+
+    def _table_exists(self, table_name: str) -> str:
         """Returns true if a table named `table_name` exists."""
-        self.cur.execute(
-            'SELECT EXISTS(SELECT * FROM information_schema.tables '
-            'WHERE table_name=%s);', (table_name, ))
-        return self.cur.fetchone()[0]
+        self.cursor.execute(
+            "SELECT EXISTS(SELECT * FROM information_schema.tables "
+            "WHERE table_name=%s);",
+            [table_name],
+        )
+        return self.cursor.fetchone()[0]
 
-    def table_create(self, meta, dims=None):
-        """Creates a table for storing instances of `meta`."""
+    def _table_create(self, meta: dlite.Metadata) -> None:
+        """Creates a table for storing instances of `meta`.
+
+        Parameters:
+            meta: Metadata around which to create an SQL table.
+
+        """
         table_name = meta.uri
-        if self.table_exists(table_name):
-            raise ValueError('Table already exists: %r' % table_name)
-        if dims:
-            dims = list(dims)
+        if self._table_exists(table_name):
+            raise ValueError(f"Table already exists: {table_name!r}")
         cols = [
-            'uuid char(36) PRIMARY KEY',
-            'uri varchar',
-            'meta varchar',
-            'dims integer[%d]' % meta.ndimensions
+            "uuid char(36) PRIMARY KEY",
+            "uri varchar",
+            "meta varchar",
+            f"dims integer[{meta.ndimensions}]"
         ]
-        for p in meta['properties']:
-            decl = f'"{p.name}" {to_pgtype(p.type)}'
-            if len(p.dims):
-                decl += '[]' * len(p.dims)
+        for meta_property in meta["properties"]:
+            decl = f"{meta_property.name} {to_pgtype(meta_property.type)}"
+            if len(meta_property.dims):
+                decl += "[]" * len(meta_property.dims)
             cols.append(decl)
-        q = sql.SQL('CREATE TABLE {} (%s);' %
-                    ', '.join(cols)).format(sql.Identifier(meta.uri))
-        self.cur.execute(q)
-        self.conn.commit()
 
-    def uuidtable_create(self):
+        sql_query = sql.SQL(
+            f"CREATE TABLE {{}} (%s);" %
+            ", ".join(cols)).format(sql.Identifier(meta.uri))
+        self.cursor.execute(sql_query)
+        self.connection.commit()
+
+    def _uuidtable_create(self) -> None:
         """Creates the uuidtable - a table mapping all uuid's to their
         metadata uri."""
-        q = sql.SQL('CREATE TABLE uuidtable ('
-                    'uuid char(36) PRIMARY KEY, '
-                    'meta varchar'
-                    ');')
-        self.cur.execute(q)
-        self.conn.commit()
-
-    def queue(self, pattern):
-        """Generator method that iterates over all UUIDs in the storage
-        who's metadata URI matches glob pattern `pattern`."""
-        if pattern:
-            # Convert glob patter to PostgreSQL regular expression
-            regex = '^{}'.format(fnmatch.translate(globex).replace(
-                '\\Z(?ms)', '$'))
-            q = sql.SQL('SELECT uuid from uuidtable WHERE uuid ~ %s;')
-            self.cur.execute(q, (regex, ))
-        else:
-            q = sql.SQL('SELECT uuid from uuidtable;')
-            self.cur.execute(q)
-        tokens = self.cur.fetchone()
-        while tokens:
-            uuid, = tokens
-            yield uuid
-            tokens = self.cur.fetchone()
+        sql_query = sql.SQL(
+            "CREATE TABLE uuidtable (uuid char(36) PRIMARY KEY, meta varchar);"
+        )
+        self.cursor.execute(sql_query)
+        self.connection.commit()
diff --git a/storages/python/python-storage-plugins/pyrdf.py b/storages/python/python-storage-plugins/pyrdf.py
index 681319e4a..ad0a9a769 100644
--- a/storages/python/python-storage-plugins/pyrdf.py
+++ b/storages/python/python-storage-plugins/pyrdf.py
@@ -1,6 +1,5 @@
 """A simple demonstrage of a DLite storage plugin written in Python."""
-import os
-import sys
+from typing import TYPE_CHECKING
 
 import rdflib
 from rdflib.util import guess_format
@@ -9,68 +8,98 @@
 from dlite.options import Options
 from dlite.rdf import DM, PUBLIC_ID, from_graph, to_graph
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Generator, Optional
+
 
 class pyrdf(dlite.DLiteStorageBase):
     """DLite storage plugin for RDF serialisation."""
 
-    def open(self, uri, options=None):
+    def open(self, uri: str, options: "Optional[str]" = None) -> None:
         """Opens `uri`.
 
-        Supported options:
-        - mode : "a" | "r" | "w"
-            Valid values are:
-            - a   Append to existing file or create new file (default)
-            - r   Open existing file for read-only
-            - w   Truncate existing file or create new file
-        - format : "turtle" | "xml" | "n3" | "nt" | "json-ld" | "nquads"...
-            File format.  For a complete list of valid formats, see
-            https://rdflib.readthedocs.io/en/stable/intro_to_parsing.html
-        - base_uri : str
-            Base URI that is prepended to the instance UUID or URI
-            (if it is not already a valid URI).
-        - base_prefix: str
-            Optional namespace prefix to use for `base_uri`.
-        - include_meta: bool
-            Whether to also serialise metadata.  The default
-            is to only include metadata if `inst` is a data object.
+        Parameters:
+            uri: A fully resolved URI to the RDF.
+            options: Supported options:
+
+                - `mode`: Mode for opening.
+                  Valid values are:
+
+                  - `a`: Append to existing file or create new file (default).
+                  - `r`: Open existing file for read-only.
+                  - `w`: Truncate existing file or create new file.
+
+                - `format`: File format. For a complete list of valid formats, see
+                  https://rdflib.readthedocs.io/en/stable/intro_to_parsing.html
+                  A sample list of valid format values: "turtle", "xml", "n3", "nt",
+                  "json-ld", "nquads".
+                - `base_uri`: Base URI that is prepended to the instance UUID or URI
+                  (if it is not already a valid URI).
+                - `base_prefix`: Optional namespace prefix to use for `base_uri`.
+                - `include_meta`: Whether to also serialise metadata. The default is to
+                  only include metadata if `inst` is a data object.
+
         """
-        self.options = Options(options, defaults='mode=a')
-        self.writable = False if 'r' in self.options.mode else True
+        self.options = Options(options, defaults="mode=a")
+        self.writable = "r" not in self.options.mode
         self.uri = uri
         self.format = (
-            self.options.format if 'format' in self.options else guess_format(
-                uri)
+            self.options.format
+            if "format" in self.options else guess_format(uri)
         )
         self.graph = rdflib.Graph()
-        if self.options.mode in 'ra':
+        if self.options.mode in "ra":
             self.graph.parse(uri, format=self.format, publicID=PUBLIC_ID)
 
-    def close(self):
+    def close(self) -> None:
         """Closes this storage."""
         if self.writable:
             self.graph.serialize(self.uri, format=self.format)
 
-    def load(self, id):
-        """Loads `uuid` from current storage and return it as a new instance."""
+    def load(self, id: str) -> dlite.Instance:
+        """Loads `uuid` from current storage and returns it as a new instance.
+
+        Parameters:
+            id: A UUID representing a DLite Instance to return from the RDF storage.
+
+        Returns:
+            A DLite Instance corresponding to the given `id` (UUID).
+
+        """
         return from_graph(self.graph, id)
 
-    def save(self, inst):
-        """Stores `inst` in current storage."""
+    def save(self, inst: dlite.Instance) -> None:
+        """Stores `inst` in current storage.
+
+        Parameters:
+            inst: A DLite Instance to store in the RDF storage.
+
+        """
         to_graph(
             inst,
             self.graph,
-            base_uri=self.options.get('base_uri'),
-            base_prefix=self.options.get('base_prefix'),
+            base_uri=self.options.get("base_uri"),
+            base_prefix=self.options.get("base_prefix"),
             include_meta=(
-                dlite.asbool(self.options) if 'include_meta' in self.options
+                dlite.asbool(self.options) if "include_meta" in self.options
                 else None
             ),
         )
 
-    def queue(self, pattern=None):
+    def queue(self, pattern: "Optional[str]" = None) -> "Generator[str, None, None]":
         """Generator method that iterates over all UUIDs in the storage
-        who's metadata URI matches glob pattern `pattern`."""
-        for s, p, o in self.graph.triples((None, DM.hasUUID, None)):
+        who"s metadata URI matches glob pattern `pattern`.
+
+        Parameters:
+            pattern: A regular expression to filter the yielded UUIDs.
+
+        Yields:
+            DLite Instance UUIDs based on the `pattern` regular expression.
+            If no `pattern` is given, all UUIDs are yielded from within the RDF
+            storage.
+
+        """
+        for s, _, o in self.graph.triples((None, DM.hasUUID, None)):
             metaid = str(list(self.graph.objects(s, DM.instanceOf))[0])
             if pattern and dlite.globmatch(pattern, metaid):
                 continue
diff --git a/storages/python/python-storage-plugins/yaml.py b/storages/python/python-storage-plugins/yaml.py
index 6e00782e9..5198caa7f 100644
--- a/storages/python/python-storage-plugins/yaml.py
+++ b/storages/python/python-storage-plugins/yaml.py
@@ -1,66 +1,109 @@
 """A simple demonstrage of a DLite storage plugin written in Python."""
 import os
-import sys
+from typing import TYPE_CHECKING
 
-import yaml as pyyaml
+import yaml as pyyaml  # To not clash with the current file name.
 
 import dlite
 from dlite.options import Options
 from dlite.utils import instance_from_dict
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Generator, Optional
+
 
 class yaml(dlite.DLiteStorageBase):
     """DLite storage plugin for YAML."""
 
-    def open(self, uri, options=None):
+    def open(self, uri: str, options: "Optional[str]" = None) -> None:
         """Opens `uri`.
 
-        Supported options:
-        - mode : "a" | "r" | "w"
-            Valid values are:
-            - a  Append to existing file or create new file (default)
-            - r  Open existing file for read-only
-            - w  Truncate existing file or create new file
-        - soft7 : bool
-            Whether to save using SOFT7 format.
-        - single : bool | "auto"
-            Whether the input is assumed to be in single-entity form.
-            The default (auto) will try to infer it automatically.
+        Parameters:
+            uri: A fully resolved URI to the PostgreSQL database.
+            options: Supported options:
+
+                - `mode`: Mode for opening.
+                  Valid values are:
+
+                  - `a`: Append to existing file or create new file (default).
+                  - `r`: Open existing file for read-only.
+                  - `w`: Truncate existing file or create new file.
+
+                - `soft7`: Whether to save using SOFT7 format.
+                - `single`: Whether the input is assumed to be in single-entity form.
+                  The default (`"auto"`) will try to infer it automatically.
+
         """
-        self.options = Options(options, defaults='mode=a;soft7=true;single=auto')
-        self.mode = dict(r='r', w='w', a='r+', append='r+')[self.options.mode]
-        self.readable = True if 'r' in self.mode else False
-        self.writable = False if 'r' == self.mode else True
+        self.options = Options(options, defaults="mode=a;soft7=true;single=auto")
+        self.mode = {"r": "r", "w": "w", "a": "r+", "append": "r+"}[self.options.mode]
+        self.readable = "r" in self.mode
+        self.writable = "r" != self.mode
         self.generic = True
         self.uri = uri
-        self.d = {}
-        if self.mode in ('r', 'r+'):
-            with open(uri, self.mode) as f:
-                d = pyyaml.safe_load(f)
-            if d:
-                self.d = d
-
-    def close(self):
+        self._data = {}
+
+        if self.mode in ("r", "r+"):
+            with open(uri, self.mode) as handle:
+                data = pyyaml.safe_load(handle)
+            if data:
+                self._data = data
+
+    def close(self) -> None:
         """Closes this storage."""
         if self.writable:
-            mode = ('w' if self.mode == 'r+' and not os.path.exists(self.uri)
-                    else self.mode)
-            with open(self.uri, mode) as f:
-                pyyaml.dump(self.d, f, default_flow_style=False, sort_keys=False)
+            mode = (
+                "w"
+                if self.mode == "r+" and not os.path.exists(self.uri)
+                else self.mode
+            )
+            with open(self.uri, mode) as handle:
+                pyyaml.dump(
+                    self._data,
+                    handle,
+                    default_flow_style=False,
+                    sort_keys=False,
+                )
+
+    def load(self, id: str) -> dlite.Instance:
+        """Loads `uuid` from current storage and return it as a new instance.
+
+        Parameters:
+            id: A UUID representing a DLite Instance to return from the RDF storage.
 
-    def load(self, id):
-        """Loads `uuid` from current storage and return it as a new instance."""
-        return instance_from_dict(self.d, id, single=self.options.single,
-                                  check_storages=False)
+        Returns:
+            A DLite Instance corresponding to the given `id` (UUID).
 
-    def save(self, inst):
-        """Stores `inst` in current storage."""
-        self.d[inst.uuid] = inst.asdict(soft7=dlite.asbool(self.options.soft7))
+        """
+        return instance_from_dict(
+            self._data,
+            id,
+            single=self.options.single,
+            check_storages=False,
+        )
+
+    def save(self, inst: dlite.Instance) -> None:
+        """Stores `inst` in current storage.
+
+        Parameters:
+            inst: A DLite Instance to store in the RDF storage.
+
+        """
+        self._data[inst.uuid] = inst.asdict(soft7=dlite.asbool(self.options.soft7))
 
-    def queue(self, pattern=None):
+    def queue(self, pattern: "Optional[str]" = None) -> "Generator[str, None, None]":
         """Generator method that iterates over all UUIDs in the storage
-        who's metadata URI matches glob pattern `pattern`."""
-        for uuid, d in self.d.items():
-            if pattern and dlite.globmatch(pattern, d['meta']):
+        who"s metadata URI matches glob pattern `pattern`.
+
+        Parameters:
+            pattern: A regular expression to filter the yielded UUIDs.
+
+        Yields:
+            DLite Instance UUIDs based on the `pattern` regular expression.
+            If no `pattern` is given, all UUIDs are yielded from within the RDF
+            storage.
+
+        """
+        for uuid, inst_as_dict in self._data.items():
+            if pattern and dlite.globmatch(pattern, inst_as_dict["meta"]):
                 continue
             yield uuid


Test patch:
diff --git a/bindings/python/tests/CMakeLists.txt b/bindings/python/tests/CMakeLists.txt
index dedc26fa1..0420351fc 100644
--- a/bindings/python/tests/CMakeLists.txt
+++ b/bindings/python/tests/CMakeLists.txt
@@ -17,7 +17,6 @@ set(tests
   test_transaction
   test_mapping
   test_rdf
-  test_triplestore
   test_postgresql_write
   test_postgresql_read
   )
diff --git a/bindings/python/tests/test_triplestore.py b/bindings/python/tests/test_triplestore.py
deleted file mode 100644
index 3251a7231..000000000
--- a/bindings/python/tests/test_triplestore.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# Skip test if rdflib is not available
-import textwrap
-from pathlib import Path
-
-from check_import import check_import
-
-from dlite.triplestore import (
-    en, Literal, Namespace, Triplestore, RDF, RDFS, XSD, OWL, SKOS
-)
-from dlite.triplestore.triplestore import function_id, NoSuchIRIError
-
-rdflib = check_import("rdflib", skip=True)
-pytest = check_import("pytest")
-ontopy = check_import("ontopy")
-
-
-thisdir = Path(__file__).absolute().parent
-ontopath_family = thisdir / "ontologies" / "family.ttl"
-ontopath_food = thisdir / "ontologies" / "food.ttl"
-
-
-# Test namespaces
-# ---------------
-assert str(RDF) == "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-assert RDF.type == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
-
-FAM = Namespace(
-    "http://onto-ns.com/ontologies/examples/family#",
-    check=True,
-    triplestore_url=ontopath_family,
-)
-FOOD = Namespace(
-    "http://onto-ns.com/ontologies/examples/food#",
-    label_annotations=True,
-    check=True,
-    triplestore_url=ontopath_food,
-)
-FOOD2 = Namespace(
-    "http://onto-ns.com/ontologies/examples/food#",
-    label_annotations=True,
-    check=False,
-    triplestore_url=ontopath_food,
-)
-assert FAM.Son == "http://onto-ns.com/ontologies/examples/family#Son"
-assert FAM["Son"] == "http://onto-ns.com/ontologies/examples/family#Son"
-assert FAM + "Son" == "http://onto-ns.com/ontologies/examples/family#Son"
-
-name = "FOOD_345ecde3_3cac_41d2_aad6_cb6835a27b41"
-assert FOOD[name] == FOOD + name
-assert FOOD.Vegetable == FOOD + name
-
-assert FOOD2[name] == FOOD2 + name
-assert FOOD2.Vegetable == FOOD2 + name
-
-if pytest:
-    with pytest.raises(NoSuchIRIError):
-        FAM.NonExisting
-
-    with pytest.raises(NoSuchIRIError):
-        FOOD.NonExisting
-
-assert FOOD2.NonExisting == FOOD2 + "NonExisting"
-
-
-# Test RDF literals
-# -----------------
-l1 = Literal("Hello world!")
-assert l1 == "Hello world!"
-assert isinstance(l1, str)
-assert l1.lang is None
-assert l1.datatype is None
-assert l1.to_python() == "Hello world!"
-assert l1.value == "Hello world!"
-assert l1.n3() == '"Hello world!"'
-
-l2 = Literal("Hello world!", lang="en")
-assert l2.lang == "en"
-assert l2.datatype == None
-assert l2.value == "Hello world!"
-assert l2.n3() == '"Hello world!"@en'
-
-l3 = en("Hello world!")
-assert l3.n3() == '"Hello world!"@en'
-
-l4 = Literal(42)
-assert l4.lang == None
-assert l4.datatype == XSD.integer
-assert l4.value == 42
-assert l4.n3() == f'"42"^^{XSD.integer}'
-
-l5 = Literal(42, datatype=float)
-assert l5.lang == None
-assert l5.datatype == XSD.double
-assert l5.value == 42.0
-assert l5.n3() == f'"42"^^{XSD.double}'
-
-
-# Test rdflib triplestore backend
-# -------------------------------
-if rdflib:
-    ts = Triplestore("rdflib")
-    assert ts.expand_iri("xsd:integer") == XSD.integer
-    assert ts.prefix_iri(RDF.type) == 'rdf:type'
-    EX = ts.bind("ex", "http://example.com/onto#")
-    assert str(EX) == "http://example.com/onto#"
-    ts.add_mapsTo(
-        EX.MyConcept, "http://onto-ns.com/meta/0.1/MyEntity", "myprop")
-    ts.add((EX.MyConcept, RDFS.subClassOf, OWL.Thing))
-    ts.add((EX.AnotherConcept, RDFS.subClassOf, OWL.Thing))
-    ts.add((EX.Sum, RDFS.subClassOf, OWL.Thing))
-    assert ts.has(EX.Sum) == True
-    assert ts.has(EX.Sum, RDFS.subClassOf, OWL.Thing) == True
-    assert ts.has(object=EX.NotInOntology) == False
-
-
-    def sum(a, b):
-        """Returns the sum of `a` and `b`."""
-        return a + b
-
-    ts.add_function(sum, expects=(EX.MyConcept, EX.AnotherConcept),
-                    returns=EX.Sum, base_iri=EX)
-
-    s = ts.serialize(format="turtle")
-    fid = function_id(sum)
-    expected = textwrap.dedent(f"""\
-    @prefix dcterms: <http://purl.org/dc/terms/> .
-    @prefix ex: <http://example.com/onto#> .
-    @prefix fno: <https://w3id.org/function/ontology#> .
-    @prefix map: <http://emmo.info/domain-mappings#> .
-    @prefix owl: <http://www.w3.org/2002/07/owl#> .
-    @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
-    @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-
-    ex:sum_{fid} a fno:Function ;
-        dcterms:description "Returns the sum of `a` and `b`."@en ;
-        fno:expects ( ex:sum_{fid}_parameter1_a ex:sum_{fid}_parameter2_b ) ;
-        fno:returns ( ex:sum_{fid}_output1 ) .
-
-    <http://onto-ns.com/meta/0.1/MyEntity#myprop> map:mapsTo ex:MyConcept .
-
-    ex:AnotherConcept rdfs:subClassOf owl:Thing .
-
-    ex:Sum rdfs:subClassOf owl:Thing .
-
-    ex:sum_{fid}_output1 a fno:Output ;
-        map:mapsTo ex:Sum .
-
-    ex:sum_{fid}_parameter1_a a fno:Parameter ;
-        rdfs:label "a"@en ;
-        map:mapsTo ex:MyConcept .
-
-    ex:sum_{fid}_parameter2_b a fno:Parameter ;
-        rdfs:label "b"@en ;
-        map:mapsTo ex:AnotherConcept .
-
-    ex:MyConcept rdfs:subClassOf owl:Thing .
-
-    """)
-    assert s == expected
-
-
-
-    # Test SPARQL query
-    rows = ts.query("SELECT ?s ?o WHERE { ?s rdfs:subClassOf ?o }")
-    assert len(rows) == 3
-    rows.sort()  # ensure consistent ordering of rows
-    assert rows[0] ==  ('http://example.com/onto#AnotherConcept',
-                        'http://www.w3.org/2002/07/owl#Thing')
-    assert rows[1] == ('http://example.com/onto#MyConcept',
-                       'http://www.w3.org/2002/07/owl#Thing')
-    assert rows[2] == ('http://example.com/onto#Sum',
-                       'http://www.w3.org/2002/07/owl#Thing')
-
-
-    # Test adding mappings and functions
-    ts2 = Triplestore("rdflib")
-    ts2.parse(format="turtle", data=s)
-    assert ts2.serialize(format="turtle") == s
-    ts2.set((EX.AnotherConcept, RDFS.subClassOf, EX.MyConcept))
-
-    def cost(x):
-        return 2*x
-
-    ts2.add_mapsTo(EX.Sum, "http://onto-ns.com/meta/0.1/MyEntity#sum",
-                   cost=cost)
-    assert list(ts2.function_repo.values())[0] == cost
-
-    def func(x):
-        return x+1
-
-    ts2.add_function(func, expects=EX.Sum, returns=EX.OneMore, cost=cost)
-    assert list(ts2.function_repo.values())[1] == func
-    assert len(ts2.function_repo) == 2  # cost is only added once
-
-    def func2(x):
-        return x+2
-
-    def cost2(x):
-        return 2*x+1
-
-    ts2.add_function(func2, expects=EX.Sum, returns=EX.EvenMore, cost=cost2)
-    assert len(ts2.function_repo) == 4
-
-    #print(ts2.serialize(format="turtle"))
-
-
-# Test ontopy triplestore backend
-# -------------------------------
-if ontopy:
-    ts3 = Triplestore("ontopy", base_iri="emmo", load=True)
-    onto = ts3.backend.onto
-    triples = list(ts3.triples((None, None, None)))
-
-    ts4 = Triplestore(
-        "ontopy", base_iri="http://onto-ns.com/ontologies/examples/food",
-    )
-    ts4.parse(ontopath_food)
-
-    ts5 = Triplestore(
-        "ontopy", base_iri="http://onto-ns.com/ontologies/examples/food",
-    )
-    ts5.bind('food', FOOD)
-    with open(ontopath_food, 'rt') as f:
-        ts5.parse(data=f.read())
diff --git a/bindings/python/triplestore/test_units.py b/bindings/python/triplestore/test_units.py
index 33f0d77e8..59857446f 100644
--- a/bindings/python/triplestore/test_units.py
+++ b/bindings/python/triplestore/test_units.py
@@ -1,5 +1,4 @@
 from units import get_pint_registry
-from pint import UnitRegistry, Quantity
 
 
 ureg = get_pint_registry(force_recreate=True)
diff --git a/storages/python/tests-python/test_postgresql_storage_python.py b/storages/python/tests-python/test_postgresql_storage_python.py
index 03ebf84ad..72d2c8915 100644
--- a/storages/python/tests-python/test_postgresql_storage_python.py
+++ b/storages/python/tests-python/test_postgresql_storage_python.py
@@ -5,10 +5,7 @@
 from pathlib import Path
 
 sys.dont_write_bytecode = True
-import psycopg2
-from psycopg2 import sql
 
-import dlite
 from dlite.utils import instance_from_dict
 from run_python_storage_tests import print_test_exception
 
@@ -42,51 +39,57 @@
             
             df = '    def '
             ind8 = '        ' # Indent of 8 spaces
-            open_start = lines.index(df + 'open(self, uri, options=None):\n')
-            close_start = lines.index(df + 'close(self):\n')
-            load_start = lines.index(df + 'load(self, uuid):\n')
-            save_start = lines.index(df + 'save(self, inst):\n')
+            open_start = lines.index(df + 'open(self, uri: str, options: "Optional[str]" = None) -> None:\n')
+            close_start = lines.index(df + 'close(self) -> None:\n')
+            load_start = lines.index(df + 'load(self, uuid: str) -> dlite.Instance:\n')
+            save_start = lines.index(df + 'save(self, inst: dlite.Instance) -> None:\n')
             
             # open(): Don't connect to server - read 'uri' instead
             lines[open_start + 1] = ind8 + 'self.data = open_pgsql(uri)\n'
             lines[open_start + 2] = ind8 + 'self.d = {}\n'
+            lines[open_start + 3] = ind8 + "return None\n"
             
             # close(): Don't disconnect from server - just pass
-            lines[close_start + 1] = ind8 + 'pass\n'
+            lines[close_start + 2] = ind8 + 'pass\n'
             
             # load(): Don't access server - read from self.data
-            lines[load_start + 3] = ind8 + 'self.d[uuid] = ' \
+            lines[load_start + 11] = ind8 + 'self.d[uuid] = ' \
                 + 'load_pgsql(self.data, uuid, ["L", "M", "N"])\n'
-            lines[load_start + 4] = ind8 \
+            lines[load_start + 12] = ind8 \
                 + 'return instance_from_dict(self.d[uuid])\n'
             
             # save(): Don't write to database, but compare the writing
             # commands to the commands in the database dump file
-            n = save_start + 2
+            n = save_start + 8
             lines[n - 1] = ind8 + 'ret = {"uuid": inst.uuid}\n'
-            while not lines[n].startswith(df + 'table_exists'):
-                lines[n] = lines[n].replace('self.conn', '#')
-                lines[n] = lines[n].replace('self.cur.execute(', \
-                    'ret = extract_exec_args(ret, ')
-                if lines[n].startswith(ind8 + 'if not self.table'):
+            while not lines[n].startswith(df + 'instances'):
+                lines[n] = lines[n].replace('self.connection', '#')
+                lines[n] = lines[n].replace(
+                    'self.cursor.execute(',
+                    'ret = extract_exec_args(ret, ',
+                )
+                if lines[n].startswith(ind8 + 'if not self._table'):
                     lines[n] = '\n'
                     lines[n + 1] = '\n'
                 n += 1
             lines[n - 1] = ind8 + 'return ret\n'
+            lines = lines[:n]
             
-            del lines[(load_start + 5):(save_start - 1)]
+            # del lines[(load_start + 5):(save_start - 1)]
             del lines[(close_start + 2):(load_start - 1)]
-            del lines[(open_start + 3):(close_start - 1)]
+            del lines[(open_start + 4):(close_start - 1)]
+            # uuidtable_create_start = lines.index(df + "_uuidtable_create(self) -> None:\n")
+            # del lines[(uuidtable_create_start + 1):(uuidtable_create_start + 3)]
             s = 'from test_postgresql_storage_python import open_pgsql, ' \
-                + 'load_pgsql, extract_exec_args\n' + str().join(lines)
+                + 'load_pgsql, extract_exec_args\n' + "".join(lines)
             s = s.replace('class postgresql', 'class dlite_postgresql')
         exec(s)
-        
+
         # Load JSON metadata
         with open(dlite_path / 'src/tests/test-entity.json', 'r') as f:
             json_dict1 = json.load(f)
         json_dict1 = instance_from_dict(json_dict1).asdict()
-        
+
         # Test loading PostgreSQL metadata
         postgresql_inst1 = dlite_postgresql()
         postgresql_inst1.open(input_path / 'test_meta.pgsql')