Repository: zincware/ZnTrack

Issue:
Can not inherit from Node
Something like the following currently does not work,

```py

class Base(Node)
   deps = zn.deps()
   ...

class Method1(Base)
   def run(self):
        ...
```

Discussion:

Patch:
diff --git a/examples/docs/02_PassingClasses.ipynb b/examples/docs/02_PassingClasses.ipynb
index 868e419..5555ac2 100644
--- a/examples/docs/02_PassingClasses.ipynb
+++ b/examples/docs/02_PassingClasses.ipynb
@@ -54,41 +54,147 @@
   },
   {
    "cell_type": "markdown",
-   "id": "3d4248c9-df8d-477f-bef4-fee69324ec60",
+   "source": [
+    "## Inheriting from Base-Nodes\n",
+    "\n",
+    "The next part of the documentation will show how you can pass a Python class to a Node to enable different methods.\n",
+    "Whilst this can be very useful it is often easier to create a Base-Node and define custom methods as subclass of this Base.\n"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "183551fb-2f96-49de-8276-bf4ea5afc53d",
    "metadata": {},
+   "outputs": [],
+   "source": [
+    "from zntrack import Node, zn"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "outputs": [],
+   "source": [
+    "class NumberManipulationBase(Node):\n",
+    "    node_name = \"NumberManipulationBase\"\n",
+    "    # define the node_name for all child classes. Otherwise, child classes can coexist.\n",
+    "    input_number = zn.params()\n",
+    "    output_number = zn.outs()\n",
+    "\n",
+    "\n",
+    "class MultiplyNumber(NumberManipulationBase):\n",
+    "    factor = zn.params()\n",
+    "\n",
+    "    def run(self):\n",
+    "        self.output_number = self.input_number * self.factor\n",
+    "\n",
+    "\n",
+    "class DivideNumber(NumberManipulationBase):\n",
+    "    divider = zn.params()\n",
+    "\n",
+    "    def run(self):\n",
+    "        self.output_number = self.input_number / self.divider"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "2022-03-26 18:47:08,865 (WARNING): Jupyter support is an experimental feature! Please save your notebook before running this command!\n",
+      "Submit issues to https://github.com/zincware/ZnTrack.\n",
+      "2022-03-26 18:47:11,353 (WARNING): Running DVC command: 'dvc run -n NumberManipulationBase ...'\n",
+      "30\n",
+      "2022-03-26 18:47:15,765 (WARNING): Running DVC command: 'dvc run -n NumberManipulationBase ...'\n",
+      "5.0\n"
+     ]
+    }
+   ],
+   "source": [
+    "MultiplyNumber(input_number=10, factor=3).write_graph(run=True)\n",
+    "print(MultiplyNumber.load().output_number)\n",
+    "\n",
+    "DivideNumber(input_number=10, divider=2).write_graph(run=True)\n",
+    "print(DivideNumber.load().output_number)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Due to lazy-loading you might be able to access the output of `DivideNumber` also through `NumberManipulationBase` and `MultiplyNumber`.\n",
+    "This is only possible for shared ZnTrackOptions between the Nodes.\n",
+    "If you try to access e.g. the `factor` you will get an Error because `factor` is not an attribute of `DivideNumber`."
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
    "source": [
     "## Creating Operations\n",
     "\n",
     "Best practice for adding different custom operations or methods is to inherit from a common parent with a method that does the computation."
-   ]
+   ],
+   "metadata": {
+    "collapsed": false
+   }
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
-   "id": "dd6ab631-01e4-4912-959e-006fad21e8b1",
-   "metadata": {},
+   "execution_count": 7,
    "outputs": [],
    "source": [
     "class Base:\n",
     "    def compute(self, inp):\n",
     "        raise NotImplementedError"
-   ]
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   }
   },
   {
    "cell_type": "markdown",
-   "id": "23828631-62dd-44a7-b91b-2cc0a1cf0a87",
-   "metadata": {},
    "source": [
     "For simplicity reasons we will look at some very simple functions but they can be of arbitrary complexity.\n",
     "We apply the `check_signature` decorator which is an optional check that the tests that the keyword arguments are identical to the class attribute names.\n",
     "This is mandatory for ZnTrack to work in the anticipated way."
-   ]
+   ],
+   "metadata": {
+    "collapsed": false
+   }
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
-   "id": "de1daf1f-f82e-4731-b1cb-19d0149c2916",
-   "metadata": {},
+   "execution_count": 8,
    "outputs": [],
    "source": [
     "from zntrack.utils.decorators import check_signature\n",
@@ -110,29 +216,26 @@
     "\n",
     "    def compute(self, inp):\n",
     "        return inp * self.factor"
-   ]
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   }
   },
   {
    "cell_type": "markdown",
-   "id": "de03e1ef-635b-40e8-bf26-630025f588d9",
-   "metadata": {},
    "source": [
     "The actual Node makes use of the typical ZnTrack functionality beeing extended by `zn.Method()`."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 6,
-   "id": "183551fb-2f96-49de-8276-bf4ea5afc53d",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from zntrack import Node, zn"
-   ]
+   ],
+   "metadata": {
+    "collapsed": false
+   }
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 9,
    "id": "38d22553-641c-42b4-a99b-9d27de2ac41d",
    "metadata": {},
    "outputs": [],
@@ -162,7 +265,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 10,
    "id": "35205014-bc4a-449e-8b48-e1936e35d985",
    "metadata": {},
    "outputs": [
@@ -170,9 +273,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "2022-02-22 13:36:04,657 (WARNING): Jupyter support is an experimental feature! Please save your notebook before running this command!\n",
-      "Submit issues to https://github.com/zincware/ZnTrack.\n",
-      "2022-02-22 13:36:08,305 (WARNING): Running DVC command: 'dvc run -n Calculator ...'\n"
+      "2022-03-26 18:47:20,381 (WARNING): Running DVC command: 'dvc run -n Calculator ...'\n"
      ]
     }
    ],
@@ -190,7 +291,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 11,
    "id": "9065e7e7-2d3a-4e39-8126-36a426eb0c2b",
    "metadata": {},
    "outputs": [
@@ -198,7 +299,7 @@
      "data": {
       "text/plain": "15"
      },
-     "execution_count": 9,
+     "execution_count": 11,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -217,7 +318,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 12,
    "id": "5986d32f-c6be-4d67-9b8d-e0ef6dd0bef8",
    "metadata": {},
    "outputs": [
@@ -225,7 +326,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "2022-02-22 13:36:15,534 (WARNING): Running DVC command: 'dvc run -n Calculator ...'\n"
+      "2022-03-26 18:47:25,126 (WARNING): Running DVC command: 'dvc run -n Calculator ...'\n"
      ]
     }
    ],
@@ -235,7 +336,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 13,
    "id": "de686f58-74ae-4740-b624-561baa540389",
    "metadata": {},
    "outputs": [
@@ -243,7 +344,7 @@
      "data": {
       "text/plain": "20"
      },
-     "execution_count": 11,
+     "execution_count": 13,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -262,7 +363,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 14,
    "id": "621c9cf0-1f69-4596-bfa5-e52754f77fd4",
    "metadata": {},
    "outputs": [],
@@ -279,7 +380,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 15,
    "id": "53052562-b1ce-4865-89f3-da5740208660",
    "metadata": {},
    "outputs": [
@@ -287,7 +388,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "2022-02-22 13:36:22,570 (WARNING): Running DVC command: 'dvc run -n Calculator ...'\n"
+      "2022-03-26 18:47:29,923 (WARNING): Running DVC command: 'dvc run -n Calculator ...'\n"
      ]
     }
    ],
@@ -299,7 +400,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": 16,
    "id": "47670ed7-e235-4b1a-a7a4-37d5a48d79d7",
    "metadata": {},
    "outputs": [
@@ -307,7 +408,7 @@
      "data": {
       "text/plain": "25"
      },
-     "execution_count": 14,
+     "execution_count": 16,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -326,7 +427,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": 17,
    "id": "f2a10182-0129-41f6-bbd0-0e21648364ec",
    "metadata": {},
    "outputs": [],
@@ -350,7 +451,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 18,
    "id": "aec74308-8466-4509-a84c-b6dcd1dec8ca",
    "metadata": {},
    "outputs": [
@@ -358,7 +459,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "2022-02-22 13:36:29,476 (WARNING): Running DVC command: 'dvc run -n CombinedCalculator ...'\n"
+      "2022-03-26 18:47:34,537 (WARNING): Running DVC command: 'dvc run -n CombinedCalculator ...'\n"
      ]
     }
    ],
@@ -370,7 +471,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": 19,
    "id": "7e1b3b2b-79d3-4d79-88e8-26653c717520",
    "metadata": {},
    "outputs": [
@@ -378,7 +479,7 @@
      "data": {
       "text/plain": "25"
      },
-     "execution_count": 17,
+     "execution_count": 19,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -389,11 +490,15 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": null,
    "id": "6fc36519-35fd-4a3a-a55b-88618dace01b",
    "metadata": {
     "nbsphinx": "hidden",
-    "tags": []
+    "tags": [],
+    "pycharm": {
+     "name": "#%%\n",
+     "is_executing": true
+    }
    },
    "outputs": [],
    "source": [
diff --git a/zntrack/core/base.py b/zntrack/core/base.py
index 9030d2a..70c0a7b 100644
--- a/zntrack/core/base.py
+++ b/zntrack/core/base.py
@@ -2,10 +2,12 @@ from __future__ import annotations
 
 import inspect
 import logging
+import typing
 
 from zntrack import utils
 from zntrack.core.dvcgraph import GraphWriter
 from zntrack.core.zntrackoption import ZnTrackOption
+from zntrack.descriptor import get_descriptors
 from zntrack.zn.dependencies import NodeAttribute, getdeps
 
 log = logging.getLogger(__name__)
@@ -16,22 +18,22 @@ def get_auto_init_signature(cls) -> (list, list):
     and create a signature Parameter"""
     zn_option_names, signature_params = [], []
     _ = cls.__annotations__  # fix for https://bugs.python.org/issue46930
-    for name, item in cls.__dict__.items():
-        if isinstance(item, ZnTrackOption):
-            if item.zn_type in utils.VALUE_DVC_TRACKED:
-                # exclude zn.outs / metrics / plots / ... options
-                continue
-            # For the new __init__
-            zn_option_names.append(name)
-
-            # For the new __signature__
-            signature_params.append(
-                inspect.Parameter(
-                    name=name,
-                    kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
-                    annotation=cls.__annotations__.get(name),
-                )
+    descriptors = get_descriptors(ZnTrackOption, cls=cls)
+    for descriptor in descriptors:
+        if descriptor.zn_type in utils.VALUE_DVC_TRACKED:
+            # exclude zn.outs / metrics / plots / ... options
+            continue
+        # For the new __init__
+        zn_option_names.append(descriptor.name)
+
+        # For the new __signature__
+        signature_params.append(
+            inspect.Parameter(
+                name=descriptor.name,
+                kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
+                annotation=cls.__annotations__.get(descriptor.name),
             )
+        )
     return zn_option_names, signature_params
 
 
@@ -66,7 +68,7 @@ class LoadViaGetItem(type):
             return cls.load(**item)
         return cls.load(name=item)
 
-    def __matmul__(self, other: str) -> NodeAttribute:
+    def __matmul__(self, other: str) -> typing.Union[NodeAttribute, typing.Any]:
         """Shorthand for: getdeps(Node, other)
 
         Parameters
@@ -100,8 +102,8 @@ class Node(GraphWriter, metaclass=LoadViaGetItem):
     is_loaded: bool = False
 
     def __init__(self, **kwargs):
-        super().__init__(**kwargs)
         self.is_loaded = kwargs.pop("is_loaded", False)
+        super().__init__(**kwargs)
         for data in self._descriptor_list:
             if data.zn_type == utils.ZnTypes.DEPS:
                 update_dependency_options(data.default_value)
@@ -121,7 +123,7 @@ class Node(GraphWriter, metaclass=LoadViaGetItem):
         origin = super().__repr__()
         return f"{origin}(name={self.node_name})"
 
-    def __matmul__(self, other: str) -> NodeAttribute:
+    def __matmul__(self, other: str) -> typing.Union[NodeAttribute, typing.Any]:
         """Shorthand for: getdeps(Node, other)
 
         Parameters
@@ -141,18 +143,30 @@ class Node(GraphWriter, metaclass=LoadViaGetItem):
 
     def __init_subclass__(cls, **kwargs):
         """Add a dataclass-like init if None is provided"""
-
+        super().__init_subclass__(**kwargs)
         # User provides an __init__
-        if cls.__dict__.get("__init__") is not None:
-            return cls
+        for inherited in cls.__mro__:
+            # Go through the mro until you find the Node class.
+            # If found an init before that class it will implement super
+            # if not add the fields to the __init__ automatically.
+            if inherited == Node:
+                log.debug("Found Node instance - adding dataclass-like __init__")
+                break
+            elif inherited.__dict__.get("__init__") is not None:
+                if not getattr(inherited.__init__, "_uses_auto_init", False):
+                    return cls
 
         # attach an automatically generated __init__ if None is provided
         zn_option_names, signature_params = get_auto_init_signature(cls)
 
-        # Add new __init__ to the sub-class
-        setattr(cls, "__init__", utils.get_auto_init(fields=zn_option_names))
+        # Add new __init__ to the subclass
+        setattr(
+            cls,
+            "__init__",
+            utils.get_auto_init(fields=zn_option_names, super_init=Node.__init__),
+        )
 
-        # Add new __signature__ to the sub-class
+        # Add new __signature__ to the subclass
         signature = inspect.Signature(parameters=signature_params)
         setattr(cls, "__signature__", signature)
 
@@ -168,6 +182,10 @@ class Node(GraphWriter, metaclass=LoadViaGetItem):
             Set this option to True if they should be saved, e.g. in run_and_save
             If true changes in e.g. zn.params will not be saved.
         """
+        if not results:
+            # Reset everything in params.yaml and zntrack.json before saving
+            utils.file_io.clear_config_file(utils.Files.params, node_name=self.node_name)
+            utils.file_io.clear_config_file(utils.Files.zntrack, node_name=self.node_name)
         # Save dvc.<option>, dvc.deps, zn.Method
         for option in self._descriptor_list:
             if results:
@@ -238,15 +256,16 @@ class Node(GraphWriter, metaclass=LoadViaGetItem):
             lazy = utils.config.lazy
         try:
             instance = cls(name=name, is_loaded=True)
-        except TypeError:
+        except TypeError as type_error:
             try:
                 instance = cls()
                 if name not in (None, cls.__name__):
                     instance.node_name = name
                 log.warning(
                     "Can not pass <name> to the super.__init__ and trying workaround!"
-                    " This can lead to unexpected behaviour and can be avoided by passing"
-                    " ( **kwargs) to the super().__init__(**kwargs)"
+                    " This can lead to unexpected behaviour and can be avoided by"
+                    " passing ( **kwargs) to the super().__init__(**kwargs) - Received"
+                    f" '{type_error}'"
                 )
             except TypeError as err:
                 raise TypeError(
diff --git a/zntrack/core/dvcgraph.py b/zntrack/core/dvcgraph.py
index fdf0eab..5297f4a 100644
--- a/zntrack/core/dvcgraph.py
+++ b/zntrack/core/dvcgraph.py
@@ -201,30 +201,31 @@ class GraphWriter:
     """Write the DVC Graph
 
     Main method that handles writing the Graph / dvc.yaml file
+
+    node_name: str
+        first priority is by passing it through kwargs
+        second is having the class attribute set in the class definition
+        last if both above are None it will be set to __class__.__name__
     """
 
-    _node_name = None
+    node_name = None
     _module = None
 
     def __init__(self, **kwargs):
-        self.node_name = kwargs.get("name", None)
+        name = kwargs.pop("name", None)
+        if name is not None:
+            # overwrite node_name attribute
+            self.node_name = name
+        if self.node_name is None:
+            # set default value of node_name attribute
+            self.node_name = self.__class__.__name__
+        if len(kwargs) > 0:
+            raise TypeError(f"'{kwargs}' are an invalid keyword argument")
 
     @property
     def _descriptor_list(self) -> typing.List[ZnTrackOption]:
         """Get all descriptors of this instance"""
-        return descriptor.get_descriptors(self, ZnTrackOption)
-
-    @property
-    def node_name(self) -> str:
-        """Name of this node"""
-        if self._node_name is None:
-            return self.__class__.__name__
-        return self._node_name
-
-    @node_name.setter
-    def node_name(self, value):
-        """Overwrite the default node name based on the class name"""
-        self._node_name = value
+        return descriptor.get_descriptors(ZnTrackOption, self=self)
 
     @property
     def module(self) -> str:
diff --git a/zntrack/core/zntrackoption.py b/zntrack/core/zntrackoption.py
index 987e9ff..f06269e 100644
--- a/zntrack/core/zntrackoption.py
+++ b/zntrack/core/zntrackoption.py
@@ -174,11 +174,9 @@ class ZnTrackOption(descriptor.Descriptor):
         # The problem here is, that I can not / don't want to load all Nodes but
         # only the ones, that are in [self.node_name][self.name] for deserializing
         if uses_node_name(self.zn_type, instance) is not None:
-            values = utils.decode_dict(
-                file_content[instance.node_name].get(self.name, None)
-            )
+            values = utils.decode_dict(file_content[instance.node_name][self.name])
         else:
-            values = utils.decode_dict(file_content.get(self.name, None))
+            values = utils.decode_dict(file_content[self.name])
 
         log.debug(f"Loading {instance.node_name} from {file}: ({values})")
         return values
diff --git a/zntrack/descriptor/__init__.py b/zntrack/descriptor/__init__.py
index 0b8c69d..6748f91 100644
--- a/zntrack/descriptor/__init__.py
+++ b/zntrack/descriptor/__init__.py
@@ -73,22 +73,37 @@ class Descriptor:
         instance.__dict__[self.name] = value
 
 
-def get_descriptors(cls, descriptor) -> list:
+def get_descriptors(descriptor=None, *, self=None, cls=None) -> list:
     """Get a list of all descriptors inheriting from "descriptor"
 
     Parameters
     ----------
     cls: any python class
+    self: any python class instance
     descriptor: any object inheriting from descriptor
 
     Returns
     -------
-    list[Descriptor]
+    list
         a list of the found descriptor objects
 
     """
+    if self is None and cls is None:
+        raise ValueError("Either self or cls must not be None")
+    if self is not None and cls is not None:
+        raise ValueError("Either self or cls must be None")
+    if self is not None:
+        cls = type(self)
     lst = []
-    for option in vars(type(cls)).values():
-        if isinstance(option, descriptor):
-            lst.append(option)
+    for option in dir(cls):
+        try:
+            value = getattr(cls, option)
+            if isinstance(value, descriptor):
+                lst.append(value)
+        except AttributeError as err:
+            raise AttributeError(
+                "Trying to call ZnTrackOption.__get__(instance=None) to retreive the"
+                " <ZnTrackOption>. Make sure you implemented that case in the __get__"
+                " method."
+            ) from err
     return lst
diff --git a/zntrack/dvc/__init__.py b/zntrack/dvc/__init__.py
index 7e3b76b..82b19fd 100644
--- a/zntrack/dvc/__init__.py
+++ b/zntrack/dvc/__init__.py
@@ -48,6 +48,8 @@ class deps(ZnTrackOption):
 
     def __get__(self, instance, owner):
         """Use load_node_dependency before returning the value"""
+        if instance is None:
+            return self
         value = super().__get__(instance, owner)
         value = utils.utils.load_node_dependency(value, log_warning=True)
         setattr(instance, self.name, value)
diff --git a/zntrack/utils/file_io.py b/zntrack/utils/file_io.py
index b4ca118..e0a3052 100644
--- a/zntrack/utils/file_io.py
+++ b/zntrack/utils/file_io.py
@@ -57,6 +57,25 @@ def write_file(file: pathlib.Path, value: dict, mkdir: bool = True):
         raise ValueError(f"File with suffix {file.suffix} is not supported")
 
 
+def clear_config_file(file: pathlib.Path, node_name: str):
+    """Clear the entries in the files for the given node name
+
+    Parameters
+    ----------
+    file: pathlib.Path
+        The file to read from, e.g. params.yaml / zntrack.json
+    node_name: str
+        The name of the Node
+    """
+    try:
+        file_content = read_file(file)
+    except FileNotFoundError:
+        file_content = {}
+
+    _ = file_content.pop(node_name, None)
+    write_file(file, value=file_content)
+
+
 def update_config_file(
     file: pathlib.Path,
     node_name: typing.Union[str, None],
diff --git a/zntrack/utils/utils.py b/zntrack/utils/utils.py
index be364b0..810991e 100644
--- a/zntrack/utils/utils.py
+++ b/zntrack/utils/utils.py
@@ -72,13 +72,16 @@ def encode_dict(value) -> dict:
     return json.loads(json.dumps(value, cls=znjson.ZnEncoder))
 
 
-def get_auto_init(fields: typing.List[str]):
-    """Automatically create a __init__ based on fields
+def get_auto_init(fields: typing.List[str], super_init: typing.Callable):
+    """Automatically create an __init__ based on fields
+
     Parameters
     ----------
     fields: list[str]
         A list of strings that will be used in the __init__, e.g. for [foo, bar]
         it will create __init__(self, foo=None, bar=None) using **kwargs
+    super_init: Callable
+        typically this is Node.__init__
     """
 
     def auto_init(self, **kwargs):
@@ -88,7 +91,10 @@ def get_auto_init(fields: typing.List[str]):
                 setattr(self, field, kwargs.pop(field))
             except KeyError:
                 pass
-        super(type(self), self).__init__(**kwargs)
+        super_init(self, **kwargs)  # call the super_init explicitly instead of super
+
+    # we add this attribute to the __init__ to make it identifiable
+    auto_init._uses_auto_init = True
 
     return auto_init
 
diff --git a/zntrack/zn/__init__.py b/zntrack/zn/__init__.py
index 5e272f1..4c1dfe4 100644
--- a/zntrack/zn/__init__.py
+++ b/zntrack/zn/__init__.py
@@ -52,6 +52,8 @@ class deps(ZnTrackOption):
 
     def __get__(self, instance, owner):
         """Use load_node_dependency before returning the value"""
+        if instance is None:
+            return self
         value = super().__get__(instance, owner)
         value = utils.utils.load_node_dependency(value)  # use value = Cls.load()
         setattr(instance, self.name, value)


Test patch:
diff --git a/tests/integration_tests/test_inheritance.py b/tests/integration_tests/test_inheritance.py
new file mode 100644
index 0000000..c95a386
--- /dev/null
+++ b/tests/integration_tests/test_inheritance.py
@@ -0,0 +1,80 @@
+import os
+import shutil
+import subprocess
+
+import pytest
+
+from zntrack import Node, zn
+
+
+@pytest.fixture()
+def proj_path(tmp_path):
+    shutil.copy(__file__, tmp_path)
+    os.chdir(tmp_path)
+    subprocess.check_call(["git", "init"])
+    subprocess.check_call(["dvc", "init"])
+
+    return tmp_path
+
+
+class InOuts(Node):
+    inputs = zn.params()
+    outputs = zn.outs()
+
+
+class WriteData(InOuts):
+    def run(self):
+        self.outputs = self.inputs
+
+
+class WriteDataWithInit(InOuts):
+    def __init__(self, inputs=None, **kwargs):
+        super().__init__(**kwargs)
+        self.inputs = inputs
+
+    def run(self):
+        self.outputs = self.inputs
+
+
+class InOutsWInit(Node):
+    inputs = zn.params()
+    outputs = zn.outs()
+
+    def __init__(self, inputs=None, **kwargs):
+        super().__init__(**kwargs)
+        self.inputs = inputs
+
+
+class WriteDataParentInit(InOutsWInit):
+    def run(self):
+        self.outputs = self.inputs
+
+
+class WriteDataParentInitWithInit(InOutsWInit):
+    def __init__(self, inputs=None, **kwargs):
+        super().__init__(**kwargs)
+        self.inputs = inputs
+
+    def run(self):
+        self.outputs = self.inputs
+
+
+@pytest.mark.parametrize(
+    "MyNode",
+    (WriteData, WriteDataWithInit, WriteDataParentInit, WriteDataParentInitWithInit),
+)
+def test_simple_inheritance(proj_path, MyNode):
+    MyNode(inputs="HelloWorld").write_graph(run=True)
+    assert MyNode.load().outputs == "HelloWorld"
+
+
+class WriteCustomData(InOuts):
+    custom = zn.params()
+
+    def run(self):
+        self.outputs = f"{self.inputs} {self.custom}"
+
+
+def test_WriteCustomData(proj_path):
+    WriteCustomData(inputs="Hello", custom="World").write_graph(run=True)
+    assert WriteCustomData.load().outputs == "Hello World"
diff --git a/tests/integration_tests/test_single_node.py b/tests/integration_tests/test_single_node.py
index 96b10ed..8e49e2a 100644
--- a/tests/integration_tests/test_single_node.py
+++ b/tests/integration_tests/test_single_node.py
@@ -282,6 +282,10 @@ def test_auto_init(proj_path):
     assert SingleNodeNoInit.load().param1 == 25
     assert SingleNodeNoInit.load().param2 == 42
     assert SingleNodeNoInit.load().result == 25 + 42
+    # TODO add a check for parameters in the auto init
+    #  if they are not set and don't have a default
+    with pytest.raises(TypeError):
+        SingleNodeNoInit(unknown_param=15)
 
 
 class OutsNotWritten(Node):
diff --git a/tests/unit_tests/core/test_core_base.py b/tests/unit_tests/core/test_core_base.py
index 49f91af..22c94cf 100644
--- a/tests/unit_tests/core/test_core_base.py
+++ b/tests/unit_tests/core/test_core_base.py
@@ -64,21 +64,23 @@ def test_save(run):
         else:
             example.save()
             assert zntrack_mock().write.mock_calls == [
+                call(json.dumps({})),  # clear everything first
                 call(
                     json.dumps(
-                        {"ExampleFullNode": {"dvc_outs": "file.txt"}},
+                        {"ExampleFullNode": {"deps": "deps.inp"}},
                         indent=4,
                     )
                 ),
                 call(
                     json.dumps(
-                        {"ExampleFullNode": {"deps": "deps.inp"}},
+                        {"ExampleFullNode": {"dvc_outs": "file.txt"}},
                         indent=4,
                     )
                 ),
             ]
             assert params_mock().write.mock_calls == [
-                call(yaml.safe_dump({"ExampleFullNode": {"params": 10}}, indent=4))
+                call(yaml.safe_dump({})),  # clear everything first
+                call(yaml.safe_dump({"ExampleFullNode": {"params": 10}}, indent=4)),
             ]
 
 
@@ -210,25 +212,23 @@ class ZnTrackOptionCollection:
     no_option2 = 42
 
 
-def test_get_auto_init_signature():
-    zn_option_names, signature_params = get_auto_init_signature(ZnTrackOptionCollection)
+class CollectionChild(ZnTrackOptionCollection):
+    pass
 
-    assert zn_option_names == [
-        "param1",
-        "param2",
-        "param3",
-        "out1",
-        "out2",
-        "out3",
-    ]
 
-    assert signature_params[0].name == "param1"
-    assert signature_params[0].annotation == dict
+@pytest.mark.parametrize("cls", (ZnTrackOptionCollection, CollectionChild))
+def test_get_auto_init_signature(cls):
+    zn_option_names, signature_params = get_auto_init_signature(cls)
 
-    assert signature_params[2].name == "param3"
-    assert signature_params[2].annotation is None
+    assert zn_option_names == ["out1", "out2", "out3", "param1", "param2", "param3"]
 
-    assert signature_params[-1].name == "out3"
+    # assert signature_params[0].name == "out1"
+    #
+    # assert signature_params[3].name == "param1"
+    # assert signature_params[3].annotation == dict
+    #
+    # assert signature_params[5].name == "param3"
+    # assert signature_params[5].annotation is None
 
 
 class NodeMock(metaclass=LoadViaGetItem):
diff --git a/tests/unit_tests/core/test_dvcgraph.py b/tests/unit_tests/core/test_dvcgraph.py
index 00d9e49..f73f609 100644
--- a/tests/unit_tests/core/test_dvcgraph.py
+++ b/tests/unit_tests/core/test_dvcgraph.py
@@ -18,13 +18,6 @@ class ExampleDVCOutsNode(GraphWriter):
     outs = dvc.outs(pathlib.Path("example.dat"))
 
 
-def test_node_name_get():
-    example = ExampleDVCOutsNode()
-    assert example.node_name == "ExampleDVCOutsNode"
-    example._node_name = "NamedExample"
-    assert example.node_name == "NamedExample"
-
-
 def test_get_dvc_arguments():
     dvc_options = DVCRunOptions(
         force=True,
diff --git a/tests/unit_tests/descriptor/test_descriptor.py b/tests/unit_tests/descriptor/test_descriptor.py
index 5f7ac45..b312f7a 100644
--- a/tests/unit_tests/descriptor/test_descriptor.py
+++ b/tests/unit_tests/descriptor/test_descriptor.py
@@ -1,12 +1,16 @@
 import pytest
 
-from zntrack.descriptor import Descriptor
+from zntrack.descriptor import Descriptor, get_descriptors
 
 
 class ExampleCls:
     desc = Descriptor()
 
 
+class ExampleChild(ExampleCls):
+    pass
+
+
 def test_get_descriptor():
     assert isinstance(ExampleCls.desc, Descriptor)
 
@@ -46,3 +50,26 @@ def test_descriptor_get():
     cls = ExampleCls()
     cls.__dict__[desc.name] = 42
     assert cls.desc == 42
+
+
+@pytest.mark.parametrize("cls", (ExampleCls, ExampleChild))
+def test_get_descriptors_cls(cls):
+    instance = cls
+    assert get_descriptors(Descriptor, cls=instance) == [cls.desc]
+
+
+@pytest.mark.parametrize("cls", (ExampleCls, ExampleChild))
+def test_get_descriptors_self(cls):
+    self = cls()
+    assert get_descriptors(Descriptor, self=self) == [cls.desc]
+
+
+def test_get_descriptors_exceptions():
+    with pytest.raises(ValueError):
+        get_descriptors(Descriptor)
+
+    with pytest.raises(ValueError):
+        get_descriptors()
+
+    with pytest.raises(ValueError):
+        get_descriptors(Descriptor, self=ExampleCls(), cls=ExampleCls)
diff --git a/tests/unit_tests/utlis/test_utils.py b/tests/unit_tests/utlis/test_utils.py
index b3c149b..ab5b6a0 100644
--- a/tests/unit_tests/utlis/test_utils.py
+++ b/tests/unit_tests/utlis/test_utils.py
@@ -1,13 +1,3 @@
-"""
-This program and the accompanying materials are made available under the terms of the
-Eclipse Public License v2.0 which accompanies this distribution, and is available at
-https://www.eclipse.org/legal/epl-v20.html
-SPDX-License-Identifier: EPL-2.0
-
-Copyright Contributors to the Zincware Project.
-
-Description: Test class for testing utils
-"""
 import json
 import os
 import pathlib
@@ -45,13 +35,15 @@ def test_get_auto_init():
     with pytest.raises(TypeError):
         Test(foo="foo")
 
-    setattr(Test, "__init__", utils.get_auto_init(fields=["foo", "bar"]))
-
+    mock = MagicMock()
+    setattr(Test, "__init__", utils.get_auto_init(fields=["foo", "bar"], super_init=mock))
     test = Test(foo="foo", bar="bar")
 
     assert test.foo == "foo"
     assert test.bar == "bar"
 
+    mock.assert_called()
+
 
 def test_module_handler():
     my_mock = MagicMock