diff --git a/python/dumux/common/properties.py b/python/dumux/common/properties.py
index d8dce6d9fb218e5b0031c5ab8fa209ecf512aaac..55d5d0a5ac62f731db36644aba8f5fb262f60469 100644
--- a/python/dumux/common/properties.py
+++ b/python/dumux/common/properties.py
@@ -1,55 +1,62 @@
-import os
-import string
-import random
-from dune.common.hashit import hashIt
-
 """
-Properties
+The DuMux property system in Python consisting of
+Property, TypeTag and Model
 """
 
+import os
+from dataclasses import dataclass
+from typing import List, Union
+from dune.common.hashit import hashIt
 
-class Property:
-    """Properties are used to construct a model"""
 
-    def __init__(self, object=None, value=None, type=None, includes=[], requiredProperties=[]):
-        if object is not None:
-            assert hasattr(object, "_typeName")
-            if type is not None or value is not None:
-                raise ValueError(
-                    "The Property constructor expects exactly one of the following arguments: object, type, or value."
-                )
-            if includes or requiredProperties:
-                raise ValueError(
-                    "The arguments includes and requiredProperties are ignored if the object argument is specified."
-                )
-            self._typeName = object._typeName
-            self._includes = object._includes if hasattr(object, "_includes") else []
-            self._requiredPropertyTypes = (
-                object._requiredPropertyTypes if hasattr(object, "_requiredPropertyTypes") else []
-            )
-        elif value is not None:
-            if object is not None or type is not None:
-                raise ValueError(
-                    "The Property constructor expects exactly one of the following arguments: object, type, or value."
-                )
-            if includes or requiredProperties:
-                raise ValueError(
-                    "The arguments includes and requiredProperties are ignored if the value argument is specified."
-                )
-            self._value = value
-        elif type is not None:
-            if object is not None or value is not None:
-                raise ValueError(
-                    "The Property constructor expects exactly one of the following arguments: object, type, or value."
-                )
-            self._typeName = type
-            self._includes = includes
-            self._requiredPropertyTypes = requiredProperties
-        else:
-            raise ValueError(
-                "The Property constructor expects exactly one of the following arguments: object, type, or value."
+@dataclass
+class Property:
+    """
+    Properties are used to construct a model
+    Instances of Property are created with
+    Property.fromInstance(...), Property.fromCppType(...),
+    or  Property.fromValue(...).
+    """
+
+    cppType: str = None
+    cppIncludes: List[str] = None
+    requiredPropertyTypes: List[str] = None
+    requiredPropertyValues: List[str] = None
+    value: Union[bool, int, float] = None
+
+    @classmethod
+    def fromInstance(cls, inst):
+        """Create a Property from an instance of a wrapper"""
+        if not hasattr(inst, "_typeName"):
+            raise TypeError(
+                "The given instance {inst} does not have an attribute _typeName. "
+                "Only generated C++ objects (with Python bindings) are accepted."
             )
 
+        return cls(cppType=inst._typeName, cppIncludes=inst._includes)
+
+    @classmethod
+    def fromCppType(
+        cls,
+        cppType: str,
+        *,
+        cppIncludes: List[str] = None,
+        requiredPropertyTypes: List[str] = None,
+        requiredPropertyValues: List[str] = None,
+    ):
+        """Create a Property from a given C++ type and includes"""
+        return cls(
+            cppType=cppType,
+            cppIncludes=cppIncludes,
+            requiredPropertyTypes=requiredPropertyTypes,
+            requiredPropertyValues=requiredPropertyValues,
+        )
+
+    @classmethod
+    def fromValue(cls, value):
+        """Create a Property from a given value"""
+        return cls(value=value)
+
 
 def typePropertyToString(propertyName, typeTagName, typeArg):
     """Converts a Type Property to a string"""
@@ -61,26 +68,23 @@ def typePropertyToString(propertyName, typeTagName, typeArg):
         propertyString += "    using type = {};\n".format("double")
     elif isinstance(typeArg, (int)):
         propertyString += "    using type = {};\n".format("int")
-    elif isinstance(typeArg, Property) or not isinstance(typeArg, (str)):
-        if hasattr(typeArg, "_requiredPropertyTypes") or hasattr(
-            typeArg, "_requiredPropertyValues"
-        ):
+    elif isinstance(typeArg, Property):
+        if typeArg.requiredPropertyTypes or typeArg.requiredPropertyValues:
             propertyString += "private:\n"
-        if hasattr(typeArg, "_requiredPropertyTypes"):
-            for reqProp in typeArg._requiredPropertyTypes:
+        if typeArg.requiredPropertyTypes is not None:
+            for reqProp in typeArg.requiredPropertyTypes:
                 propertyString += "    using {} = {};\n".format(
                     reqProp, "GetPropType<TypeTag, Properties::{}>".format(reqProp)
                 )
-
-        if hasattr(typeArg, "_requiredPropertyValues"):
-            for reqProp in typeArg._requiredPropertyValues:
+        if typeArg.requiredPropertyValues is not None:
+            for reqProp in typeArg.requiredPropertyValues:
                 reqPropLowerCase = reqProp[0].lower() + reqProp[1:]
                 propertyString += "    static constexpr auto {} = {};\n".format(
                     reqPropLowerCase, "getPropValue<TypeTag, Properties::{}>()".format(reqProp)
                 )
 
         propertyString += "public:\n"
-        propertyString += "    using type = {};\n".format(typeArg._typeName)
+        propertyString += "    using type = {};\n".format(typeArg.cppType)
 
     propertyString += "};"
 
@@ -96,22 +100,26 @@ def valuePropertyToString(propertyName, typeTagName, value):
     # make sure to get the correct C++ types and values
     if isinstance(value, bool):
         value = str(value).lower()
-        type = "bool"
+        cppType = "bool"
     elif isinstance(value, int):
-        type = "int"
-    else:
-        type = "Scalar"
+        cppType = "int"
+    elif isinstance(value, float):
+        cppType = "Scalar"
         propertyString += "\nprivate:\n"
         propertyString += "    using Scalar = GetPropType<TypeTag, Properties::Scalar>;\n"
         propertyString += "public:"
+    else:
+        raise ValueError(f"Invalid argument {value}. Expects bool, int or float.")
 
-    propertyString += "\n    static constexpr {} value = {};\n".format(type, value)
+    propertyString += "\n"
+    propertyString += f"    static constexpr {cppType} value = {value};"
+    propertyString += "\n"
     propertyString += "};"
 
     return propertyString
 
 
-TYPETAGS = {
+_typeTags = {
     "CCTpfaModel": {
         "include": "dumux/discretization/cctpfa.hh",
         "description": "A cell-centered two-point flux finite volume discretization scheme.",
@@ -132,79 +140,63 @@ def listTypeTags():
 
     print("\n**********************************\n")
     print("The following TypeTags are availabe:")
-    for key in TYPETAGS.keys():
-        print(key, ":", TYPETAGS[key]["description"])
+    for key, value in _typeTags.items():
+        print(key, ":", value["description"])
     print("\n**********************************")
 
 
-def getKnownProperties():
-    filepath = os.path.abspath(
+def predefinedProperties():
+    """Create a list of properties defined in properties.hh"""
+
+    propertiesHeader = os.path.abspath(
         os.path.dirname(__file__) + "/../../../../dumux/common/properties.hh"
     )
-    with open(filepath) as f:
-        result = []
-        for line in f:
+    with open(propertiesHeader) as header:
+        properties = []
+        for line in header:
             if line.startswith("struct"):
-                result.append(line.split(" ")[1])
-        return result
+                properties.append(line.split(" ")[1])
+        return properties
 
 
 class TypeTag:
-    knownProperties = getKnownProperties()
+    """TypeTags are inheritable collections of properties"""
 
-    def __init__(self, name=None, *, inheritsFrom=None, gridGeometry=None, scalar="double"):
+    knownProperties = predefinedProperties()
+
+    def __init__(self, name, *, inheritsFrom=None, gridGeometry=None):
         self.inheritsFrom = inheritsFrom
         self.includes = []
         self.properties = {}
         self.newPropertyDefinitions = []
         self.gridGeometry = gridGeometry
+        self.name = name
 
-        if name is not None:
-            self.name = name
-        else:
-            if gridGeometry is None and inheritsFrom is None:
-                self.name = "typetag_" + "".join(
-                    random.choices(string.ascii_uppercase + string.digits, k=8)
-                )
-            else:
-                self.name = "typetag_" + hashIt(
-                    "".join(inheritsFrom) + gridGeometry._typeName + scalar
-                )
-
-        if self.name in TYPETAGS.keys():
+        if self.name in _typeTags.keys():
             if inheritsFrom is not None:
                 raise ValueError(
-                    f"Existing TypeTag {name} cannot inherit from other TypeTags. Use TypeTag({name}) only."
+                    f"Existing TypeTag {name} cannot inherit from other TypeTags."
+                    f" Use TypeTag({name}) only."
                 )
             self.isExistingTypeTag = True
-            self.includes = [TYPETAGS[self.name]["include"]]
+            self.includes = [_typeTags[self.name]["include"]]
         else:
             self.isExistingTypeTag = False
 
-        if self.gridGeometry is not None:
-            discretizationMethod = self.gridGeometry.discMethod
-            map = {
-                "box": "BoxModel",
-                "cctpfa": "CCTpfaModel",
-            }
-            if discretizationMethod in map:
-                self.inheritsFrom += [map[discretizationMethod]]
-
         if self.inheritsFrom is not None:
-            # treat existing TypeTags by converting the given string to a real TypeTag object
+            # treat existing TypeTags by converting the given string to a real TypeTag instance
             for idx, parentTypeTag in enumerate(self.inheritsFrom):
                 if not isinstance(parentTypeTag, TypeTag):
                     if not isinstance(parentTypeTag, str):
                         raise ValueError(
                             "Unknown parent TypeTag {}. Use either argument of type TypeTag "
-                            "or a string for an existing TypeTag. List of existing TypeTags: {}".format(
-                                parentTypeTag, TYPETAGS.keys()
-                            )
+                            "or a string for an existing TypeTag. "
+                            "List of existing TypeTags: {}".format(parentTypeTag, _typeTags.keys())
                         )
-                    if parentTypeTag not in TYPETAGS.keys():
+                    if parentTypeTag not in _typeTags.keys():
                         raise ValueError(
                             "Unknown TypeTag {}. List of existing TypeTags: {}".format(
-                                parentTypeTag, TYPETAGS.keys()
+                                parentTypeTag, _typeTags.keys()
                             )
                         )
                     self.inheritsFrom[idx] = TypeTag(parentTypeTag)
@@ -218,13 +210,8 @@ class TypeTag:
                     for include in parentTypeTag.includes:
                         self.includes.append(include)
 
-        self._typeName = "Dumux::Properties::TTag::" + self.name
-
-        # set the scalar type
-        self.__setitem__("Scalar", Property(type=scalar))
-
-    # the [] operator for setting values
-    def __setitem__(self, key, value):
+    def __setitem__(self, key, value: Property):
+        """the [] operator for setting values"""
         if not isinstance(value, Property):
             raise ValueError("Only values of type Property can be assigned to a model")
 
@@ -233,20 +220,23 @@ class TypeTag:
             self.newPropertyDefinitions += [key]
 
         self.properties[key] = value
-        if hasattr(value, "_includes"):
-            for include in value._includes:
+        if value.cppIncludes is not None:
+            for include in value.cppIncludes:
                 self.includes.append(include)
 
-    # the [] operator for getting values
     def __getitem__(self, key):
+        """the [] operator for getting values"""
         return self.properties[key]
 
-    # returns the TypeTag as a string
-    def getTypeTag(self):
+    @property
+    def cppType(self):
+        """Returns the TypeTag as a string"""
         return "Dumux::Properties::TTag::" + self.name
 
-    # creates a string resembling a properties.hh file
-    def getProperties(self):
+    @property
+    def cppHeader(self):
+        """creates a string resembling a properties.hh file"""
+
         file = "#ifndef DUMUX_{}_PROPERTIES_HH\n".format(self.name.upper())
         file += "#define DUMUX_{}_PROPERTIES_HH\n\n".format(self.name.upper())
 
@@ -281,15 +271,15 @@ class TypeTag:
             file += "struct " + newDef + " { using type =  UndefinedProperty; };\n\n"
 
         for prop in self.properties:
-            if hasattr(self[prop], "_value"):
-                file += valuePropertyToString(prop, self.name, self[prop]._value) + "\n\n"
+            if self[prop].value is not None:
+                file += valuePropertyToString(prop, self.name, self[prop].value) + "\n\n"
             else:
                 file += typePropertyToString(prop, self.name, self[prop]) + "\n\n"
 
         if self.gridGeometry is not None:
             file += (
                 typePropertyToString(
-                    "Grid", self.name, Property(type="typename TypeTag::GridGeometry::Grid")
+                    "Grid", self.name, Property.fromCppType("typename TypeTag::GridGeometry::Grid")
                 )
                 + "\n\n"
             )
@@ -301,5 +291,23 @@ class TypeTag:
 
 
 class Model(TypeTag):
-    # TODO maybe rename TypeTag to Model and remove this class here
-    pass
+    """A DuMux model specifies all properties necessary to build a DuMux simulator"""
+
+    def __init__(self, *, inheritsFrom: List[str], gridGeometry, scalar: str = "double"):
+        # generate a generic name
+        genericName = "TypeTag" + hashIt("".join(inheritsFrom) + gridGeometry._typeName + scalar)
+
+        # deduce the discretization tag from the grid geometry
+        discretizationMethod = gridGeometry.discMethod
+        discretizationMap = {
+            "box": "BoxModel",
+            "cctpfa": "CCTpfaModel",
+        }
+        if discretizationMethod in discretizationMap:
+            inheritsFrom += [discretizationMap[discretizationMethod]]
+
+        # call parent constructor
+        super().__init__(name=genericName, inheritsFrom=inheritsFrom, gridGeometry=gridGeometry)
+
+        # set the scalar type
+        self.__setitem__("Scalar", Property.fromCppType(scalar))