Repository: AaltoPML/VAI-Lab (language: python)

Issue:
Cm type hint fixing
# Purpose
Add static type hints and testing to the codebase and fix bugs resulting from static type errors.

All current type errors have been fixed, but some methods are skipped by mypy at present, as they are they are not considered to by typed - will add more as we go

## In Brief
 - Added static typing to most scripts and plugins
 - Added `mypy.ini` for mypy setup
 - Added Protocols and custom types to `aidesign/_types.py`
 - Moved many of the common functionality scripts out of `./utils` and to `./` with underscore notation, e.g `aidesign/utils/import_helper.py` -> `aidesign/_import_helper.py`

### New types and protocols in `_types.py`
`aidesign/_types.py` defines custom types and `Protocol`s for general plugins and specific plugins . These acts as pre-runtime checkers and aids in autocompletion checking to aid development. 

These work in a similar way to Abstract Classes, but any call to a class defined by a Protocol knows what methods are in the class, as the Protocol holds this information.

Example, the following Protocol defines what you should expect any plugin to have
```python 
class PluginSpecsInterface(Protocol):
    @property
    def names(self):
        ...

    @property
    def class_names(self):
        ...

    @property
    def module_options(self):
        ...

    @property
    def required_settings(self):
        ...

    @property
    def class_descriptions(self):
        ...

    @property
    def optional_settings(self):
        ...

    @property
    def available_plugin_names(self):
        ...

    def find_from_class_name(self, value):
        ...

    def find_from_readable_name(self, value):
        ...

    def print(self, value):
        ...
```

### Work In Progress `pipelineCanvas` code commented out
During the work for this PR, a few issues with `pluginCanvas` were complicated by `pipelineCanvas` having largely duplicated functions, but without being used and complicated further by a few dicts not being defined due to copy-paste. A lot of `pipelineCanvas` code has been commented out until it is implemented fully.

### New filetree
```
├── aidesign
│   ├── Core
│   │   ├── Assets
│   │   ├── AI_design_core.py
│   │   └── __init__.py
│   ├── Data
│   │   ├── resources
│   │   │   ├── image_classification
│   │   │   │   ├── training_images
│   │   │   │   └── Y_data.csv
│   │   │   ├── state-action
│   │   │   │   └── X_data.csv
│   │   │   ├── supervised_classification
│   │   │   │   ├── randomInputDataClassification.py
│   │   │   │   ├── randomInputData.mat
│   │   │   │   ├── X_tr.csv
│   │   │   │   ├── X_tst.csv
│   │   │   │   ├── Y_tr.csv
│   │   │   │   └── Y_tst.csv
│   │   │   ├── supervised_regression
│   │   │   │   ├── 1
│   │   │   │   │   ├── x_train.csv
│   │   │   │   │   └── y_train.csv
│   │   │   │   ├── import_test.csv
│   │   │   │   ├── import_test user_interface.csv
│   │   │   │   ├── randomInputData.mat
│   │   │   │   ├── randomInputDataRegression.py
│   │   │   │   ├── X_tr.csv
│   │   │   │   ├── X_tst.csv
│   │   │   │   ├── Y_tr.csv
│   │   │   │   └── Y_tst.csv
│   │   │   └── xml_files
│   │   │       ├── basic_operation.xml
│   │   │       ├── classification_test.xml
│   │   │       ├── data_passing_canvas_test.xml
│   │   │       ├── data_passing_test.xml
│   │   │       ├── data_passing_userfeedback_test.xml
│   │   │       ├── example_config.xml
│   │   │       ├── Hospital.xml
│   │   │       ├── logisticregression_test.xml
│   │   │       ├── proc_class_test.xml
│   │   │       ├── proc_clust_test.xml
│   │   │       ├── processing_test.xml
│   │   │       ├── proc_reg_test.xml
│   │   │       ├── regerssion_test.xml
│   │   │       ├── reg_proc_reg_test.xml
│   │   │       ├── ridge_test.xml
│   │   │       ├── SVR_test.xml
│   │   │       ├── test2.xml
│   │   │       ├── test3.xml
│   │   │       ├── test 4.xml
│   │   │       ├── test-delete.xml
│   │   │       ├── testing.xml
│   │   │       └── test.xml
│   │   ├── Data_core.py
│   │   ├── __init__.py
│   │   └── xml_handler.py
│   ├── DataProcessing
│   │   ├── plugins
│   │   │   ├── binarizer.py
│   │   │   ├── kbinsdiscretizer.py
│   │   │   ├── labelbinarizer.py
│   │   │   ├── labelencoder.py
│   │   │   ├── maxabsscaler.py
│   │   │   ├── minmaxscaler.py
│   │   │   ├── multilabelbinarizer.py
│   │   │   ├── normalizer.py
│   │   │   ├── onehotencoder.py
│   │   │   ├── ordinalencoder.py
│   │   │   ├── polynomialfeatures.py
│   │   │   ├── quantiletransformer.py
│   │   │   └── standardscaler.py
│   │   ├── DataProcessing_core.py
│   │   ├── data_processing_plugin_template.py
│   │   └── __init__.py
│   ├── DecisionMaking
│   │   ├── __init__.py
│   │   └── Modelling_core.py
│   ├── GUI
│   │   ├── plugins
│   │   │   └── __init__.py
│   │   ├── GUI_core.py
│   │   ├── __init__.py
│   │   └── User_Feedback_template.py
│   ├── InputData
│   │   ├── __init__.py
│   │   └── InputData_core.py
│   ├── Modelling
│   │   ├── plugins
│   │   │   ├── affinitypropagation.py
│   │   │   ├── bayesianridge.py
│   │   │   ├── birch.py
│   │   │   ├── decisiontreeclassifier.py
│   │   │   ├── decisiontreeregressor.py
│   │   │   ├── elasticnet.py
│   │   │   ├── gpclassifier.py
│   │   │   ├── gpregressor.py
│   │   │   ├── kernelridge.py
│   │   │   ├── kmeans.py
│   │   │   ├── knnclassifier.py
│   │   │   ├── knnregressor.py
│   │   │   ├── lasso.py
│   │   │   ├── linearregression.py
│   │   │   ├── logisticregression.py
│   │   │   ├── meanshift.py
│   │   │   ├── passiveaggressiveclassifier.py
│   │   │   ├── perceptron.py
│   │   │   ├── randomforestclassifier.py
│   │   │   ├── randomforestregressor.py
│   │   │   ├── ridgeregression.py
│   │   │   ├── svc.py
│   │   │   └── svr.py
│   │   ├── __init__.py
│   │   ├── Modelling_core.py
│   │   └── modelling_plugin_template.py
│   ├── UserInteraction
│   │   ├── plugins
│   │   │   ├── resources
│   │   │   │   └── Assets
│   │   │   ├── CanvasInput.py
│   │   │   ├── __init__.py
│   │   │   └── ManualInput.py
│   │   ├── __init__.py
│   │   ├── UserInteraction_core.py
│   │   └── User_Interaction_template.py
│   ├── utils
│   │   ├── plugins
│   │   │   ├── aidCanvas.py
│   │   │   ├── dataLoader.py
│   │   │   ├── __init__.py
│   │   │   ├── MainPage.py
│   │   │   ├── pipelineCanvas.py
│   │   │   ├── pluginCanvas.py
│   │   │   └── startpage.py
│   │   ├── resources
│   │   │   ├── Assets
│   │   │   │   └── Hospital.xml
│   │   │   └── example_radiography_images
│   │   ├── AID_core.py
│   │   ├── __init__.py
│   │   └── UserInterfaceClass.py
│   ├── _import_helper.py
│   ├── __init__.py
│   ├── _plugin_helpers.py
│   ├── _plugin_template.py
│   └── _types.py
├── mypy.ini
├── README.md
├── run_UI.py
└── setup.py
```
Cm type hint fixing
# Purpose
Add static type hints and testing to the codebase and fix bugs resulting from static type errors.

All current type errors have been fixed, but some methods are skipped by mypy at present, as they are they are not considered to by typed - will add more as we go

## In Brief
 - Added static typing to most scripts and plugins
 - Added `mypy.ini` for mypy setup
 - Added Protocols and custom types to `aidesign/_types.py`
 - Moved many of the common functionality scripts out of `./utils` and to `./` with underscore notation, e.g `aidesign/utils/import_helper.py` -> `aidesign/_import_helper.py`

### New types and protocols in `_types.py`
`aidesign/_types.py` defines custom types and `Protocol`s for general plugins and specific plugins . These acts as pre-runtime checkers and aids in autocompletion checking to aid development. 

These work in a similar way to Abstract Classes, but any call to a class defined by a Protocol knows what methods are in the class, as the Protocol holds this information.

Example, the following Protocol defines what you should expect any plugin to have
```python 
class PluginSpecsInterface(Protocol):
    @property
    def names(self):
        ...

    @property
    def class_names(self):
        ...

    @property
    def module_options(self):
        ...

    @property
    def required_settings(self):
        ...

    @property
    def class_descriptions(self):
        ...

    @property
    def optional_settings(self):
        ...

    @property
    def available_plugin_names(self):
        ...

    def find_from_class_name(self, value):
        ...

    def find_from_readable_name(self, value):
        ...

    def print(self, value):
        ...
```

### Work In Progress `pipelineCanvas` code commented out
During the work for this PR, a few issues with `pluginCanvas` were complicated by `pipelineCanvas` having largely duplicated functions, but without being used and complicated further by a few dicts not being defined due to copy-paste. A lot of `pipelineCanvas` code has been commented out until it is implemented fully.

### New filetree
```
├── aidesign
│   ├── Core
│   │   ├── Assets
│   │   ├── AI_design_core.py
│   │   └── __init__.py
│   ├── Data
│   │   ├── resources
│   │   │   ├── image_classification
│   │   │   │   ├── training_images
│   │   │   │   └── Y_data.csv
│   │   │   ├── state-action
│   │   │   │   └── X_data.csv
│   │   │   ├── supervised_classification
│   │   │   │   ├── randomInputDataClassification.py
│   │   │   │   ├── randomInputData.mat
│   │   │   │   ├── X_tr.csv
│   │   │   │   ├── X_tst.csv
│   │   │   │   ├── Y_tr.csv
│   │   │   │   └── Y_tst.csv
│   │   │   ├── supervised_regression
│   │   │   │   ├── 1
│   │   │   │   │   ├── x_train.csv
│   │   │   │   │   └── y_train.csv
│   │   │   │   ├── import_test.csv
│   │   │   │   ├── import_test user_interface.csv
│   │   │   │   ├── randomInputData.mat
│   │   │   │   ├── randomInputDataRegression.py
│   │   │   │   ├── X_tr.csv
│   │   │   │   ├── X_tst.csv
│   │   │   │   ├── Y_tr.csv
│   │   │   │   └── Y_tst.csv
│   │   │   └── xml_files
│   │   │       ├── basic_operation.xml
│   │   │       ├── classification_test.xml
│   │   │       ├── data_passing_canvas_test.xml
│   │   │       ├── data_passing_test.xml
│   │   │       ├── data_passing_userfeedback_test.xml
│   │   │       ├── example_config.xml
│   │   │       ├── Hospital.xml
│   │   │       ├── logisticregression_test.xml
│   │   │       ├── proc_class_test.xml
│   │   │       ├── proc_clust_test.xml
│   │   │       ├── processing_test.xml
│   │   │       ├── proc_reg_test.xml
│   │   │       ├── regerssion_test.xml
│   │   │       ├── reg_proc_reg_test.xml
│   │   │       ├── ridge_test.xml
│   │   │       ├── SVR_test.xml
│   │   │       ├── test2.xml
│   │   │       ├── test3.xml
│   │   │       ├── test 4.xml
│   │   │       ├── test-delete.xml
│   │   │       ├── testing.xml
│   │   │       └── test.xml
│   │   ├── Data_core.py
│   │   ├── __init__.py
│   │   └── xml_handler.py
│   ├── DataProcessing
│   │   ├── plugins
│   │   │   ├── binarizer.py
│   │   │   ├── kbinsdiscretizer.py
│   │   │   ├── labelbinarizer.py
│   │   │   ├── labelencoder.py
│   │   │   ├── maxabsscaler.py
│   │   │   ├── minmaxscaler.py
│   │   │   ├── multilabelbinarizer.py
│   │   │   ├── normalizer.py
│   │   │   ├── onehotencoder.py
│   │   │   ├── ordinalencoder.py
│   │   │   ├── polynomialfeatures.py
│   │   │   ├── quantiletransformer.py
│   │   │   └── standardscaler.py
│   │   ├── DataProcessing_core.py
│   │   ├── data_processing_plugin_template.py
│   │   └── __init__.py
│   ├── DecisionMaking
│   │   ├── __init__.py
│   │   └── Modelling_core.py
│   ├── GUI
│   │   ├── plugins
│   │   │   └── __init__.py
│   │   ├── GUI_core.py
│   │   ├── __init__.py
│   │   └── User_Feedback_template.py
│   ├── InputData
│   │   ├── __init__.py
│   │   └── InputData_core.py
│   ├── Modelling
│   │   ├── plugins
│   │   │   ├── affinitypropagation.py
│   │   │   ├── bayesianridge.py
│   │   │   ├── birch.py
│   │   │   ├── decisiontreeclassifier.py
│   │   │   ├── decisiontreeregressor.py
│   │   │   ├── elasticnet.py
│   │   │   ├── gpclassifier.py
│   │   │   ├── gpregressor.py
│   │   │   ├── kernelridge.py
│   │   │   ├── kmeans.py
│   │   │   ├── knnclassifier.py
│   │   │   ├── knnregressor.py
│   │   │   ├── lasso.py
│   │   │   ├── linearregression.py
│   │   │   ├── logisticregression.py
│   │   │   ├── meanshift.py
│   │   │   ├── passiveaggressiveclassifier.py
│   │   │   ├── perceptron.py
│   │   │   ├── randomforestclassifier.py
│   │   │   ├── randomforestregressor.py
│   │   │   ├── ridgeregression.py
│   │   │   ├── svc.py
│   │   │   └── svr.py
│   │   ├── __init__.py
│   │   ├── Modelling_core.py
│   │   └── modelling_plugin_template.py
│   ├── UserInteraction
│   │   ├── plugins
│   │   │   ├── resources
│   │   │   │   └── Assets
│   │   │   ├── CanvasInput.py
│   │   │   ├── __init__.py
│   │   │   └── ManualInput.py
│   │   ├── __init__.py
│   │   ├── UserInteraction_core.py
│   │   └── User_Interaction_template.py
│   ├── utils
│   │   ├── plugins
│   │   │   ├── aidCanvas.py
│   │   │   ├── dataLoader.py
│   │   │   ├── __init__.py
│   │   │   ├── MainPage.py
│   │   │   ├── pipelineCanvas.py
│   │   │   ├── pluginCanvas.py
│   │   │   └── startpage.py
│   │   ├── resources
│   │   │   ├── Assets
│   │   │   │   └── Hospital.xml
│   │   │   └── example_radiography_images
│   │   ├── AID_core.py
│   │   ├── __init__.py
│   │   └── UserInterfaceClass.py
│   ├── _import_helper.py
│   ├── __init__.py
│   ├── _plugin_helpers.py
│   ├── _plugin_template.py
│   └── _types.py
├── mypy.ini
├── README.md
├── run_UI.py
└── setup.py
```


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index fbf9fdc9..b59e92f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 # Byte-compiled / optimized / DLL files
 __pycache__/
 **/__pycache__
+**/.vscode
 **/*.pyc
 *.py[cod]
 *$py.class
diff --git a/aidesign/Core/AI_design_core.py b/aidesign/Core/AI_design_core.py
index 51552b2a..5085fcca 100644
--- a/aidesign/Core/AI_design_core.py
+++ b/aidesign/Core/AI_design_core.py
@@ -1,17 +1,17 @@
-from aidesign.utils.import_helper import import_module
+from aidesign._import_helper import import_module
 from aidesign.Data.xml_handler import XML_handler
 from aidesign.GUI.GUI_core import GUI
 from aidesign.Data.Data_core import Data
-from aidesign.utils.plugin_helpers import PluginSpecs
+from aidesign._plugin_helpers import PluginSpecs
+from aidesign._types import PluginSpecsInterface, DataInterface, ModuleInterface
 
-
-class Core(object):
+class Core:
     def __init__(self) -> None:
         self._xml_handler = XML_handler()
-        self._avail_plugins = PluginSpecs()
+        self._avail_plugins: PluginSpecsInterface = PluginSpecs()
         self.data = Data()
-        self.loop_level = 0
-        self.setup_complete = False
+        self.loop_level: int = 0
+        self.setup_complete: bool = False
 
     def launch(self):
         gui_app = GUI()
@@ -29,7 +29,7 @@ def load_config_file(self, filename: str):
         self._xml_handler.load_XML(filename)
         self.setup_complete = True
 
-    def _load_data(self):
+    def _load_data(self) -> None:
         init_data_fn = self._xml_handler.data_to_load
         self.data.import_data_from_config(init_data_fn)
 
@@ -39,7 +39,7 @@ def _execute_module(self, specs):
 
         :param specs: dict of module to be executed
         """
-        mod = import_module(globals(), specs["module_type"]).__call__()
+        mod: ModuleInterface = import_module(globals(), specs["module_type"]).__call__()
         mod.set_avail_plugins(self._avail_plugins)
         mod.set_data_in(self.data)
         mod.set_options(specs)
diff --git a/aidesign/Data/Data_core.py b/aidesign/Data/Data_core.py
index c751ca6a..42d61f87 100644
--- a/aidesign/Data/Data_core.py
+++ b/aidesign/Data/Data_core.py
@@ -4,26 +4,30 @@
 from os import path
 
 if not __package__:
-    import sys 
+    import sys
     root_mod = path.dirname(path.dirname(path.dirname(__file__)))
     sys.path.append(root_mod)
 
-from aidesign.utils.import_helper import get_lib_parent_dir
-from aidesign.Data.xml_handler import XML_handler
-import pandas as pd
-import numpy as np
+from typing import Dict, KeysView, TypeVar
 
+from aidesign._import_helper import get_lib_parent_dir
+from aidesign.Data.xml_handler import XML_handler
 
-class Data(object):
-    def __init__(self) -> None:
-        self.lib_base_path = get_lib_parent_dir()
-        self.xml_parser = XML_handler()
-        self.data = {}
+import pandas as pd # type: ignore
+import numpy as np
 
-    def _import_csv(self, 
-                    filename:str,
-                    data_name:str,
-                    strip_whitespace:bool=True):
+DataT = TypeVar("DataT",bound="Data")
+class Data:
+    
+    def __init__(self: DataT) -> None:
+        self._lib_base_path = get_lib_parent_dir()
+        self._xml_parser = XML_handler()
+        self.data: Dict[str, pd.core.frame.DataFrame] = {}
+
+    def _import_csv(self: DataT,
+                    filename: str,
+                    data_name: str,
+                    strip_whitespace: bool = True) -> None:
         """import data directly into DataFrame
 
         :param filename: str, filename of csv file to be loaded
@@ -32,45 +36,45 @@ def _import_csv(self,
         TODO: pandas has a lot of inbuilt read functions, including excel - implement
         """
         self.data[data_name] = pd.read_csv(filename,
-                            delimiter=',',
-                            quotechar='|')
+                                           delimiter=',',
+                                           quotechar='|')
         if strip_whitespace:
-            self.data[data_name].columns = [c.strip() for c in self.data[data_name].columns]
+            self.data[data_name].columns = [c.strip()
+                                            for c in self.data[data_name].columns]
 
-    def _import_png(self, 
-                    filename:str,
-                    data_name:str):
+    def _import_png(self: DataT,
+                    filename: str,
+                    data_name: str) -> None:
         """Loads png into PIL.Image class. Adds instance to self.data
         The image is stored as a function (not a matrix - can be added if needed)
 
         :param filename: str, filename of csv file to be loaded
         :param data_name: str, name of dict key in which data will be stored
         """
-        from PIL import Image
+        from PIL import Image # type: ignore
         self.data[data_name][filename] = Image.open(filename)
 
-
-    def _import_dir(self,
-                        folder_dir:str,
-                        data_name:str = "data"):
+    def _import_dir(self: DataT,
+                    folder_dir: str,
+                    data_name: str = "data") -> None:
         from glob import glob
-        files =np.sort(glob(folder_dir + "*"))
+        files = np.sort(glob(folder_dir + "*"))
         self.data[data_name] = {}
         for f in files:
-            self.import_data(f,data_name)
+            self.import_data(f, data_name)
 
-    def _rel_to_abs(self, filename: str):
+    def _rel_to_abs(self: DataT, filename: str) -> str:
         """Checks if path is relative or absolute
         If absolute, returns original path 
         If relative, converts path to absolute by appending to base directory
         """
         if filename[0] == ".":
-            filename = path.join(self.lib_base_path,filename)
+            filename = path.join(self._lib_base_path, filename)
         elif filename[0] == "/" or (filename[0].isalpha() and filename[0].isupper()):
             filename = filename
         return filename
 
-    def _get_ext(self, path_dir:str):
+    def _get_ext(self: DataT, path_dir: str) -> str:
         """Extracts extension from path_dir, or check if is dir
 
         :param path_dir: str, path_dir to be checked
@@ -82,47 +86,59 @@ def _get_ext(self, path_dir:str):
         else:
             return path_dir.split(".")[-1]
 
-    def import_data(self,
-                        filename:str,
-                        data_name:str = "data"):
+    def import_data(self: DataT,
+                    filename: str,
+                    data_name: str = "data") -> None:
         """Import file directly into DataFrame
 
         Translates relative files to absolute before parsing - not ideal
 
         Filename to parsing method based on extension name.
-        
+
         :param filename: str, filename of file to be loaded
         :param data_name: str, name of class variable data will be loaded to
         """
-        filename = self._rel_to_abs(filename).replace("\\","/").replace("/",path.sep)
+        filename = self._rel_to_abs(filename).replace(
+            "\\", "/").replace("/", path.sep)
         ext = self._get_ext(filename)
-        getattr(self,"_import_{0}".format(ext))(filename,data_name)
+        getattr(self, "_import_{0}".format(ext))(filename, data_name)
 
-    def import_data_from_config(self,config:dict):
+    def import_data_from_config(self: DataT, config: dict) -> None:
         for c in config.keys():
-            self.import_data(config[c],c)
+            self.import_data(config[c], c)
 
-    def append_data_column(self, col_name:str, data=None):
+    def append_data_column(self: DataT, col_name: str, data=None) -> None:
         self.data[col_name] = data
 
-    def _parse_data_options(self):
+    def _parse_data_options(self: DataT) -> None:
         for s in self.loaded_options["data_fields"]:
             self.append_data_column(s)
 
-    def load_data_settings(self, filename:str):
-        self.xml_parser.load_XML(filename)
-        self.loaded_options = self.xml_parser.init_data_structure
+    def load_data_settings(self: DataT, filename: str) -> None:
+        self._xml_parser.load_XML(filename)
+        self.loaded_options = self._xml_parser._get_init_data_structure()
         self._parse_data_options()
-        
-    def keys(self):
+
+    def keys(self: DataT) -> KeysView:
         return self.data.keys()
-    
-    def __getitem__(self, key:str):
+
+    def __getitem__(self: DataT, key: str) -> pd.core.frame.DataFrame:
         return self.data[key]
 
+    def copy(self: DataT) -> DataT:
+        from copy import deepcopy
+        return deepcopy(self)
+
 if __name__ == "__main__":
     d = Data()
     # d.load_data_settings("./Data/resources/data_passing_test.xml")
-    d.import_data("./Data/resources/import_test.csv")
-    print(d.data_names)
-    d.data_names
\ No newline at end of file
+    d.import_data("./Data/resources/supervised_regression/1/x_train.csv")
+    # print(d.data["data"])
+    print(d["data"]["input"].loc[0:3])
+    dc = d.copy()
+    d["data"]["input"].loc[0:3] = 9
+    print(d["data"]["input"].loc[0:3])
+    print(dc["data"]["input"].loc[0:3])
+    dc.keys()
+    # print(dc["input"])
+    # d.data_names
diff --git a/aidesign/Data/resources/supervised_classification/randomInputData.py b/aidesign/Data/resources/supervised_classification/randomInputDataClassification.py
similarity index 100%
rename from aidesign/Data/resources/supervised_classification/randomInputData.py
rename to aidesign/Data/resources/supervised_classification/randomInputDataClassification.py
diff --git a/aidesign/Data/resources/supervised_regression/randomInputData.py b/aidesign/Data/resources/supervised_regression/randomInputDataRegression.py
similarity index 100%
rename from aidesign/Data/resources/supervised_regression/randomInputData.py
rename to aidesign/Data/resources/supervised_regression/randomInputDataRegression.py
diff --git a/aidesign/Data/resources/xml_files/Hospital.xml b/aidesign/Data/resources/xml_files/Hospital.xml
index 425fa1b2..f8076273 100644
--- a/aidesign/Data/resources/xml_files/Hospital.xml
+++ b/aidesign/Data/resources/xml_files/Hospital.xml
@@ -65,7 +65,7 @@
                 (322, 207)
             </coordinates>
         </InputData>
-        <UserFeedback name="User Feedback Adaptation">
+        <UserInteraction name="User Feedback Adaptation">
             <plugin type="" />
             <relationships>
                 <parent name="Decision making" />
@@ -75,7 +75,7 @@
             <coordinates>
                 (454, 325)
             </coordinates>
-        </UserFeedback>
+        </UserInteraction>
         <DecisionMaking name="Decision making">
             <plugin type="" />
             <relationships>
diff --git a/aidesign/Data/resources/xml_files/basic_operation.xml b/aidesign/Data/resources/xml_files/basic_operation.xml
index 4b77e2da..2d28bbd8 100644
--- a/aidesign/Data/resources/xml_files/basic_operation.xml
+++ b/aidesign/Data/resources/xml_files/basic_operation.xml
@@ -1,17 +1,17 @@
 <pipeline>
     <Initialiser name="Init">
-        <plugin type="">
-            <initial_data file="../resources/intial_data_test.csv" />
-            <goal file="../resources/design_goals_test.csv" />
-        </plugin>
+        <inputdata>
+            <X file="./Data/resources/image_classification/training_images/" />
+            <Y file="./Data/resources/image_classification/Y_data.csv" />
+        </inputdata>
         <relationships>
-            <child name="My First UserFeedback Module" />
+            <child name="My First UserInteraction Module" />
         </relationships>
     </Initialiser>
     <loop type="for" condition="2" name="For Loop 1">
-        <UserFeedback name="My First UserFeedback Module">
+        <UserInteraction name="My First UserInteraction Module">
             <plugin type="manual">
-                <class_list>
+                <!-- <class_list>
                     Atelectasis
                     Cardiomelagy
                     Effusion
@@ -20,15 +20,15 @@
                     Nodule
                     Pneumonia
                     Pneumothorax
-                </class_list>
+                </class_list> -->
             </plugin>
             <relationships>
                 <parent name="Init" />
                 <child name="Data Processing 1" />
             </relationships>
-        </UserFeedback>
+        </UserInteraction>
         <loop type="for" condition="1" name="For Loop 2">
-            <UserFeedback name="My Second UserFeedback Module">
+            <UserInteraction name="My Second UserInteraction Module">
                 <plugin type="canvas">
                     <class_list>
                         ['State_a','Action_a']
@@ -41,7 +41,7 @@
                     <child name="Data Processing 1" />
                     <child name="Output" />
                 </relationships>
-            </UserFeedback>
+            </UserInteraction>
         </loop>
     </loop>
     <Output>
@@ -52,7 +52,7 @@
             <save_to file="../resources/output_test.csv" />
         </plugin>
         <relationships>
-            <parent name="My Second UserFeedback Module" />
+            <parent name="My Second UserInteraction Module" />
         </relationships>
     </Output>
 </pipeline>
diff --git a/aidesign/Data/xml_handler.py b/aidesign/Data/xml_handler.py
index 14117cb3..aef193a6 100644
--- a/aidesign/Data/xml_handler.py
+++ b/aidesign/Data/xml_handler.py
@@ -1,10 +1,20 @@
-import xml.etree.ElementTree as ET
+from os import path
+
+if not __package__:
+    import sys
+    root_mod = path.dirname(path.dirname(path.dirname(__file__)))
+    sys.path.append(root_mod)
+
+
+from aidesign._import_helper import get_lib_parent_dir
+
+from typing import Any, Dict, Union, Optional, List
 from os import path
 from ast import literal_eval
-from aidesign.utils.import_helper import get_lib_parent_dir
+import xml.etree.ElementTree as ET
 
 
-class XML_handler(object):
+class XML_handler:
     def __init__(self, filename: str = None):
         """Loads XML file and parses into nested dictionaries
         This module is used as the backbone for all future modules.
@@ -13,13 +23,13 @@ def __init__(self, filename: str = None):
         :param filename [optional]: filename from which to load XML
         """
         self.lib_base_path = get_lib_parent_dir()
-        self.loaded_modules = {}
+        self.loaded_modules: Dict[str, Dict] = {}
 
         """valid_tags lists the available XML tags and their function
         TODO: populate the modules in this list automatically
         TODO: Can extend the plugin_helpers.py script to automatically populate these things
         """
-        self._valid_tags = {
+        self._valid_tags: Dict[str, str] = {
             "pipeline": "declaration",
             "relationships": "relationships",
             "plugin": "plugin",
@@ -60,19 +70,19 @@ def _check_file_exists(self, filename: str):
 
     def set_filename(self, filename: str):
         """Sets filename. Converts relative paths to absolute first."""
-        self.filename = self._check_filename_abs_rel(filename)
+        self.filename: str = self._check_filename_abs_rel(filename)
 
     def append_initialiser(self):
         self.append_pipeline_module("Initialiser",
-                            "Initialiser",
-                            None,
-                            None,
-                            [],
-                            [],
-                            None,
-                            [0,0,0])
-
-    def new_config_file(self, filename: str = None):
+                                    "Initialiser",
+                                    None,
+                                    None,
+                                    [],
+                                    [],
+                                    None,
+                                    [0, 0, 0])
+
+    def new_config_file(self, filename: str = None) -> None:
         """Constructs new XML file with minimal format"""
         if filename is not None:
             self.set_filename(filename)
@@ -80,7 +90,7 @@ def new_config_file(self, filename: str = None):
         self.root = self.tree.getroot()
         self.append_initialiser()
 
-    def load_XML(self, filename: str):
+    def load_XML(self, filename: str) -> None:
         """Loads XML file into class. 
         """
         if filename != None:
@@ -88,11 +98,15 @@ def load_XML(self, filename: str):
         self.tree = ET.parse(self.filename)
         self._parse_XML()
 
-    def _parse_XML(self):
+    def _parse_XML(self) -> None:
         self.root = self.tree.getroot()
         self._parse_tags(self.root, self.loaded_modules)
 
-    def _parse_tags(self, element: ET.Element, parent: dict):
+    def _bullet_list(self, val_list: Union[List[Any], Dict[str, str]]) -> str:
+        sep = "\n\t\u2022"
+        return sep + " {}".format(("," + sep + " ").join([*val_list]))
+
+    def _parse_tags(self, element: ET.Element, parent: dict) -> None:
         """Detect tags and send them to correct method for parsing
         Uses getattr to call the correct method
 
@@ -105,19 +119,18 @@ def _parse_tags(self, element: ET.Element, parent: dict):
                 getattr(self, "_load_{}".format(tag_type))(child, parent)
             except KeyError:
                 from sys import exit
-                print("\nError: Invalid XML Tag.")
-                print("XML tag \"{0}\" in \"{1}\" not found".format(
-                    child.tag, element.tag))
-                print("Valid tags are:")
-                print("\t\u2022 {}".format(
-                    ",\n\t\u2022 ".join([*self._valid_tags])))
+                print("\nError: Invalid XML Tag." +
+                      "\nXML tag \"{0}\"".format(child.tag) +
+                      "in \"{0}\" not found".format(element.tag) +
+                      "\nValid tags are:" +
+                      self._bullet_list(self._valid_tags))
                 exit(1)
             except AssertionError as Error:
                 from sys import exit
                 print(Error)
                 exit(1)
 
-    def _load_module(self, element: ET.Element, parent: dict):
+    def _load_module(self, element: ET.Element, parent: dict) -> None:
         """Parses tags associated with modules and appends to parent dict
 
         :param elem: xml.etree.ElementTree.Element to be parsed
@@ -130,7 +143,7 @@ def _load_module(self, element: ET.Element, parent: dict):
                                "module_type": module_type}
         self._parse_tags(element, parent[module_name])
 
-    def _load_plugin(self, element: ET.Element, parent: dict):
+    def _load_plugin(self, element: ET.Element, parent: dict) -> None:
         """Parses tags associated with plugins and appends to parent dict
 
         :param elem: xml.etree.ElementTree.Element to be parsed
@@ -140,7 +153,7 @@ def _load_plugin(self, element: ET.Element, parent: dict):
         parent["plugin"]["plugin_name"] = element.attrib["type"]
         parent["plugin"]["options"] = {}
         for child in element:
-            if child.text != None:
+            if child.text is not None:
                 val = self._parse_text_to_list(child)
                 val = (val[0] if len(val) == 1 else val)
                 parent["plugin"]["options"][child.tag] = val
@@ -151,7 +164,7 @@ def _load_plugin(self, element: ET.Element, parent: dict):
                     parent["plugin"]["options"][child.tag] = {
                         key: child.attrib[key]}
 
-    def _load_entry_point(self, element: ET.Element, parent: dict):
+    def _load_entry_point(self, element: ET.Element, parent: dict) -> None:
         """Parses tags associated with initialiser and appends to parent dict
 
         :param elem: xml.etree.ElementTree.Element to be parsed
@@ -162,7 +175,7 @@ def _load_entry_point(self, element: ET.Element, parent: dict):
                                     "class": self._valid_tags[element.tag]}
         self._parse_tags(element, parent[initialiser_name])
 
-    def _load_data(self, element: ET.Element, parent: dict):
+    def _load_data(self, element: ET.Element, parent: dict) -> None:
         """Parses tags associated with initial data files and appends to parent dict
 
         :param elem: xml.etree.ElementTree.Element to be parsed
@@ -172,20 +185,21 @@ def _load_data(self, element: ET.Element, parent: dict):
                      if "name" in element.attrib else "input_data")
         parent[data_name] = {"name": data_name,
                              "class": "data",
-                             "to_load":{}}
+                             "to_load": {}}
         for child in element:
-            assert "file" in child.attrib \
-                or "folder" in child.attrib,\
+            assert "file" in child.attrib or "folder" in child.attrib,\
                 str("XML Parse Error \
-                    \n\tA path to data file must be specified. \
-                    \n\t{0} does not contain the \"file\" tag. \
-                    \n\tCorrect usage: {0} file = <path-to-file>".format(child.tag))
+                        \n\tA path to data file must be specified. \
+                        \n\t{0} does not contain the \"file\" tag. \
+                        \n\tCorrect usage: {0} file = <path-to-file>".format(child.tag))
+
             if "file" in child.attrib:
                 assert self._check_file_exists(child.attrib["file"]),\
                     str("Error: Data file not found. \
                         \n\tFile: {0} does not exist"
                         .format(self._check_filename_abs_rel(child.attrib["file"])))
                 parent[data_name]["to_load"][child.tag] = child.attrib["file"]
+
             if "folder" in child.attrib:
                 assert self._check_file_exists(child.attrib["folder"]),\
                     str("Error: Data folder not found. \
@@ -193,7 +207,7 @@ def _load_data(self, element: ET.Element, parent: dict):
                         .format(self._check_filename_abs_rel(child.attrib["folder"])))
                 parent[data_name]["to_load"][child.tag] = child.attrib["folder"]
 
-    def _load_exit_point(self, element: ET.Element, parent: dict):
+    def _load_exit_point(self, element: ET.Element, parent: dict) -> None:
         """Parses tags associated with output and appends to parent dict
 
         :param elem: xml.etree.ElementTree.Element to be parsed
@@ -203,7 +217,7 @@ def _load_exit_point(self, element: ET.Element, parent: dict):
                             "class": self._valid_tags[element.tag]}
         self._parse_tags(element, parent["output"])
 
-    def _load_loop(self, element: ET.Element, parent: dict):
+    def _load_loop(self, element: ET.Element, parent: dict) -> None:
         """Parses tags associated with loops and appends to parent dict
 
         :param elem: xml.etree.ElementTree.Element to be parsed
@@ -218,7 +232,7 @@ def _load_loop(self, element: ET.Element, parent: dict):
         }
         self._parse_tags(element, parent[loop_name])
 
-    def _load_relationships(self, element: ET.Element, parent: dict):
+    def _load_relationships(self, element: ET.Element, parent: dict) -> None:
         """Parses tags associated with relationships and adds to parent dict
 
         :param elem: xml.etree.ElementTree.Element to be parsed
@@ -232,25 +246,27 @@ def _load_relationships(self, element: ET.Element, parent: dict):
             elif rel.tag == "child":
                 parent["children"].append(rel.attrib["name"])
 
-    def _load_list(self, element: ET.Element, parent: dict):
+    def _load_list(self, element: ET.Element, parent: dict) -> None:
         """Parses elements consisting of lists, e.g. coordinates
 
         :param elem: xml.etree.ElementTree.Element to be parsed
         :param parent: dict or dict fragment parsed tags will be appened to
         """
-        if element.text != None:
+        if element.text is not None:
             parent[element.tag] = self._parse_text_to_list(element)
             if len(parent[element.tag]) == 1:
                 parent[element.tag] = parent[element.tag][0]
 
-    def _parse_text_to_list(self, element: ET.Element) -> list:
+    def _parse_text_to_list(self, element: ET.Element) -> List:
         """Formats raw text data
 
         :param elem: xml.etree.ElementTree.Element to be parsed
         :returns out: list containing parsed text data
         """
-        new = element.text.strip().replace(" ", "")
-        out = new.split("\n")
+        if element.text is not None:
+            new = element.text.strip().replace(" ", "")
+
+        out: List[Any] = new.split("\n")
         raw_elem_text = str()
         for idx in range(0, len(out)):
             raw_elem_text = (raw_elem_text+"\n{}").format(out[idx])
@@ -273,7 +289,6 @@ def _print_xml_config(self):
             self._parse_XML()
         self._print_pretty(self.loaded_modules)
 
-
     def write_to_XML(self):
         """Formats XML Tree correctly, then writes to filename
         TODO: add overwrite check and alternate filename option
@@ -308,8 +323,8 @@ def _indent(self, elem: ET.Element, level: int = 0):
     def _find_dict_with_key_val_pair(self,
                                      parent: dict,
                                      key: str,
-                                     val,
-                                     out = None):
+                                     val: Any,
+                                     out=None) -> List[Dict[str, str]]:
         """Seach nested dict for a given key-value pair
 
         :param parent: dict to be searched
@@ -330,17 +345,22 @@ def _find_dict_with_key_val_pair(self,
                 self._find_dict_with_key_val_pair(parent[k], key, val, out)
         return out
 
-    def _get_element_from_name(self, name: str):
+    def _get_element_from_name(self, name: Union[str, Optional[str]]) -> ET.Element:
         """Find a module name and return its parent tags"""
         if name == None:
             return self.root
+
         elems = self.root.findall(".//*[@name='{0}']".format(name))
+
         unique_elem = [e for e in elems if e.tag !=
                        "parent" and e.tag != "child"]
-        assert len(
-            unique_elem) < 2, "Error: More than one tag with same identifier"
-        assert len(
-            unique_elem) > 0, "Error: No element exists with name \"{0}\"".format(name)
+
+        assert len(unique_elem) < 2,\
+            "Error: More than one tag with same identifier"
+
+        assert len(unique_elem) > 0, \
+            "Error: No element exists with name \"{0}\"".format(name)
+
         return unique_elem[0]
 
     def _get_all_elements_with_tag(self, tag: str):
@@ -389,17 +409,17 @@ def _add_relationships(self,
         return elem
 
     def append_module_relationships(self,
-                        module_name: str,
-                        parents: list,
-                        children: list):
+                                    module_name: str,
+                                    parents: list,
+                                    children: list):
         elem = self._get_element_from_name(module_name)
-        self._add_relationships(elem,parents,children)
+        self._add_relationships(elem, parents, children)
 
     def update_module_coords(self,
-                    module_name: str,
-                    coords: list = None):
+                             module_name: str,
+                             coords: list = None):
         elem = self._get_element_from_name(module_name)
-        self._add_coords(elem,coords)
+        self._add_coords(elem, coords)
 
     def _add_coords(self,
                     elem: ET.Element,
@@ -431,10 +451,10 @@ def _add_plugin_options(self,
                 self._add_plugin_options(plugin_elem, options[key])
 
     def append_input_data(self,
-                            data_name: str,
-                            data_dir: str,
-                            xml_parent: ET.Element or str = "Initialiser",
-                            save_dir_as_relative: bool = True):
+                          data_name: str,
+                          data_dir: str,
+                          xml_parent: Union[ET.Element, str] = "Initialiser",
+                          save_dir_as_relative: bool = True):
         """Appened path to input datafile. Replaces windows backslash
 
         :param plugin_type: string type of plugin to be loaded into module
@@ -454,18 +474,18 @@ def append_input_data(self,
 
         if input_data_elem is None:
             input_data_elem = ET.SubElement(xml_parent, "inputdata")
-            
+
         plugin_elem = ET.SubElement(input_data_elem, data_name)
         if save_dir_as_relative:
-            data_dir = data_dir.replace(self.lib_base_path,"./")
-        data_dir = data_dir.replace("\\","/")
+            data_dir = data_dir.replace(self.lib_base_path, "./")
+        data_dir = data_dir.replace("\\", "/")
         plugin_elem.set('file', data_dir)
 
     def append_plugin_to_module(self,
                                 plugin_type: str,
                                 plugin_options: dict,
-                                xml_parent: ET.Element or str,
-                                overwrite_existing: bool = False
+                                xml_parent: Union[ET.Element, str],
+                                overwrite_existing: Union[bool, int] = False
                                 ):
         """Appened plugin as subelement to existing module element
 
@@ -493,12 +513,12 @@ def append_pipeline_module(self,
                                module_type: str,
                                module_name: str,
                                plugin_type: str,
-                               plugin_options: dict,
+                               plugin_options: Dict[str, Any],
                                parents: list,
                                children: list,
-                               xml_parent_element: str,
+                               xml_parent_element_name: str,
                                coords: list = None
-                               ):
+                               ) -> None:
         """Append new pipeline module to existing XML elementTree to be written later
 
         :param module_type: string declare type of module (UserFeedback,data_processing etc)
@@ -507,20 +527,21 @@ def append_pipeline_module(self,
         :param plugin_options: dict where keys & values are options & values
         :param parents: list of parent names for this module (can be empty)
         :param children: list of child names for this module (can be empty)
-        :param xml_parent_element: str containing name of parent Element for new module
+        :param xml_parent_element_name: str containing name of parent Element for new module
         :param coords [optional]: list of coordinates for UserFeedback canvas
         """
-        xml_parent_element = self._get_element_from_name(xml_parent_element)
+        xml_parent_element: ET.Element = self._get_element_from_name(
+            xml_parent_element_name)
 
         new_mod = ET.Element(module_type.replace(" ", ""))
         new_mod.set('name', module_name)
 
         if plugin_type != None:
             self.append_plugin_to_module(plugin_type,
-                                        plugin_options,
-                                        new_mod,
-                                        0
-                                        )
+                                         plugin_options,
+                                         new_mod,
+                                         0
+                                         )
 
         if xml_parent_element.tag == "loop":
             parents.append(xml_parent_element.attrib["name"])
@@ -536,9 +557,9 @@ def append_pipeline_loop(self,
                              loop_name: str,
                              parents: list,
                              children: list,
-                             xml_parent_element: str = None,
+                             xml_parent_element_name: str = None,
                              coords: list = None
-                             ):
+                             ) -> None:
         """Append new pipeline module to existing XML file
 
         :param loop_type: string declare type of loop (for/while/manual etc)
@@ -546,10 +567,11 @@ def append_pipeline_loop(self,
         :param plugin_type: string type of plugin to be loaded into module
         :param parents: list of parent names for this module (can be empty)
         :param children: list of child names for this module (can be empty)
-        :param xml_parent_element: str containing name of parent Element for new module
+        :param xml_parent_element_name: str containing name of parent Element for new module
         :param coords [optional]: list of coordinates for UserFeedback canvas
         """
-        xml_parent_element = self._get_element_from_name(xml_parent_element)
+        xml_parent_element: ET.Element = self._get_element_from_name(
+            xml_parent_element_name)
 
         new_loop = ET.Element("loop")
         new_loop.set('type', loop_type)
@@ -565,21 +587,25 @@ def append_pipeline_loop(self,
 
         xml_parent_element.append(new_loop)
 
-    def _get_init_data_structure(self):
+    def _get_init_data_structure(self) -> Dict[str, Any]:
         data_struct = self._find_dict_with_key_val_pair(
-                            self.loaded_modules, "class", "data")
+            self.loaded_modules,
+            "class", "data")
+
+        assert len(data_struct) < 2, \
+            "Multiple data with same ID, please check XML"
+
+        out: Dict[str, Any]
         if len(data_struct) == 1:
-            return data_struct[0]
-        elif len(data_struct) > 1:
-            print("Multiple data options specified, please check XML")
-            return data_struct
-        else: 
-            return data_struct
+            out = data_struct[0]
+        else:
+            out = {"to_load": {}}
+        return out
 
     @property
-    def data_to_load(self):
-        return(self._get_init_data_structure()["to_load"])
-        
+    def data_to_load(self) -> Dict[str, str]:
+        return (self._get_init_data_structure()["to_load"])
+
 
 # Use case examples:
 if __name__ == "__main__":
@@ -588,16 +614,18 @@ def data_to_load(self):
     s = XML_handler()
     # s.new_config_file("./resources/example_config.xml")
     # s._get_all_elements_with_tag("loop")
-    # s.load_XML("./resources/example_config.xml")
+    s.load_XML("./Data/resources/xml_files/regerssion_test.xml")
     # s.append_plugin_to_module("Input Data Plugin",{"option":{"test":4}},"Input data",1)
-    s.new_config_file()
-    s.append_input_data("X","./Data/resources/supervised_regression/1/y_train.csv")
-    s.append_input_data("Y","./Data/resources/supervised_regression/1/y_train.csv")
+    # s.new_config_file()
+    # s.append_input_data("X","./Data/resources/supervised_regression/1/y_train.csv")
+    # s.append_input_data("Y","./Data/resources/supervised_regression/1/y_train.csv")
     # print(s.root)
     # print(s.data_to_load)
     # s.update_module_coords("Initialiser",[1,1,1])
     # s.append_module_relationships("Initialiser",["test1","test2"],[])
     s._print_xml_config()
+    a = s.data_to_load
+    print(1)
 
     # s.write_to_XML()
     # s.append_pipeline_loop("for",
diff --git a/aidesign/DataProcessing/DataProcessing_core.py b/aidesign/DataProcessing/DataProcessing_core.py
index c3e2f6a6..f036cd77 100644
--- a/aidesign/DataProcessing/DataProcessing_core.py
+++ b/aidesign/DataProcessing/DataProcessing_core.py
@@ -1,25 +1,26 @@
-# -*- coding: utf-8 -*-
-from aidesign.utils.import_helper import import_plugin_absolute
-from copy import copy
+from aidesign._import_helper import import_plugin_absolute
+from aidesign._types import PluginSpecsInterface, DataInterface, DataProcessingPluginInterface
+
 class DataProcessing(object):
-    def __init__(self):
-        self.output_data = None
+    def __init__(self) -> None:
+        self.output_data: DataInterface
 
-    def set_avail_plugins(self,avail_plugins):
+    def set_avail_plugins(self, avail_plugins: PluginSpecsInterface) -> None:
         self._avail_plugins = avail_plugins
 
-    def set_data_in(self,data_in):
+    def set_data_in(self, data_in: DataInterface) -> None:
         self._data_in = data_in
 
-    def _load_plugin(self, plugin_name:str):
-        avail_plugins = self._avail_plugins.find_from_readable_name(plugin_name)
+    def _load_plugin(self, plugin_name: str) -> None:
+        avail_plugins = self._avail_plugins.find_from_readable_name(
+            plugin_name)
         self._plugin_name = plugin_name
-        self._plugin = import_plugin_absolute(globals(),\
-                                avail_plugins["_PLUGIN_PACKAGE"],\
-                                avail_plugins["_PLUGIN_CLASS_NAME"])\
-                                .__call__()
+        self._plugin: DataProcessingPluginInterface = import_plugin_absolute(globals(),
+                                              avail_plugins["_PLUGIN_PACKAGE"],
+                                              avail_plugins["_PLUGIN_CLASS_NAME"])\
+            .__call__()
 
-    def set_options(self, module_config: dict):
+    def set_options(self, module_config: dict) -> None:
         """Send configuration arguments to plugin
 
         :param module_config: dict of settings to configure the plugin
@@ -27,12 +28,12 @@ def set_options(self, module_config: dict):
         self._module_config = module_config
         self._load_plugin(self._module_config["plugin"]["plugin_name"])
 
-    def launch(self):
+    def launch(self) -> None:
         self._plugin.set_data_in(self._data_in)
         self._plugin.configure(self._module_config["plugin"])
         self._plugin.fit()
-        self.output_data = copy(self._data_in)
+        self.output_data = self._data_in.copy()
         self.output_data = self._plugin.transform(self.output_data)
 
-    def get_result(self):
+    def get_result(self) -> DataInterface:
         return self.output_data
diff --git a/aidesign/DataProcessing/plugins/binarizer.py b/aidesign/DataProcessing/plugins/binarizer.py
index 929466dd..e4761698 100644
--- a/aidesign/DataProcessing/plugins/binarizer.py
+++ b/aidesign/DataProcessing/plugins/binarizer.py
@@ -1,73 +1,43 @@
-from sklearn.preprocessing import Binarizer as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
+
 import pandas as pd
+from sklearn.preprocessing import Binarizer as model
 
-_PLUGIN_READABLE_NAMES = {"Binarizer":"default","binarizer":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"threshold": "float"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"Binarizer":"default","binarizer":"alias"}    # type:ignore  
+_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}                            # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                             # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"threshold": "float"}                      # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}                      # type:ignore
 
-class Binarizer(object):
+class Binarizer(DataProcessingT):
     """
     Binarize data (set feature values to 0 or 1) according to a threshold
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(Binarizer) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
     def transform(self,data):
diff --git a/aidesign/DataProcessing/plugins/kbinsdiscretizer.py b/aidesign/DataProcessing/plugins/kbinsdiscretizer.py
index 456d1405..3e2e26ab 100644
--- a/aidesign/DataProcessing/plugins/kbinsdiscretizer.py
+++ b/aidesign/DataProcessing/plugins/kbinsdiscretizer.py
@@ -1,73 +1,43 @@
-from sklearn.preprocessing import KBinsDiscretizer as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
+
 import pandas as pd
+from sklearn.preprocessing import KBinsDiscretizer as model
 
-_PLUGIN_READABLE_NAMES = {"KBinsDiscretizer":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_bins": "int"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"KBinsDiscretizer":"default"} # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}            # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}             # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_bins": "int"}           # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}      # type:ignore
 
-class KBinsDiscretizer(object):
+class KBinsDiscretizer(DataProcessingT):
     """
     Bin continuous data into interval
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(KBinsDiscretizer) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
     def transform(self,data):
diff --git a/aidesign/DataProcessing/plugins/labelbinarizer.py b/aidesign/DataProcessing/plugins/labelbinarizer.py
index ae337aa8..a9908045 100644
--- a/aidesign/DataProcessing/plugins/labelbinarizer.py
+++ b/aidesign/DataProcessing/plugins/labelbinarizer.py
@@ -1,73 +1,41 @@
 from sklearn.preprocessing import LabelBinarizer as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"LabelBinarizer":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"neg_label": "int", "pos_label": "int"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"LabelBinarizer":"default"}                   # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}                            # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                             # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"neg_label": "int", "pos_label": "int"}    # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}                      # type:ignore
 
-class LabelBinarizer(object):
+class LabelBinarizer(DataProcessingT):
     """
     Binarize labels in a one-vs-all fashion
     """
-
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(LabelBinarizer) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
     def transform(self,data):
diff --git a/aidesign/DataProcessing/plugins/labelencoder.py b/aidesign/DataProcessing/plugins/labelencoder.py
index c800a97c..594c4c20 100644
--- a/aidesign/DataProcessing/plugins/labelencoder.py
+++ b/aidesign/DataProcessing/plugins/labelencoder.py
@@ -1,73 +1,42 @@
 from sklearn.preprocessing import LabelEncoder as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"LabelEncoder":"default","LE":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"LabelEncoder":"default","LE":"alias"}    # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}                        # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                         # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                                      # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                          # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}                  # type:ignore
 
-class LabelEncoder(object):
+class LabelEncoder(DataProcessingT):
     """
     Encode target labels with value between 0 and n_classes-1
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(LabelEncoder) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
     def transform(self,data):
diff --git a/aidesign/DataProcessing/plugins/maxabsscaler.py b/aidesign/DataProcessing/plugins/maxabsscaler.py
index cb1d091a..c513aee0 100644
--- a/aidesign/DataProcessing/plugins/maxabsscaler.py
+++ b/aidesign/DataProcessing/plugins/maxabsscaler.py
@@ -1,73 +1,42 @@
 from sklearn.preprocessing import MaxAbsScaler as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"MaxAbsScaler":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "scaler"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"MaxAbsScaler":"default"}     # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "scaler"}             # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}             # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                          # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}      # type:ignore
 
-class MaxAbsScaler(object):
+class MaxAbsScaler(DataProcessingT):
     """
     Scale each feature by its maximum absolute value
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(MaxAbsScaler) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
     def transform(self,data):
diff --git a/aidesign/DataProcessing/plugins/minmaxscaler.py b/aidesign/DataProcessing/plugins/minmaxscaler.py
index 62bc9a8d..a156adee 100644
--- a/aidesign/DataProcessing/plugins/minmaxscaler.py
+++ b/aidesign/DataProcessing/plugins/minmaxscaler.py
@@ -1,74 +1,43 @@
 from sklearn.preprocessing import MinMaxScaler as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"MinMaxScaler":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "scaler"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"feature_range": "tuple"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"MinMaxScaler":"default"}     # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "scaler"}             # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}             # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"feature_range": "tuple"}  # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}      # type:ignore
 
-class MinMaxScaler(object):
+class MinMaxScaler(DataProcessingT):
     """
     This estimator scales and translates each feature individually such that it\n
     is in the given range on the training set, e.g. between zero and one
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(MinMaxScaler) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
     def transform(self,data):
diff --git a/aidesign/DataProcessing/plugins/multilabelbinarizer.py b/aidesign/DataProcessing/plugins/multilabelbinarizer.py
index f9a3c2d0..0c66f8eb 100644
--- a/aidesign/DataProcessing/plugins/multilabelbinarizer.py
+++ b/aidesign/DataProcessing/plugins/multilabelbinarizer.py
@@ -1,73 +1,42 @@
 from sklearn.preprocessing import MultiLabelBinarizer as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"MultiLabelBinarizer":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"classes": "array-like"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"MultiLabelBinarizer":"default"}  # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}                # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                 # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"classes": "array-like"}       # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}          # type:ignore
 
-class MultiLabelBinarizer(object):
+class MultiLabelBinarizer(DataProcessingT):
     """
     Transform between iterable of iterables and a multilabel format
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(MultiLabelBinarizer) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
     def transform(self,data):
diff --git a/aidesign/DataProcessing/plugins/normalizer.py b/aidesign/DataProcessing/plugins/normalizer.py
index 26212c41..b8273e5d 100644
--- a/aidesign/DataProcessing/plugins/normalizer.py
+++ b/aidesign/DataProcessing/plugins/normalizer.py
@@ -1,77 +1,51 @@
+from aidesign._plugin_templates import DataProcessingT
+from aidesign._types import DataInterface
+
 from sklearn.preprocessing import Normalizer as model
-import numpy as np
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"Normalizer":"default","Norm":"alias","normalizer":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "scaler"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"norm": "str"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"Normalizer": "default",
+                          "Norm": "alias", "normalizer": "alias"}   # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "scaler"}                         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                         # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"norm": "str"}                         # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                          # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X", "Y", "X_tst", 'Y_tst'}                # type:ignore
+
 
-class Normalizer(object):
+class Normalizer(DataProcessingT):
     """
     Normalize samples individually to unit norm
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(Normalizer) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
-    def transform(self,data):
+    def transform(self, data: DataInterface) -> DataInterface:
         data.append_data_column("X", pd.DataFrame(self.proc.transform(self.X)))
         if self.X_tst is not None:
-            data.append_data_column("X_test", pd.DataFrame(self.proc.transform(self.X_tst)))
-        return data
\ No newline at end of file
+            data.append_data_column("X_test", pd.DataFrame(
+                self.proc.transform(self.X_tst)))
+        return data
diff --git a/aidesign/DataProcessing/plugins/onehotencoder.py b/aidesign/DataProcessing/plugins/onehotencoder.py
index 1eb0eb8c..127735f2 100644
--- a/aidesign/DataProcessing/plugins/onehotencoder.py
+++ b/aidesign/DataProcessing/plugins/onehotencoder.py
@@ -1,73 +1,42 @@
 from sklearn.preprocessing import OneHotEncoder as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"OneHotEncoder":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"categories": "array-like"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"OneHotEncoder":"default"}        # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}                # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                 # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"categories": "array-like"}    # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}          # type:ignore
 
-class OneHotEncoder(object):
+class OneHotEncoder(DataProcessingT):
     """
     Encode categorical features as a one-hot numeric array
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(OneHotEncoder) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
     def transform(self,data):
diff --git a/aidesign/DataProcessing/plugins/ordinalencoder.py b/aidesign/DataProcessing/plugins/ordinalencoder.py
index d522d99b..f1d2c347 100644
--- a/aidesign/DataProcessing/plugins/ordinalencoder.py
+++ b/aidesign/DataProcessing/plugins/ordinalencoder.py
@@ -1,77 +1,48 @@
 from sklearn.preprocessing import OrdinalEncoder as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"OrdinalEncoder":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"categories": "array-like"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"OrdinalEncoder": "default"}      # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}                # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                 # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"categories": "array-like"}    # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X", "Y", "X_tst", 'Y_tst'}        # type:ignore
 
-class OrdinalEncoder(object):
+
+class OrdinalEncoder(DataProcessingT):
     """
     Encode categorical features as an integer array
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(OrdinalEncoder) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
-    def transform(self,data):
+    def transform(self, data):
         data.append_data_column("X", pd.DataFrame(self.proc.transform(self.X)))
         if self.X_tst is not None:
-            data.append_data_column("X_test", pd.DataFrame(self.proc.transform(self.X_tst)))
-        return data
\ No newline at end of file
+            data.append_data_column("X_test", pd.DataFrame(
+                self.proc.transform(self.X_tst)))
+        return data
diff --git a/aidesign/DataProcessing/plugins/polynomialfeatures.py b/aidesign/DataProcessing/plugins/polynomialfeatures.py
index 4511a8ef..0bada445 100644
--- a/aidesign/DataProcessing/plugins/polynomialfeatures.py
+++ b/aidesign/DataProcessing/plugins/polynomialfeatures.py
@@ -1,77 +1,52 @@
-from sklearn.preprocessing import PolynomialFeatures as PF
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
+from sklearn.preprocessing import PolynomialFeatures as model
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"PolynomialFeatures":"default","polyfeat":"alias","polynomialfeatures":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "other"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"degree": "int", "interaction_only": "bool", "include_bias": "bool"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"PolynomialFeatures": "default",
+                          "polyfeat": "alias",
+                          "polynomialfeatures": "alias"}    # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "other"}                  # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                 # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"degree": "int",
+                             "interaction_only": "bool",
+                             "include_bias": "bool"}        # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X", "Y", "X_tst", 'Y_tst'}        # type:ignore
 
-class PolynomialFeatures(object):
+
+class PolynomialFeatures(DataProcessingT):
     """ 
     Generate polynomial and interaction features
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
-        self.proc = PF()
-
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(PolynomialFeatures) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
+        self.proc = model()
+
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
+
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
-    def transform(self,data):
+    def transform(self, data):
         data.append_data_column("X", pd.DataFrame(self.proc.transform(self.X)))
         if self.X_tst is not None:
-            data.append_data_column("X_test", pd.DataFrame(self.proc.transform(self.X_tst)))
-        return data
\ No newline at end of file
+            data.append_data_column("X_test", pd.DataFrame(
+                self.proc.transform(self.X_tst)))
+        return data
diff --git a/aidesign/DataProcessing/plugins/quantiletransformer.py b/aidesign/DataProcessing/plugins/quantiletransformer.py
index c56ecc77..472f7358 100644
--- a/aidesign/DataProcessing/plugins/quantiletransformer.py
+++ b/aidesign/DataProcessing/plugins/quantiletransformer.py
@@ -1,77 +1,49 @@
 from sklearn.preprocessing import QuantileTransformer as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"QuantileTransformer":"default","Quantile":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_quantiles": "int"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {
+    "QuantileTransformer": "default", "Quantile": "alias"}  # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "encoder"}                # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                 # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_quantiles": "int"}          # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X", "Y", "X_tst", 'Y_tst'}        # type:ignore
 
-class QuantileTransformer(object):
+
+class QuantileTransformer(DataProcessingT):
     """
     Transform features using quantiles information
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(QuantileTransformer) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
-    def transform(self,data):
+    def transform(self, data):
         data.append_data_column("X", pd.DataFrame(self.proc.transform(self.X)))
         if self.X_tst is not None:
-            data.append_data_column("X_test", pd.DataFrame(self.proc.transform(self.X_tst)))
-        return data
\ No newline at end of file
+            data.append_data_column("X_test", pd.DataFrame(
+                self.proc.transform(self.X_tst)))
+        return data
diff --git a/aidesign/DataProcessing/plugins/standardscaler.py b/aidesign/DataProcessing/plugins/standardscaler.py
index 7f6c8c0a..34904d0e 100644
--- a/aidesign/DataProcessing/plugins/standardscaler.py
+++ b/aidesign/DataProcessing/plugins/standardscaler.py
@@ -1,77 +1,49 @@
 from sklearn.preprocessing import StandardScaler as model
-import numpy as np
+from aidesign._plugin_templates import DataProcessingT
 import pandas as pd
 
-_PLUGIN_READABLE_NAMES = {"StandardScaler":"default","standardscaler":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "scaler"}
-_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}
-_PLUGIN_OPTIONAL_SETTINGS = {"with_mean": "bool"}
-_PLUGIN_REQUIRED_DATA = {}
-_PLUGIN_OPTIONAL_DATA = {"X","Y","X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {
+    "StandardScaler": "default", "standardscaler": "alias"}     # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "scaler"}                     # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"Data": "str"}                     # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"with_mean": "bool"}               # type:ignore
+_PLUGIN_REQUIRED_DATA = {}                                      # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X", "Y", "X_tst", 'Y_tst'}            # type:ignore
 
-class StandardScaler(object):
+
+class StandardScaler(DataProcessingT):
     """
     Standardize features by removing the mean and scaling to unit variance
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.proc = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(StandardScaler) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        if self._config["options"]["Data"] == 'X':
-            self.X  = self._data_in["X"]
-            self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        elif self._config["options"]["Data"] == 'Y':
-            self.X  = self._data_in["Y"]
-            self.X_tst = self._is_name_passed(self._data_in, "Y_test")
-        else:
-            print('Invalid Data name. Indicate whether to use `X` or `Y`')
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        new_dict = {}
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if key != 'Data':
-                if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                    val = float(val)
-                    if val.is_integer():
-                        val = int(val)
-                new_dict[key] = val
-        return new_dict
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def fit(self):
-        config = self._check_numeric(self._config["options"])
-        self.proc.set_params(**config)
+        cleaned_options = self._clean_solver_options()
+        self.proc.set_params(**cleaned_options)
         self.proc.fit(self.X)
 
-    def transform(self,data):
+    def transform(self, data):
         data.append_data_column("X", pd.DataFrame(self.proc.transform(self.X)))
         if self.X_tst is not None:
-            data.append_data_column("X_test", pd.DataFrame(self.proc.transform(self.X_tst)))
-        return data
\ No newline at end of file
+            data.append_data_column("X_test", pd.DataFrame(
+                self.proc.transform(self.X_tst)))
+        return data
diff --git a/aidesign/GUI/GUI_core.py b/aidesign/GUI/GUI_core.py
index 9a9b5cf9..17f174b8 100644
--- a/aidesign/GUI/GUI_core.py
+++ b/aidesign/GUI/GUI_core.py
@@ -1,10 +1,11 @@
-# -*- coding: utf-8 -*-
+from aidesign.Data.xml_handler import XML_handler
+from aidesign._import_helper import import_plugin_absolute
+from aidesign._types import DataInterface, PluginSpecsInterface, DictT
+
+from typing import Any
 import tkinter as tk
 from tkinter.font import Font
-from aidesign.utils.import_helper import import_plugin_absolute
-from aidesign.utils.plugin_helpers import PluginSpecs
 
-from aidesign.Data.xml_handler import XML_handler
 
 class GUI(tk.Tk):
     """
@@ -28,18 +29,26 @@ def __init__(self, *args, **kwargs):
         self.startpage = False
         self.output = {}
 
-    def set_avail_plugins(self,avail_plugins):
+    def set_avail_plugins(self, avail_plugins: PluginSpecsInterface):
         self._avail_plugins = avail_plugins
 
-    def set_data_in(self,data_in):
+    def set_data_in(self, data_in: DataInterface):
         self._data_in = data_in
 
+    def set_options(self, module_config: DictT):
+        """Send configuration arguments to GUI
+
+        :param module_config: dict of settings to congfigure the plugin
+        """
+        self._module_config = module_config
+        self._load_plugin(self._module_config["plugin"]["plugin_name"])
+
     def set_gui_as_startpage(self):
         self.startpage = True
         self._load_plugin("main")
         self.s = XML_handler()
         self.s.new_config_file()
-        
+
     def set_gui(self):
         self.startpage = True
         self._load_plugin("pipelineCanvas")
@@ -98,33 +107,28 @@ def _load_plugin(self, ui_type: list):
                 print(
                     "Error: User Interface \"{0}\" not recognised. \
                     \nAvailable methods are: \
-                    \n  - {1}"\
+                    \n  - {1}"
                     .format(ui, ",\n  - ".join(
                         [i for i in self._avail_plugins.available_plugin_names])))
                 exit(1)
 
-    def _append_to_output(self, key:str, value:any):
+    def _append_to_output(self, key: str, value: Any):
         self.output[key] = value
 
-    def set_options(self, module_config: dict):
-        """Send configuration arguments to GUI
-
-        :param module_config: dict of settings to congfigure the plugin
-        """
-        self._module_config = module_config
-        self._load_plugin(self._module_config["plugin"]["plugin_name"])
-
     def _show_frame(self, page_name):
         '''Show a frame for the given page name'''
-        if isinstance(page_name,dict):
+        if isinstance(page_name, dict):
             page_name = page_name["_PLUGIN_CLASS_NAME"]
         frame = self.frames[page_name]
         frame.tkraise()
 
-    def _on_closing(self):
+    def _on_closing(self) -> None:
         self.closed = True
         self.destroy()
-        
+
+    def destroy(self) -> None:
+        return super().destroy()
+
     def launch(self):
         """Runs UserInterface Plugin. 
         If multiple frames exist, they are stacked
@@ -133,9 +137,7 @@ def launch(self):
         container.pack(side="top", fill="both", expand=True)
         container.grid_rowconfigure(0, weight=1)
         container.grid_columnconfigure(0, weight=1)
-
-        
-        
+        self.output_data= []
         self.frames = {}
         for F in self._desired_ui_types:
             page_name = F.__name__
@@ -146,8 +148,16 @@ def launch(self):
             self.frames[page_name] = frame
 
             frame.grid(row=0, column=0, sticky="nsew")
+            self.output_data.append(frame.out_data)
 
         self._show_frame(self._top_ui_layer)
         self.protocol("WM_DELETE_WINDOW", self._on_closing)
         self.mainloop()
         return self.output
+
+    def get_result(self):
+        """TODO: This currently does not work, as the output data is stored in a list
+        This is due to the way this module is constructed, it cycles through plugins, 
+        which is useful for the startpage, but complicates things when running
+        UserInterface plugins"""
+        return self.output_data
\ No newline at end of file
diff --git a/aidesign/GUI/User_Feedback_template.py b/aidesign/GUI/User_Feedback_template.py
index aa6ee9b7..b63161ad 100644
--- a/aidesign/GUI/User_Feedback_template.py
+++ b/aidesign/GUI/User_Feedback_template.py
@@ -1,30 +1,8 @@
-from abc import ABC, abstractmethod
-
-"""Defines the parent class for all potential methods of user interface for the framework
-
-Todo: declare what is commonly needed for this module and add below (WIP)
-"""
-
-class UI(ABC):
-    @property
-    def class_list(self):
-        pass
-        # """Set the class list"""
-        
-    @class_list.setter
-    @abstractmethod
-    def class_list(self,value):
-        pass
-    
-
-    @abstractmethod
-    def save_file(self):
-        pass
-
-    @abstractmethod
-    def save_file_as(self):
-        pass
-
-    # @abstractmethod
-    # def on_press(self):
-    #     pass
+from aidesign.UserInteraction.User_Interaction_template import UI
+
+class UserFeedbackTemplate(UI):
+    """UserFeedbackTemplate directly duplicates:
+        aidesign.UserInteraction.User_Interaction_template
+    """
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
\ No newline at end of file
diff --git a/aidesign/InputData/InputData_core.py b/aidesign/InputData/InputData_core.py
index 97a81d24..9f2b40a0 100644
--- a/aidesign/InputData/InputData_core.py
+++ b/aidesign/InputData/InputData_core.py
@@ -1,29 +1,41 @@
-# -*- coding: utf-8 -*-
-class Modelling(object):
+from aidesign.Data.Data_core import Data
+from aidesign._import_helper import import_plugin_absolute
+from aidesign._types import PluginSpecsInterface, DataInterface
+
+class InputData(Data):
     def __init__(self):
+        super().__init__()
         self.node_name = None
         self.plugin_name = None
         self.output_data = None
 
-    def set_input_data(self, data):
-        self.input_data = data
+    def set_data_in(self, data_in: DataInterface) -> None:
+        """Pass existing data from another module to be stored in this class"""
+        self._data_in = data_in
 
-    def set_target_data(self, data):
-        self.target_data = data
+    def load_data_from_file(self, filename: str, data_id: str) -> None:
+        """Load data from file. Calls parent class method to store data in self.data"""
+        super().import_data(filename, data_id)
 
-    def _load_plugin(self):
-        pass
+    def set_options(self, module_config: dict) -> None:
+        """Send configuration arguments to plugin
 
-    def set_plugin_name(self, plugin_name):
-        self.plugin_name = plugin_name
-        self._load_plugin()
+        :param module_config: dict of settings to configure the plugin
+        """
+        self._module_config = module_config
+        self._load_plugin(self._module_config["plugin"]["plugin_name"])
 
-    def set_options(self, options):
-        self.options = options
+    def set_avail_plugins(self, avail_plugins: PluginSpecsInterface) -> None:
+        self._avail_plugins = avail_plugins
 
-    def solve(self):
-        # send the input data to the modelling plugin and assign it to an output property
-        pass
+    def _load_plugin(self, plugin_name: str) -> None:
+        avail_plugins = self._avail_plugins.find_from_readable_name(
+            plugin_name)
+        self._plugin_name = plugin_name
+        self._plugin: PluginSpecsInterface = import_plugin_absolute(globals(),
+                                                    avail_plugins["_PLUGIN_PACKAGE"],
+                                                    avail_plugins["_PLUGIN_CLASS_NAME"])\
+                                                    .__call__()
 
     def get_result(self):
-        return self.output_data
+        return self._data_in
diff --git a/aidesign/InputData/__init__.py b/aidesign/InputData/__init__.py
index 99e78a66..faf6a263 100644
--- a/aidesign/InputData/__init__.py
+++ b/aidesign/InputData/__init__.py
@@ -3,5 +3,4 @@
 """Import all submodules inside this folder and expose them with their respective names
 
 Let's look for a more automatic way of doing this when we have more modules
-"""
-from .Modelling_core import Modelling
+"""
\ No newline at end of file
diff --git a/aidesign/Modelling/Modelling_core.py b/aidesign/Modelling/Modelling_core.py
index 65fa4d5c..7ca67626 100644
--- a/aidesign/Modelling/Modelling_core.py
+++ b/aidesign/Modelling/Modelling_core.py
@@ -1,6 +1,5 @@
 # -*- coding: utf-8 -*-
-from aidesign.utils.import_helper import import_plugin_absolute
-from copy import copy
+from aidesign._import_helper import import_plugin_absolute
 class Modelling(object):
     def __init__(self):
         self.output_data = None
@@ -31,8 +30,8 @@ def launch(self):
         self._plugin.set_data_in(self._data_in)
         self._plugin.configure(self._module_config["plugin"])
         self._plugin.solve()
-        self.output_data = copy(self._data_in)
-        self.output_data = self._plugin._test(self.output_data)
+        self.output_data = self._data_in.copy()
+        # self.output_data = self._plugin._test(self.output_data)
 
     def get_result(self):
         return self.output_data
diff --git a/aidesign/Modelling/plugins/affinitypropagation.py b/aidesign/Modelling/plugins/affinitypropagation.py
index 3d6292cf..2f0b529e 100644
--- a/aidesign/Modelling/plugins/affinitypropagation.py
+++ b/aidesign/Modelling/plugins/affinitypropagation.py
@@ -1,92 +1,61 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.cluster import AffinityPropagation as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"Birch":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "clustering"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"damping": "float"}
-_PLUGIN_REQUIRED_DATA = {"X"}
-_PLUGIN_OPTIONAL_DATA = {"Y", "X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"Birch": "default"}       # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "clustering"}     # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                      # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"damping": "float"}    # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X"}                       # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"Y", "X_tst", 'Y_tst'}     # type:ignore
 
-class AffinityPropagation(object):
+
+class AffinityPropagation(ModellingPluginT):
     """
     Perform Affinity Propagation Clustering of data
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(AffinityPropagation) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._is_name_passed(self._data_in, "Y")).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/bayesianridge.py b/aidesign/Modelling/plugins/bayesianridge.py
index 3e58bd39..a4e29dbb 100644
--- a/aidesign/Modelling/plugins/bayesianridge.py
+++ b/aidesign/Modelling/plugins/bayesianridge.py
@@ -1,92 +1,61 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.linear_model import BayesianRidge as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"BayesianRidge":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_iter": "int"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"BayesianRidge": "default"}   # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_iter": "int"}           # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                      # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}              # type:ignore
 
-class BayesianRidge(object):
+
+class BayesianRidge(ModellingPluginT):
     """
     Bayesian ridge regression
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(BayesianRidge) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/birch.py b/aidesign/Modelling/plugins/birch.py
index b84b3b64..4b426d3c 100644
--- a/aidesign/Modelling/plugins/birch.py
+++ b/aidesign/Modelling/plugins/birch.py
@@ -1,92 +1,62 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.cluster import Birch as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"Birch":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "clustering"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_clusters": "int", "threshold": "float"}
-_PLUGIN_REQUIRED_DATA = {"X"}
-_PLUGIN_OPTIONAL_DATA = {"Y", "X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"Birch": "default"}       # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "clustering"}     # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                      # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_clusters": "int",   # type:ignore
+                             "threshold": "float"}  # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X"}                       # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"Y", "X_tst", 'Y_tst'}     # type:ignore
 
-class Birch(object):
+
+class Birch(ModellingPluginT):
     """
     Implements the BIRCH clustering algorithm
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(Birch) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._is_name_passed(self._data_in, "Y")).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/decisiontreeclassifier.py b/aidesign/Modelling/plugins/decisiontreeclassifier.py
index a13034b2..33d4f140 100644
--- a/aidesign/Modelling/plugins/decisiontreeclassifier.py
+++ b/aidesign/Modelling/plugins/decisiontreeclassifier.py
@@ -1,92 +1,62 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.tree import DecisionTreeClassifier as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"DecissionTreeClassifier":"default","DTClassifier":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"max_depth": "int"} # model().get_params()
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"DecissionTreeClassifier": "default",
+                          "DTClassifier": "alias"}              # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}             # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"max_depth": "int"}                # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                      # type:ignore
 
-class DecisionTreeClassifier(object):
+
+class DecisionTreeClassifier(ModellingPluginT):
     """
     A decision tree classifier
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(DecisionTreeClassifier) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/decisiontreeregressor.py b/aidesign/Modelling/plugins/decisiontreeregressor.py
index 23899e5d..64cb7d80 100644
--- a/aidesign/Modelling/plugins/decisiontreeregressor.py
+++ b/aidesign/Modelling/plugins/decisiontreeregressor.py
@@ -1,92 +1,62 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.tree import DecisionTreeRegressor as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"DecisionTreeRegressor":"default","DTregressor":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"max_depth": "int"} # model().get_params()
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"DecisionTreeRegressor": "default",
+                          "DTregressor": "alias"}               # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}                 # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"max_depth": "int"}                # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                      # type:ignore
 
-class DecisionTreeRegressor(object):
+
+class DecisionTreeRegressor(ModellingPluginT):
     """
     A decision tree regressor
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(DecisionTreeRegressor) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/elasticnet.py b/aidesign/Modelling/plugins/elasticnet.py
index c4714e76..1621561e 100644
--- a/aidesign/Modelling/plugins/elasticnet.py
+++ b/aidesign/Modelling/plugins/elasticnet.py
@@ -1,92 +1,62 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.linear_model import ElasticNet as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"ElasticNet":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float", "l1_ratio": "float"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"ElasticNet": "default"}      # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float",
+                             "l1_ratio": "float"}       # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                      # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}              # type:ignore
 
-class ElasticNet(object):
+
+class ElasticNet(ModellingPluginT):
     """
     Linear regression with combined L1 and L2 priors as regularizer
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(ElasticNet) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X_tst, Y_tst, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X_tst : array-like of shape (n_samples, n_features)
+        :param  Y_tst :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X_tst, Y_tst, sample_weight)
diff --git a/aidesign/Modelling/plugins/gpclassifier.py b/aidesign/Modelling/plugins/gpclassifier.py
index 3e3d3467..307bf85d 100644
--- a/aidesign/Modelling/plugins/gpclassifier.py
+++ b/aidesign/Modelling/plugins/gpclassifier.py
@@ -1,92 +1,64 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.gaussian_process import GaussianProcessClassifier as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"GPClassifier":"default","GPC":"alias","GaussianProcessClassifier":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_restarts_optimizer": "int", "random_state": "int"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"GPClassifier": "default",
+                          "GPC": "alias",
+                          "GaussianProcessClassifier": "alias"}     # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}                 # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                      # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_restarts_optimizer": "int",
+                             "random_state": "int"}                 # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                                  # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                          # type:ignore
 
-class GPClassifier(object):
+
+class GPClassifier(ModellingPluginT):
     """
     Gaussian process Classifier (GPC) based on Laplace approximation
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(GPClassifier) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/gpregressor.py b/aidesign/Modelling/plugins/gpregressor.py
index 972ea19d..c07b3f06 100644
--- a/aidesign/Modelling/plugins/gpregressor.py
+++ b/aidesign/Modelling/plugins/gpregressor.py
@@ -1,92 +1,64 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.gaussian_process import GaussianProcessRegressor as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"GPRegressor":"default","GPR":"alias","GaussianProcessRegressor":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_restarts_optimizer": "int", "random_state": "int"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"GPRegressor": "default",
+                          "GPR": "alias",
+                          "GaussianProcessRegressor": "alias"}      # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}                     # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                      # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_restarts_optimizer": "int",
+                             "random_state": "int"}                 # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                                  # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                          # type:ignore
 
-class GPRegressor(object):
+
+class GPRegressor(ModellingPluginT):
     """
     Gaussian process regressor
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(GPRegressor) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/kernelridge.py b/aidesign/Modelling/plugins/kernelridge.py
index 196a181e..eb73e53f 100644
--- a/aidesign/Modelling/plugins/kernelridge.py
+++ b/aidesign/Modelling/plugins/kernelridge.py
@@ -1,92 +1,65 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.kernel_ridge import KernelRidge as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"KernelRidge":"default","KR":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float", "kernel": "str", "gamma": "float", "degree": "int"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"KernelRidge": "default",
+                          "KR": "alias"}                # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float",
+                             "kernel": "str",
+                             "gamma": "float",
+                             "degree": "int"}           # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                      # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}              # type:ignore
 
-class KernelRidge(object):
+
+class KernelRidge(ModellingPluginT):
     """
     Kernel ridge regression.
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(KernelRidge) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/kmeans.py b/aidesign/Modelling/plugins/kmeans.py
index 1f5587cc..0879145b 100644
--- a/aidesign/Modelling/plugins/kmeans.py
+++ b/aidesign/Modelling/plugins/kmeans.py
@@ -1,92 +1,62 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.cluster import KMeans as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"KMeans":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "clustering"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_clusters": "int", "n_init": "int"}
-_PLUGIN_REQUIRED_DATA = {"X"}
-_PLUGIN_OPTIONAL_DATA = {"Y", "X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"KMeans": "default"}      # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "clustering"}     # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                      # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_clusters": "int",
+                             "n_init": "int"}       # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X"}                       # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"Y", "X_tst", 'Y_tst'}     # type:ignore
 
-class KMeans(object):
+
+class KMeans(ModellingPluginT):
     """
     K-Means clustering
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(KMeans) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._is_name_passed(self._data_in, "Y")).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/knnclassifier.py b/aidesign/Modelling/plugins/knnclassifier.py
index b726d02d..40dc3610 100644
--- a/aidesign/Modelling/plugins/knnclassifier.py
+++ b/aidesign/Modelling/plugins/knnclassifier.py
@@ -1,92 +1,63 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.neighbors import KNeighborsClassifier as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"KNNClassifier":"default","KNN-C":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_neighbors":"int", "weights": "str"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"KNNClassifier": "default",
+                          "KNN-C": "alias"}             # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}     # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_neighbors": "int",
+                             "weights": "str"}          # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                      # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}              # type:ignore
 
-class KNNclassifier(object):
+
+class KNNclassifier(ModellingPluginT):
     """
     Classifier implementing the k-nearest neighbors vote
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(KNNclassifier) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/knnregressor.py b/aidesign/Modelling/plugins/knnregressor.py
index efe0d888..78e4c78a 100644
--- a/aidesign/Modelling/plugins/knnregressor.py
+++ b/aidesign/Modelling/plugins/knnregressor.py
@@ -1,92 +1,63 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.neighbors import KNeighborsRegressor as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"KNNRegressor":"default","KNN-R":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"n_neighbors":"int", "weights": "str"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"KNNRegressor": "default",
+                          "KNN-R": "alias"}             # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"n_neighbors": "int",
+                             "weights": "str"}          # type:ignore  
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                      # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}              # type:ignore
 
-class KNNRegressor(object):
+
+class KNNRegressor(ModellingPluginT):
     """
     Regression based on k-nearest neighbors
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(KNNRegressor) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/lasso.py b/aidesign/Modelling/plugins/lasso.py
index a26d72b8..abc8d026 100644
--- a/aidesign/Modelling/plugins/lasso.py
+++ b/aidesign/Modelling/plugins/lasso.py
@@ -1,92 +1,60 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.linear_model import Lasso as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"Lasso":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"Lasso":"default"}        # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}     # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                      # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float"}      # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X","Y"}                   # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}          # type:ignore
 
-class Lasso(object):
+class Lasso(ModellingPluginT):
     """
     Linear Model trained with L1 prior as regularizer
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(Lasso) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/linearregression.py b/aidesign/Modelling/plugins/linearregression.py
index 44e51aae..fbb6232d 100644
--- a/aidesign/Modelling/plugins/linearregression.py
+++ b/aidesign/Modelling/plugins/linearregression.py
@@ -1,92 +1,62 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.linear_model import LinearRegression as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"LinearRegression":"default","LR":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"LinearRegression": "default",
+                          "LR": "alias"}                    # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}             # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                              # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                              # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                          # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                  # type:ignore
 
-class LinearRegression(object):
+
+class LinearRegression(ModellingPluginT):
     """
     Ordinary least squares Linear Regression
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(LinearRegression) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/logisticregression.py b/aidesign/Modelling/plugins/logisticregression.py
index 9d4843f6..7ad50423 100644
--- a/aidesign/Modelling/plugins/logisticregression.py
+++ b/aidesign/Modelling/plugins/logisticregression.py
@@ -1,92 +1,63 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.linear_model import LogisticRegression as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"LogisticRegression":"default","logit":"alias","MaxEnt":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"penalty":"str", "C": "float"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"LogisticRegression": "default",
+                          "logit": "alias",
+                          "MaxEnt": "alias"}                    # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}             # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"penalty": "str", "C": "float"}    # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                      # type:ignore
 
-class LogisticRegression(object):
+
+class LogisticRegression(ModellingPluginT):
     """
     Logistic regression classification.
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(LogisticRegression) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/meanshift.py b/aidesign/Modelling/plugins/meanshift.py
index e06a1ba9..a2994637 100644
--- a/aidesign/Modelling/plugins/meanshift.py
+++ b/aidesign/Modelling/plugins/meanshift.py
@@ -1,92 +1,60 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.cluster import MeanShift as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"MeanShift":"default"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "clustering"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"bandwidth": "float"}
-_PLUGIN_REQUIRED_DATA = {"X"}
-_PLUGIN_OPTIONAL_DATA = {"Y", "X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"MeanShift":"default"}        # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "clustering"}         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"bandwidth": "float"}      # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X"}                           # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"Y", "X_tst", 'Y_tst'}         # type:ignore
 
-class MeanShift(object):
+class MeanShift(ModellingPluginT):
     """
     Mean shift clustering using a flat kernel.
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(MeanShift) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._is_name_passed(self._data_in, "Y")).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/passiveaggressiveclassifier.py b/aidesign/Modelling/plugins/passiveaggressiveclassifier.py
index 57e38ca9..289efa3b 100644
--- a/aidesign/Modelling/plugins/passiveaggressiveclassifier.py
+++ b/aidesign/Modelling/plugins/passiveaggressiveclassifier.py
@@ -1,92 +1,62 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.linear_model import PassiveAggressiveClassifier as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"PassiveAggressiveClassifier":"default","PassiveAgressive":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"C": "float"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"PassiveAggressiveClassifier": "default",
+                          "PassiveAgressive": "alias"}                  # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}                     # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"C": "float"}                              # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                                      # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                              # type:ignore
 
-class PassiveAggressiveClassifier(object):
+
+class PassiveAggressiveClassifier(ModellingPluginT):
     """
     Passive aggressive classifier
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(PassiveAggressiveClassifier) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
-
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: Maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/perceptron.py b/aidesign/Modelling/plugins/perceptron.py
index 4f727ae7..4683321c 100644
--- a/aidesign/Modelling/plugins/perceptron.py
+++ b/aidesign/Modelling/plugins/perceptron.py
@@ -1,92 +1,63 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.linear_model import Perceptron as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"Perceptron":"default","LinearPerceptron":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float", "penalty": "str"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"Perceptron": "default",
+                          "LinearPerceptron": "alias"}      # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                              # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float",
+                             "penalty": "str"}              # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                          # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                  # type:ignore
 
-class Perceptron(object):
+
+class Perceptron(ModellingPluginT):
     """
     Linear perceptron classification
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(Perceptron) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/randomforestclassifier.py b/aidesign/Modelling/plugins/randomforestclassifier.py
index e34a69b8..dcfdc0c9 100644
--- a/aidesign/Modelling/plugins/randomforestclassifier.py
+++ b/aidesign/Modelling/plugins/randomforestclassifier.py
@@ -1,92 +1,64 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.ensemble import RandomForestClassifier as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"RandomForestClassifier":"default","RFclassifier":"alias","RFC":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"max_depth": "int", "n_estimators": "int"} # model().get_params()
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"RandomForestClassifier": "default",
+                          "RFclassifier": "alias",
+                          "RFC": "alias"}                       # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}             # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"max_depth": "int",
+                             "n_estimators": "int"}             # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                      # type:ignore
 
-class RandomForestClassifier(object):
+
+class RandomForestClassifier(ModellingPluginT):
     """
     A random forest classifier
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(RandomForestClassifier) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/randomforestregressor.py b/aidesign/Modelling/plugins/randomforestregressor.py
index 1f2470c7..77bd729f 100644
--- a/aidesign/Modelling/plugins/randomforestregressor.py
+++ b/aidesign/Modelling/plugins/randomforestregressor.py
@@ -1,92 +1,64 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.ensemble import RandomForestRegressor as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"RandomForestRegressor":"default","RFRegressor":"alias","RFR":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"max_depth": "int", "n_estimators": "int"} # model().get_params()
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"RandomForestRegressor": "default",
+                          "RFRegressor": "alias",
+                          "RFR": "alias"}               # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"max_depth": "int",
+                             "n_estimators": "int"}     # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                      # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}              # type:ignore
 
-class RandomForestRegressor(object):
+
+class RandomForestRegressor(ModellingPluginT):
     """
     A random forest regression
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(RandomForestRegressor) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/ridgeregression.py b/aidesign/Modelling/plugins/ridgeregression.py
index 117e4e0d..273834eb 100644
--- a/aidesign/Modelling/plugins/ridgeregression.py
+++ b/aidesign/Modelling/plugins/ridgeregression.py
@@ -1,92 +1,62 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.linear_model import Ridge as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"Ridge":"default", "RidgeRegression": "alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"Ridge": "default",
+                          "RidgeRegression": "alias"}       # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}             # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                              # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"alpha": "float"}              # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                          # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                  # type:ignore
 
-class RidgeRegression(object):
+
+class RidgeRegression(ModellingPluginT):
     """
     Linear least squares with l2 regularization
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(RidgeRegression) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
\ No newline at end of file
diff --git a/aidesign/Modelling/plugins/svc.py b/aidesign/Modelling/plugins/svc.py
index 17ec4fd6..cce3c59c 100644
--- a/aidesign/Modelling/plugins/svc.py
+++ b/aidesign/Modelling/plugins/svc.py
@@ -1,92 +1,65 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.svm import SVC as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"SVC":"default","SupportVectorClassification":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"C": "float", "kernel": "str", "gamma": "float", "degree": "int"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"SVC": "default",
+                          "SupportVectorClassification": "alias"}   # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "classification"}                 # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                      # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"C": "float",
+                             "kernel": "str",
+                             "gamma": "float",
+                             "degree": "int"}                       # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                                  # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                          # type:ignore
 
-class SVC(object):
+
+class SVC(ModellingPluginT):
     """
     C-Support Vector Classification
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(SVC) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/Modelling/plugins/svr.py b/aidesign/Modelling/plugins/svr.py
index 2bb2e1c4..fc1a9ff9 100644
--- a/aidesign/Modelling/plugins/svr.py
+++ b/aidesign/Modelling/plugins/svr.py
@@ -1,92 +1,65 @@
+from aidesign._plugin_templates import ModellingPluginT
 from sklearn.svm import SVR as model
-import numpy as np
 
-_PLUGIN_READABLE_NAMES = {"SVR":"default","SupportVectorRegression":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {"C": "float", "kernel": "str", "gamma": "float", "degree": "int"}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}
+_PLUGIN_READABLE_NAMES = {"SVR": "default",
+                          "SupportVectorRegression": "alias"}   # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"Type": "regression"}                 # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                  # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {"C": "float",
+                             "kernel": "str",
+                             "gamma": "float",
+                             "degree": "int"}                   # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                              # type:ignore
+_PLUGIN_OPTIONAL_DATA = {"X_tst", 'Y_tst'}                      # type:ignore
 
-class SVR(object):
+
+class SVR(ModellingPluginT):
     """
     Epsilon-Support Vector Regression
     """
 
     def __init__(self):
-        self.X = None
-        self.Y = None
-        self.X_tst = None
-        self.Y_tst = None
+        """Initialises parent class. 
+            Passes `globals` dict of all current variables
+        """
+        super().__init__(globals())
         self.clf = model()
 
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
-        if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format(SVR) \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
-                            + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
-        self._data_in = data_in
-
-    def configure(self, config:dict):
-        self._config = config
-        self._parse_config()
-
-    def _parse_config(self):
-        self.X = self._data_in["X"]
-        self.Y = np.array(self._data_in["Y"]).ravel()
-        self.X_tst = self._is_name_passed(self._data_in, "X_test")
-        self.Y_tst = np.array(self._is_name_passed(self._data_in, "Y_test")).ravel()
-        # print(self._config["options"])
+    def configure(self, config: dict):
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        super().configure(config)
 
-    def _is_name_passed(self, dic: dict, key: str, default = None):
-        return dic[key] if key in dic.keys() and dic[key] is not None else default
-    
-    def _reshape(self,data,shape):
-        return data.reshape(shape[0],shape[1])
-    
-    def _check_numeric(self, dict_opt):
-        for key, val in dict_opt.items():
-            """ 
-            TODO: maybe, if list -> cv
-            """
-            if type(val) == str and val.replace('.','').replace(',','').isnumeric():
-                val = float(val)
-                if val.is_integer():
-                    val = int(val)
-            dict_opt[key] = val
-        return dict_opt
+    def set_data_in(self, data_in):
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        super().set_data_in(data_in)
 
     def solve(self):
-        self._check_numeric(self._config["options"])
+        """Sends params to solver, then runs solver"""
         self.clf.set_params(**self._config["options"])
         self.clf.fit(self.X, self.Y)
 
-    def predict(self,data):
+    def predict(self, data):
+        """Uses fitted model to predict output of a given Y
+        :param data: array-like or sparse matrix, shape (n_samples, n_features)
+                    Samples
+                    expected type: aidesign.Data.Data_core.Data
+        :returns: array, shape (n_samples,)
+                    Returns predicted values.
+        """
         return self.clf.predict(data)
 
-    def score(self,X_tst, Y_tst):
-        return self.clf.score(X_tst, Y_tst)
+    def score(self, X, Y, sample_weight=None):
+        """Return the coefficient of determination
+        :param  X : array-like of shape (n_samples, n_features)
+        :param  Y :  array-like of shape (n_samples,) or (n_samples, n_outputs)
+        :param sample_weight : array-like of shape (n_samples,), default=None
+                    Sample weights.
 
-    def _test(self, data):
-        if _PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
-            print('Training accuracy: %.2f%%' %(self.score(self.X, self.Y)*100))
-            if self.Y_tst is not None:
-                print('Test accuracy: %.2f%%' %(self.score(self.X_tst, self.Y_tst)*100))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
-            print('Training R2 score: %.3f' %(self.score(self.X, self.Y)))
-            if self.Y_tst is not None:
-                print('Training R2 score: %.3f' %(self.score(self.X_tst, self.Y_tst)))
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
-        elif _PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
-            print('Clustering completed')
-            if self.X_tst is not None:
-                data.append_data_column("Y_pred", self.predict(self.X_tst))
-            return data
\ No newline at end of file
+        :returns: score : float R^2` of ``self.predict(X)`` wrt. `y`.
+        """
+        return self.clf.score(X, Y, sample_weight=None)
diff --git a/aidesign/UserInteraction/User_Interaction_template.py b/aidesign/UserInteraction/User_Interaction_template.py
deleted file mode 100644
index aa6ee9b7..00000000
--- a/aidesign/UserInteraction/User_Interaction_template.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from abc import ABC, abstractmethod
-
-"""Defines the parent class for all potential methods of user interface for the framework
-
-Todo: declare what is commonly needed for this module and add below (WIP)
-"""
-
-class UI(ABC):
-    @property
-    def class_list(self):
-        pass
-        # """Set the class list"""
-        
-    @class_list.setter
-    @abstractmethod
-    def class_list(self,value):
-        pass
-    
-
-    @abstractmethod
-    def save_file(self):
-        pass
-
-    @abstractmethod
-    def save_file_as(self):
-        pass
-
-    # @abstractmethod
-    # def on_press(self):
-    #     pass
diff --git a/aidesign/UserInteraction/plugins/CanvasInput.py b/aidesign/UserInteraction/plugins/CanvasInput.py
index 158e8a61..25811d95 100644
--- a/aidesign/UserInteraction/plugins/CanvasInput.py
+++ b/aidesign/UserInteraction/plugins/CanvasInput.py
@@ -1,58 +1,66 @@
-from . import UI
-import tkinter as tk
-import os
 
-from tkinter import messagebox, ttk
+from typing import List
+from aidesign._plugin_templates import UI
 
+import tkinter as tk
+from tkinter import messagebox, ttk
 from tkinter.filedialog import asksaveasfile, askopenfile, askopenfilename
+
+import os
 import numpy as np
 import pandas as pd
 import math
 
-_PLUGIN_READABLE_NAMES = {"canvas":"default","state-action":"alias","robot":"alias"}
+_PLUGIN_READABLE_NAMES = {"canvas": "default",
+                          "state-action": "alias", "robot": "alias"}    # type:ignore
 _PLUGIN_MODULE_OPTIONS = {"layer_priority": 2,
-                            "required_children": None}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {}
-_PLUGIN_REQUIRED_DATA = {"X"}
-class CanvasInput(tk.Frame,UI):
+                          "required_children": None}                    # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                                          # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X"}                                           # type:ignore
+
+
+class CanvasInput(tk.Frame, UI):
     """Method of user interaction for state-action pairs"""
-    def __init__(self, parent, controller, config:dict):
+
+    def __init__(self, parent, controller, config: dict):
         self.parent = parent
-        super().__init__(self.parent, bg = self.parent['bg'])
+        super().__init__(self.parent, bg=self.parent['bg'])
         self.controller = controller
-        
+
         self.controller.title('Canvas Input')
         self.notebook = ttk.Notebook(self)
-        self.notebook.grid(row=0, column=0, columnspan=7, 
-                           rowspan = 12, pady = 15)
-        self.frame = []
-        self.tree = []
-        self.canvas = []
-        self.draw = []
-        self.state = []
-        self.type = []
-        self.clock = []
+        self.notebook.grid(row=0, column=0, columnspan=7,
+                           rowspan=12, pady=15)
+        self.frame: List[tk.Frame] = []
+        self.tree: List[ttk.Treeview] = []
+        self.canvas: List[tk.Canvas] = []
+        self.draw: List[tk.StringVar] = []
+        self.state: List[tk.StringVar] = []
+        self.type: List[str] = []
+        self.clock: List[tk.StringVar] = []
         self.save_path = ''
         self.saved = True
-    
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
+
+    def set_data_in(self, data_in):
+        req_check = [
+            r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
         if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format("CanvasInput") \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
+            raise Exception("Minimal Data Requirements not met"
+                            + "\n\t{0} ".format("CanvasInput")
+                            + "requires data: {0}".format(_PLUGIN_REQUIRED_DATA)
+                            + "\n\tThe following data is missing:"
                             + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
         self._data_in = data_in
         self._load_classes_from_data()
 
-    def configure(self, config:dict):
+    # Causes static type error as it overloads the method in the parent class
+    def configure(self, config: dict):          # type:ignore
         self._config = config
 
     def class_list(self):
         """Getter for required _class_list variable
-        
+
         :return: list of class labels
         :rtype: list of strings
         """
@@ -60,7 +68,7 @@ def class_list(self):
 
     def _load_classes_from_data(self):
         """Setter for required _class_list variable
-        
+
         :param value: labels for state-action pair headers
         :type value: list of strings
         """
@@ -72,18 +80,19 @@ def _load_classes_from_data(self):
             islist.append(isinstance(self._class_list[ii], list))
         if not all(islist):
             self._class_list = [list(self._class_list)]
-        
+
         self.out_data = {}
         self.out_data[0] = {}
         for ii in np.arange(len(self._class_list)):
             self.out_data[ii] = {}
             for k in np.arange(len(self._class_list[ii])):
-                self._class_list[ii][k] = self._class_list[ii][k].replace(' ', '_').lower()
+                self._class_list[ii][k] = self._class_list[ii][k].replace(
+                    ' ', '_').lower()
                 self.out_data[ii][self._class_list[ii][k]] = []
-        
+
         for ii in np.arange(len(self.out_data)):
-            self.frame.append(tk.Frame(self, bg = self.parent['bg']))
-            self.frame[-1].grid(row=0, column=0, sticky="nsew", pady = 15)
+            self.frame.append(tk.Frame(self, bg=self.parent['bg']))
+            self.frame[-1].grid(row=0, column=0, sticky="nsew", pady=15)
             # if self._class_list[ii][0].split('_')[1] == 'a':
             if self._class_list[ii][0].split('_')[1] == 'a':
                 self.type.append('Rotating')
@@ -93,127 +102,125 @@ def _load_classes_from_data(self):
                 self.type.append('Sliding')
                 self.clock.append(-1)
             self.notebook.add(
-                self.frame[-1], 
-                text = 'Object '+ str(ii) + ' - ' + self.type[-1])
-                
-            
+                self.frame[-1],
+                text='Object ' + str(ii) + ' - ' + self.type[-1])
+
             # Create a canvas widget
             self.width, self.height = 600, 600
             self.canvas.append(tk.Canvas(
-                self.frame[-1], width=self.width, 
+                self.frame[-1], width=self.width,
                 height=self.height, background="white"))
-            self.canvas[-1].grid(row=0, column=0, columnspan=4, rowspan = 2)
+            self.canvas[-1].grid(row=0, column=0, columnspan=4, rowspan=2)
             self.canvas[-1].bind('<Button-1>', self.draw_dot)
             self.canvas[-1].bind("<B1-Motion>", self.on_drag)
             # self.canvas[-1].bind('<Motion>', self.motion)
-            
+
             # Booleans to identify what the user wants to do
             self.draw.append(tk.StringVar())
             self.draw[ii].set('draw')
             self.state.append(tk.StringVar())
-            self.state[ii].set('state')    
+            self.state[ii].set('state')
             # Buttons under the canvas
             self.button_draw = tk.Radiobutton(
-                self.frame[-1], text = 'Draw', fg = 'white', bg = self.parent['bg'],
-                height = 3, width = 20, var = self.draw[ii], 
-                selectcolor = 'black', value = 'draw').grid(column = 4, row = 3)
+                self.frame[-1], text='Draw', fg='white', bg=self.parent['bg'],
+                height=3, width=20, var=self.draw[ii],
+                selectcolor='black', value='draw').grid(column=4, row=3)
             self.button_drag = tk.Radiobutton(
-                self.frame[-1], text = 'Move', fg = 'white', bg = self.parent['bg'],
-                height = 3, width = 20, var = self.draw[ii], 
-                selectcolor = 'black', value = 'drag').grid(column = 5, row = 3)
+                self.frame[-1], text='Move', fg='white', bg=self.parent['bg'],
+                height=3, width=20, var=self.draw[ii],
+                selectcolor='black', value='drag').grid(column=5, row=3)
             self.clock_arc = tk.Radiobutton(
-                self.frame[-1], text = 'Clockwise', fg = 'white', bg = self.parent['bg'],
-                height = 3, width = 20, var = self.clock[ii], 
-                selectcolor = 'black', value = 'clock').grid(column = 6, row = 3)            
+                self.frame[-1], text='Clockwise', fg='white', bg=self.parent['bg'],
+                height=3, width=20, var=self.clock[ii],
+                selectcolor='black', value='clock').grid(column=6, row=3)
             self.countclock_arc = tk.Radiobutton(
-                self.frame[-1], text = 'Counterclockwise', fg = 'white', bg = self.parent['bg'],
-                height = 3, width = 20, var = self.clock[ii], 
-                selectcolor = 'black', value = 'countclock').grid(column = 7, row = 3)
+                self.frame[-1], text='Counterclockwise', fg='white', bg=self.parent['bg'],
+                height=3, width=20, var=self.clock[ii],
+                selectcolor='black', value='countclock').grid(column=7, row=3)
             # self.button_edit = tk.Radiobutton(
             #     self.frame[-1], text = 'Edit', fg = 'white', bg = self.parent['bg'],
-            #     height = 3, width = 20, var = self.draw[ii], 
+            #     height = 3, width = 20, var = self.draw[ii],
             #     selectcolor = 'black', value = 'edit').grid(column = 6,row = 3)
             self.button_save = tk.Button(
-                self.frame[-1], text = 'Save', fg = 'white', bg = self.parent['bg'],
-                height = 3, width = 20, 
-                command = self.save_file).grid(column = 1, row = 3)
+                self.frame[-1], text='Save', fg='white', bg=self.parent['bg'],
+                height=3, width=20,
+                command=self.save_file).grid(column=1, row=3)
             self.button_upload = tk.Button(
-                self.frame[-1], text = 'Upload coordinates', fg = 'white', 
-                bg = self.parent['bg'], height = 3, width = 20, 
-                command = self.upload_sa).grid(column = 0, row = 3)
+                self.frame[-1], text='Upload coordinates', fg='white',
+                bg=self.parent['bg'], height=3, width=20,
+                command=self.upload_sa).grid(column=0, row=3)
             self.button_reset = tk.Button(
-                self.frame[-1], text = 'Reset', fg = 'white', 
-                bg = self.parent['bg'], height = 3, width = 20, 
-                command = self.reset).grid(column = 2, row = 3)
+                self.frame[-1], text='Reset', fg='white',
+                bg=self.parent['bg'], height=3, width=20,
+                command=self.reset).grid(column=2, row=3)
             button_main = tk.Button(
-                self.frame[-1], text="Done", fg = 'white', 
-                bg = self.parent['bg'], height = 3, width = 20,
-                command = self.check_quit).grid(column = 3, row = 3)
+                self.frame[-1], text="Done", fg='white',
+                bg=self.parent['bg'], height=3, width=20,
+                command=self.check_quit).grid(column=3, row=3)
             style = ttk.Style()
             style.configure(
-                "Treeview", background = 'white', foreground = 'white', 
-                rowheight = 25, fieldbackground = 'white', 
-                font = self.controller.pages_font)
-            style.configure("Treeview.Heading", 
-                            font = self.controller.pages_font)
-            style.map('Treeview', background = [('selected', 'grey')])
-            
+                "Treeview", background='white', foreground='white',
+                rowheight=25, fieldbackground='white',
+                font=self.controller.pages_font)
+            style.configure("Treeview.Heading",
+                            font=self.controller.pages_font)
+            style.map('Treeview', background=[('selected', 'grey')])
 
             tree_frame = tk.Frame(self.frame[-1])
-            tree_frame.grid(row = 0, column = 4, columnspan = 3, rowspan = 10)
-            
-            tree_scrollx = tk.Scrollbar(tree_frame, orient = 'horizontal')
-            tree_scrollx.pack(side = tk.BOTTOM, fill = tk.X)
+            tree_frame.grid(row=0, column=4, columnspan=3, rowspan=10)
+
+            tree_scrollx = tk.Scrollbar(tree_frame, orient='horizontal')
+            tree_scrollx.pack(side=tk.BOTTOM, fill=tk.X)
             tree_scrolly = tk.Scrollbar(tree_frame)
-            tree_scrolly.pack(side = tk.RIGHT, fill = tk.Y)
-            
+            tree_scrolly.pack(side=tk.RIGHT, fill=tk.Y)
+
             self.tree.append(ttk.Treeview(
-                tree_frame, 
-                yscrollcommand = tree_scrolly.set, 
-                xscrollcommand = tree_scrollx.set))
+                tree_frame,
+                yscrollcommand=tree_scrolly.set,
+                xscrollcommand=tree_scrollx.set))
             self.tree[-1].pack()
-            
-            tree_scrollx.config(command = self.tree[-1].xview)
-            tree_scrolly.config(command = self.tree[-1].yview)
-            
+
+            tree_scrollx.config(command=self.tree[-1].xview)
+            tree_scrolly.config(command=self.tree[-1].yview)
+
             if self.type[-1] == 'Rotating':
                 self.x_ini, self.y_ini = int(self.width/2), int(self.height/2)
                 self.r = int(np.min([self.x_ini, self.y_ini])/1.5)
-                self.create_circle(self.x_ini, self.y_ini, 4, 
-                                    fill="#DDD", outline="", width=4)
-                self.create_circle(self.x_ini, self.y_ini, self.r, 
-                                    fill="", outline="#DDD", width=4)
-                
+                self.create_circle(self.x_ini, self.y_ini, 4,
+                                   fill="#DDD", outline="", width=4)
+                self.create_circle(self.x_ini, self.y_ini, self.r,
+                                   fill="", outline="#DDD", width=4)
+
             self.tree[-1]['columns'] = self._class_list[ii]
-            
+
             # Format columns
-            self.tree[-1].column("#0", width = 50)
+            self.tree[-1].column("#0", width=50)
             for n, cl in enumerate(self._class_list[ii]):
                 self.tree[-1].column(
-                    cl, width = int(
-                        self.controller.pages_font.measure(str(cl)))+20, 
-                    minwidth = 50, anchor = tk.CENTER)
-                    
+                    cl, width=int(
+                        self.controller.pages_font.measure(str(cl)))+20,
+                    minwidth=50, anchor=tk.CENTER)
+
             # Headings
-            self.tree[-1].heading("#0", text = "Sample", anchor = tk.CENTER)
+            self.tree[-1].heading("#0", text="Sample", anchor=tk.CENTER)
             for cl in self._class_list[ii]:
-                self.tree[-1].heading(cl, text = cl, anchor = tk.CENTER)
-            
+                self.tree[-1].heading(cl, text=cl, anchor=tk.CENTER)
+
             # for nn in np.arange(N):
             #     self.tree[-1].insert(
-            #                 parent = '', index = 'end', iid = (nn+1)*1000, text = 'Trial '+str(nn), 
+            #                 parent = '', index = 'end', iid = (nn+1)*1000, text = 'Trial '+str(nn),
             #                 values = tuple())
-            
-            self.tree[-1].tag_configure('odd', foreground = 'black', 
+
+            self.tree[-1].tag_configure('odd', foreground='black',
                                         background='#E8E8E8')
-            self.tree[-1].tag_configure('even', foreground = 'black', 
+            self.tree[-1].tag_configure('even', foreground='black',
                                         background='#DFDFDF')
-        
+
             # Define double-click on row action
             self.tree[-1].bind("<Double-1>", self.OnDoubleClick)
 
     def draw_dot(self, event):
-        
+
         ii = self.notebook.index(self.notebook.select())
         if self.draw[ii].get() == 'drag':
             self.selected = self.canvas[ii].find_overlapping(
@@ -227,96 +234,98 @@ def draw_dot(self, event):
             else:
                 self.canvas[ii].selected = None
 
-        if self.draw[ii].get()  == 'draw':
+        if self.draw[ii].get() == 'draw':
             # Draw an oval in the given coordinates
             self.saved = False
             # Angle of the selected point with respect to the circle center
             if self.type[ii] == 'Rotating':
-                alpha, x_o, y_o = self.angle_calc(event.x, event.y, 
+                alpha, x_o, y_o = self.angle_calc(event.x, event.y,
                                                   circxy=True)
-                if self.state[ii].get()  == 'state': # Write state coordinates
+                if self.state[ii].get() == 'state':  # Write state coordinates
                     self.canvas[ii].create_oval(
-                        x_o-3, y_o-3, x_o+3, y_o+3, fill="black", width=0, 
+                        x_o-3, y_o-3, x_o+3, y_o+3, fill="black", width=0,
                         tags=("state"+str(ii)+'-'
                               + str(len(self.out_data[ii]['state_a']))))
                     self.out_data[ii]['state_a'].append((
-                        -np.rad2deg(alpha)+360)%360)
+                        -np.rad2deg(alpha)+360) % 360)
                     self.state[ii].set('action')
-                    if self.tree[ii].selection(): # Update coordinates in corresponding row if exists.
+                    # Update coordinates in corresponding row if exists.
+                    if self.tree[ii].selection():
                         n = int(self.tree[ii].selection()[0]) + 1
-                        if n%2 == 0:
+                        if n % 2 == 0:
                             tag = ('even',)
                         else:
                             tag = ('odd',)
                         self.tree[ii].insert(
-                            parent = '', index = 'end', iid = n, text = n+1, 
-                            values = tuple(self.dict2mat(
-                                self.out_data[ii])[n,:].astype(int)), 
-                            tags = tag)
+                            parent='', index='end', iid=n, text=n+1,
+                            values=tuple(self.dict2mat(
+                                self.out_data[ii])[n, :].astype(int)),
+                            tags=tag)
                         self.tree[ii].selection_set(str(n))
                     else:
                         self.tree[ii].insert(
-                            parent = '', index = 'end', iid = 0, text = 1, 
-                            values = tuple(self.dict2mat(
-                                self.out_data[ii])[0,:].astype(int)), 
-                                tags = ('even',))
+                            parent='', index='end', iid=0, text=1,
+                            values=tuple(self.dict2mat(
+                                self.out_data[ii])[0, :].astype(int)),
+                            tags=('even',))
                         self.tree[ii].selection_set(str(0))
-                    
-                elif self.state[ii].get()  == 'action':
-                    if self.clock[ii].get()  == 'clock':
+
+                elif self.state[ii].get() == 'action':
+                    if self.clock[ii].get() == 'clock':
                         start = self.out_data[ii]['state_a'][-1]
                         end = np.rad2deg(-alpha)
                     else:
                         start = self.out_data[ii]['state_a'][-1] - 360
                         end = 360 - np.rad2deg(alpha)
                     self.create_circle_arc(
-                        self.x_ini, self.y_ini, self.r, fill = "", 
-                        outline = "red", start = start, end = end, width=2, 
-                        style = tk.ARC, tags = ("action"+str(ii)+'-' + str(len(
-                            self.out_data[ii]['action_a']))))              
+                        self.x_ini, self.y_ini, self.r, fill="",
+                        outline="red", start=start, end=end, width=2,
+                        style=tk.ARC, tags=("action"+str(ii)+'-' + str(len(
+                            self.out_data[ii]['action_a']))))
                     self.out_data[ii]['action_a'].append((
-                        -np.rad2deg(alpha)+360)%360)
+                        -np.rad2deg(alpha)+360) % 360)
                     self.state[ii].set('state')
                     n = int(self.tree[ii].selection()[0])
                     self.tree[ii].item(
-                        self.tree[ii].get_children()[n], text = n+1, 
-                        values = tuple(self.dict2mat(
-                            self.out_data[ii])[n,:].astype(int)))
+                        self.tree[ii].get_children()[n], text=n+1,
+                        values=tuple(self.dict2mat(
+                            self.out_data[ii])[n, :].astype(int)))
             else:
-                if self.state[ii].get()  == 'state': # Write state coordinates
+                if self.state[ii].get() == 'state':  # Write state coordinates
                     self.canvas[ii].create_oval(
-                        event.x-3, event.y-3, event.x+3, event.y+3, 
-                        fill="black", width=0, 
-                        tags=("state"+str(ii)+'-'+ str(len(
+                        event.x-3, event.y-3, event.x+3, event.y+3,
+                        fill="black", width=0,
+                        tags=("state"+str(ii)+'-' + str(len(
                             self.out_data[ii]['state_x']))))
                     self.out_data[ii]['state_x'].append(event.x)
                     self.out_data[ii]['state_y'].append(event.y)
                     self.state[ii].set('action')
-                    if self.tree[ii].selection(): # Update coordinates in corresponding row if exists.
+                    # Update coordinates in corresponding row if exists.
+                    if self.tree[ii].selection():
                         n = int(self.tree[ii].selection()[0]) + 1
-                        if n%2 == 0:
+                        if n % 2 == 0:
                             tag = ('even',)
                         else:
                             tag = ('odd',)
                         self.tree[ii].insert(
-                            parent = '', index = 'end', iid = n, text = n+1, 
-                            values = tuple(self.dict2mat(
-                                self.out_data[ii])[n,:].astype(int)), 
-                            tags = tag)
+                            parent='', index='end', iid=n, text=n+1,
+                            values=tuple(self.dict2mat(
+                                self.out_data[ii])[n, :].astype(int)),
+                            tags=tag)
                         self.tree[ii].selection_set(str(n))
                     else:
                         self.tree[ii].insert(
-                            parent = '', index = 'end', iid = 0, text = 1, 
-                            values = tuple(self.dict2mat(
-                                self.out_data[ii])[0,:].astype(int)), 
-                                tags = ('even',))
+                            parent='', index='end', iid=0, text=1,
+                            values=tuple(self.dict2mat(
+                                self.out_data[ii])[0, :].astype(int)),
+                            tags=('even',))
                         self.tree[ii].selection_set(str(0))
-                    
-                elif self.state[ii].get()  == 'action':
+
+                elif self.state[ii].get() == 'action':
                     self.canvas[ii].create_line(
-                        self.out_data[ii]['state_x'][-1], 
-                        self.out_data[ii]['state_y'][-1], event.x, event.y, 
-                        fill="red", arrow=tk.LAST, 
+                        self.out_data[ii]['state_x'][-1],
+                        self.out_data[ii]['state_y'][-1], event.x, event.y,
+                        fill="red", arrow=tk.LAST,
                         tags=("action"+str(ii)+'-' + str(len(
                             self.out_data[ii]['action_x']))))
                     self.out_data[ii]['action_x'].append(event.x)
@@ -324,34 +333,35 @@ def draw_dot(self, event):
                     self.state[ii].set('state')
                     n = int(self.tree[ii].selection()[0])
                     self.tree[ii].item(
-                        self.tree[ii].get_children()[n], text = n+1, 
-                        values = tuple(self.dict2mat(
-                            self.out_data[ii])[n,:].astype(int)))
-        
+                        self.tree[ii].get_children()[n], text=n+1,
+                        values=tuple(self.dict2mat(
+                            self.out_data[ii])[n, :].astype(int)))
+
     def on_drag(self, event):
-        
+
         ii = self.notebook.index(self.notebook.select())
-        if self.draw[ii].get()  == 'drag' and self.canvas[ii].selected:
+        if self.draw[ii].get() == 'drag' and self.canvas[ii].selected:
             # move the selected item
             n = int(self.canvas[ii].gettags("current")[0].split('-')[1])
-            
+
             if self.type[ii] == 'Rotating':
                 alpha, dx, dy = self.angle_calc(event.x, event.y, circxy=True)
                 _, dx_pr, dy_pr = self.angle_calc(
-                    self.canvas[ii].startxy[0], self.canvas[ii].startxy[1], 
+                    self.canvas[ii].startxy[0], self.canvas[ii].startxy[1],
                     circxy=True)
                 xa = self.r * np.cos(
                     np.deg2rad(self.out_data[ii]['action_a'][n])) + self.x_ini
                 ya = self.r * np.sin(
                     np.deg2rad(self.out_data[ii]['action_a'][n])) + self.y_ini
                 if self.canvas[ii].gettags(
-                        "current")[0].split('-')[0] == 'state'+str(ii): # If state, updates both the state placement and the arrow
+                        "current")[0].split('-')[0] == 'state'+str(ii):  # If state, updates both the state placement and the arrow
                     self.canvas[ii].move(
-                        self.canvas[ii].selected, dx-dx_pr, 
+                        self.canvas[ii].selected, dx-dx_pr,
                         dy-dy_pr)
-                    self.out_data[ii]['state_a'][n] = (360-np.rad2deg(alpha))%360
+                    self.out_data[ii]['state_a'][n] = (
+                        360-np.rad2deg(alpha)) % 360
 
-                    if self.clock[ii].get()  == 'clock':
+                    if self.clock[ii].get() == 'clock':
                         start = self.out_data[ii]['action_a'][n]
                         end = np.rad2deg(-alpha)
                     else:
@@ -359,67 +369,68 @@ def on_drag(self, event):
                         end = 360 - np.rad2deg(alpha)
 
                     self.canvas[ii].itemconfigure(
-                        "action"+str(ii)+'-'+str(n), start = start, 
-                        extent = end - start)
-                    
+                        "action"+str(ii)+'-'+str(n), start=start,
+                        extent=end - start)
+
                 elif self.canvas[ii].gettags(
-                        "current")[0].split('-')[0] == 'action'+str(ii): # If action, updates the arrow end of the line position
-                    
-                    self.out_data[ii]['action_a'][n] = (360-np.rad2deg(alpha))%360
+                        "current")[0].split('-')[0] == 'action'+str(ii):  # If action, updates the arrow end of the line position
+
+                    self.out_data[ii]['action_a'][n] = (
+                        360-np.rad2deg(alpha)) % 360
 
-                    if self.clock[ii].get()  == 'clock':
+                    if self.clock[ii].get() == 'clock':
                         start = self.out_data[ii]['state_a'][n]
                         end = np.rad2deg(-alpha)
                     else:
                         start = self.out_data[ii]['state_a'][n] - 360
                         end = 360 - np.rad2deg(alpha)
-                    
+
                     self.canvas[ii].itemconfigure(
-                        "action"+str(ii)+'-'+str(n), start = start, 
-                        extent = end - start)
+                        "action"+str(ii)+'-'+str(n), start=start,
+                        extent=end - start)
 
             else:
                 # calculate distance moved from last position
                 dx = event.x - self.canvas[ii].startxy[0]
                 dy = event.y - self.canvas[ii].startxy[1]
                 if self.canvas[ii].gettags(
-                        "current")[0].split('-')[0] == 'state'+str(ii): # If state, updates both the state placement and the arrow
+                        "current")[0].split('-')[0] == 'state'+str(ii):  # If state, updates both the state placement and the arrow
                     self.canvas[ii].move(self.canvas[ii].selected, dx, dy)
                     self.canvas[ii].coords(
-                        "action"+str(ii)+'-'+str(n), 
-                        (event.x, event.y, self.out_data[ii]['action_x'][n], 
-                        self.out_data[ii]['action_y'][n]))
+                        "action"+str(ii)+'-'+str(n),
+                        (event.x, event.y, self.out_data[ii]['action_x'][n],
+                         self.out_data[ii]['action_y'][n]))
                     self.out_data[ii]['state_x'][n] = event.x
-                    self.out_data[ii]['state_y'][n] = event.y  
+                    self.out_data[ii]['state_y'][n] = event.y
                 elif self.canvas[ii].gettags(
-                        "current")[0].split('-')[0] == 'action'+str(ii): # If action, updates the arrow end of the line position
+                        "current")[0].split('-')[0] == 'action'+str(ii):  # If action, updates the arrow end of the line position
                     self.canvas[ii].coords(
-                        self.canvas[ii].gettags("current")[0], 
-                        (self.out_data[ii]['state_x'][n], 
+                        self.canvas[ii].gettags("current")[0],
+                        (self.out_data[ii]['state_x'][n],
                          self.out_data[ii]['state_y'][n], event.x, event.y))
                     self.out_data[ii]['action_x'][n] = event.x
                     self.out_data[ii]['action_y'][n] = event.y
             # update last position
-            self.canvas[ii].startxy = (event.x, event.y)                
-            self.tree[ii].item(self.tree[ii].get_children()[n], text = n+1, 
-                           values = tuple(self.dict2mat(
-                               self.out_data[ii])[n,:].astype(int)))
+            self.canvas[ii].startxy = (event.x, event.y)
+            self.tree[ii].item(self.tree[ii].get_children()[n], text=n+1,
+                               values=tuple(self.dict2mat(
+                                   self.out_data[ii])[n, :].astype(int)))
             self.tree[ii].selection_set(str(n))
 
     def checkered(self, line_distance):
-        
-        ii = self.notebook.index(self.notebook.select()) 
+
+        ii = self.notebook.index(self.notebook.select())
         # vertical lines at an interval of "line_distance" pixel
         for x in range(line_distance, self.width, line_distance):
             self.canvas[ii].create_line(x, 0, x, self.height, fill="#476042")
         # horizontal lines at an interval of "line_distance" pixel
         for y in range(line_distance, self.height, line_distance):
-            self.canvas[ii].create_line(0, y, self.width, y, fill="#476042")   
-            
+            self.canvas[ii].create_line(0, y, self.width, y, fill="#476042")
+
     def check_quit(self):
         if not self.saved:
             response = messagebox.askokcancel(
-                "Exit?", 
+                "Exit?",
                 "Do you want to leave the program without saving?")
             if response:
                 self.controller.destroy()
@@ -428,23 +439,24 @@ def check_quit(self):
                 "Exit?",
                 "Are you sure you are finished?")
             self.controller.destroy()
-            
+
     def save_file_as(self):
         self.save_path = asksaveasfile(mode='w')
         self.save_file()
-        
+
     def save_file(self):
-        
+
         if self.save_path == '':
-            self.save_path = asksaveasfile(defaultextension = '.txt', filetypes = 
-                                      [('Text file', '.txt'), 
-                                       ('CSV file', '.csv'), 
-                                       ('All Files', '*.*')])
-        if self.save_path is not None: # asksaveasfile return `None` if dialog closed with "cancel".
-            # Transform input for output file    
+            self.save_path = asksaveasfile(defaultextension='.txt', filetypes=[('Text file', '.txt'),
+                                                                               ('CSV file',
+                                                                                '.csv'),
+                                                                               ('All Files', '*.*')])
+        # asksaveasfile return `None` if dialog closed with "cancel".
+        if self.save_path is not None:
+            # Transform input for output file
             data = {}
             samples = np.max([len(self.out_data[element][
-                list(self.out_data[element].keys())[0]]) 
+                list(self.out_data[element].keys())[0]])
                 for element in self.out_data])
             for ii in self.out_data.keys():
                 for c in self.out_data.keys():
@@ -452,161 +464,162 @@ def save_file(self):
                     aux = self.out_data[c]
                     aux.extend([-1]*(samples - len(self.out_data[c])))
                     data[a+str(ii)+'_'+b] = aux
-                    
-            filedata = pd.DataFrame(data, columns = data.keys()).to_string()
-            self.save_path.seek(0) # Move to the first row to overwrite it
+
+            filedata = pd.DataFrame(data, columns=data.keys()).to_string()
+            self.save_path.seek(0)  # Move to the first row to overwrite it
             self.save_path.write(filedata)
-            self.save_path.flush() # Save without closing
+            self.save_path.flush()  # Save without closing
             # typically the above line would do. however this is used to ensure that the file is written
             os.fsync(self.save_path.fileno())
             self.saved = True
-         
+
     def on_return(self, event):
         ii = self.notebook.index(self.notebook.select())
         val = self.tree[ii].item(self.treerow)['values']
         val = [float(i) for i in val]
         val[int(self.treecol[1:])-1] = float(self.entry.get())
-        self.tree[ii].item(self.treerow, values = val)
-        self.entry.destroy()        
+        self.tree[ii].item(self.treerow, values=val)
+        self.entry.destroy()
         self.saved = False
-        
+
         if self.type[ii] == 'Rotating':
             dx, dy = self.coord_calc(np.deg2rad(360 - val[0]))
-            dx_pr, dy_pr = self.coord_calc(np.deg2rad(360 - self.out_data[ii]['state_a'][self.treerow]))            
+            dx_pr, dy_pr = self.coord_calc(np.deg2rad(
+                360 - self.out_data[ii]['state_a'][self.treerow]))
             self.out_data[ii]['state_a'][self.treerow] = float(val[0])
-            self.canvas[ii].move("state"+str(ii)+'-'+str(self.treerow), dx-dx_pr, 
-                dy-dy_pr)
-            
+            self.canvas[ii].move("state"+str(ii)+'-'+str(self.treerow), dx-dx_pr,
+                                 dy-dy_pr)
+
             self.out_data[ii]['action_a'][self.treerow] = float(val[1])
-            if self.clock[ii].get()  == 'clock':
+            if self.clock[ii].get() == 'clock':
                 start = self.out_data[ii]['state_a'][self.treerow]
                 end = self.out_data[ii]['action_a'][self.treerow]
             else:
                 start = self.out_data[ii]['state_a'][self.treerow] - 360
                 end = self.out_data[ii]['action_a'][self.treerow]
-            
+
             self.canvas[ii].itemconfigure(
-                "action"+str(ii)+'-'+str(self.treerow), start = start, 
-                extent = end - start)
+                "action"+str(ii)+'-'+str(self.treerow), start=start,
+                extent=end - start)
         else:
             # calculate distance moved from last position
             dx_s = val[0] - self.out_data[ii]['state_x'][self.treerow]
             dy_s = val[1] - self.out_data[ii]['state_y'][self.treerow]
 
-            self.canvas[ii].move("state"+str(ii)+'-'+str(self.treerow), dx_s, dy_s)
+            self.canvas[ii].move("state"+str(ii)+'-' +
+                                 str(self.treerow), dx_s, dy_s)
 
             self.out_data[ii]['state_x'][self.treerow] = val[0]
             self.out_data[ii]['state_y'][self.treerow] = val[1]
 
             self.canvas[ii].coords(
-                "action"+str(ii)+'-'+str(self.treerow), 
-                (self.out_data[ii]['state_x'][self.treerow], 
-                  self.out_data[ii]['state_y'][self.treerow], val[2], val[3]))
+                "action"+str(ii)+'-'+str(self.treerow),
+                (self.out_data[ii]['state_x'][self.treerow],
+                 self.out_data[ii]['state_y'][self.treerow], val[2], val[3]))
             self.out_data[ii]['action_x'][self.treerow] = val[2]
             self.out_data[ii]['action_y'][self.treerow] = val[3]
-            
+
     def OnDoubleClick(self, event):
-        
         """ Executed when a row is double clicked.
         Opens an entry box to edit a cell and updates the canvas and the 
         stored data. """
-        
+
         ii = self.notebook.index(self.notebook.select())
         self.treerow = int(self.tree[ii].identify_row(event.y))
         self.treecol = self.tree[ii].identify_column(event.x)
-        
+
         # get column position info
         x, y, width, height = self.tree[ii].bbox(self.treerow, self.treecol)
-    
+
         # y-axis offset
         pady = height // 2
         # pady = 0
-        
+
         if hasattr(self, 'entry'):
             self.entry.destroy()
-            
-        self.entry = tk.Entry(self.tree[ii], justify='center') 
-        
+
+        self.entry = tk.Entry(self.tree[ii], justify='center')
+
         if int(self.treecol[1:]) > 0:
             self.entry.insert(
                 0, self.tree[ii].item(self.treerow)['values'][int(str(self.treecol[1:]))-1])
             # self.entry['selectbackground'] = '#123456'
             self.entry['exportselection'] = False
-    
+
             self.entry.focus_force()
             self.entry.bind("<Return>", self.on_return)
             self.entry.bind("<Escape>", lambda *ignore: self.entry.destroy())
-            
-            self.entry.place(x = x, 
-                             y = y + pady, 
-                             anchor = tk.W, width = width)
+
+            self.entry.place(x=x,
+                             y=y + pady,
+                             anchor=tk.W, width=width)
 
     def upload_sa(self):
-        
+
         ii = self.notebook.index(self.notebook.select())
-        filename = askopenfilename(initialdir = os.getcwd(), 
-                                   title = 'Select a file', 
-                                   defaultextension = '.txt', 
-                                   filetypes = [('Text file', '.txt'), 
-                                                ('CSS file', '.css'), 
-                                                ('All Files', '*.*')])
+        filename = askopenfilename(initialdir=os.getcwd(),
+                                   title='Select a file',
+                                   defaultextension='.txt',
+                                   filetypes=[('Text file', '.txt'),
+                                              ('CSS file', '.css'),
+                                              ('All Files', '*.*')])
         if filename is not None:
-            data = open(filename,'r')
+            data = open(filename, 'r')
             read = False
             self.draw[ii].set('drag')
             for n, point in enumerate(data):
-                if read: # Not elegant at all, just to omit the header.
+                if read:  # Not elegant at all, just to omit the header.
                     i, sx, sy, ax, ay = point.split()
                     # Draw an oval in the given coordinates
                     self.canvas[ii].create_oval(
-                        float(sx)-3, float(sy)-3, float(sx)+3, float(sy)+3, 
-                        fill="black", width=0, 
+                        float(sx)-3, float(sy)-3, float(sx)+3, float(sy)+3,
+                        fill="black", width=0,
                         tags=("state-" + str(len(
                             self.out_data[ii]['state_x']))))
                     self.canvas[ii].create_line(
-                        float(sx), float(sy), float(ax), float(ay), 
-                        fill="red", arrow=tk.LAST, 
-                        tags=("action"+str(ii)+"-"+ str(len(
+                        float(sx), float(sy), float(ax), float(ay),
+                        fill="red", arrow=tk.LAST,
+                        tags=("action"+str(ii)+"-" + str(len(
                             self.out_data[ii]['action_x']))))
                     self.out_data[ii]['state_x'].append(sx)
                     self.out_data[ii]['state_y'].append(sy)
                     self.out_data[ii]['action_x'].append(ax)
                     self.out_data[ii]['action_y'].append(ay)
                     self.tree.insert(
-                        parent = '', index = 'end', 
-                        iid = len(self.out_data[ii]['action_x'])-1, 
-                        text = len(self.out_data[ii]['action_x']), 
-                        values = tuple(self.dict2mat(
+                        parent='', index='end',
+                        iid=len(self.out_data[ii]['action_x'])-1,
+                        text=len(self.out_data[ii]['action_x']),
+                        values=tuple(self.dict2mat(
                             self.out_data[ii])
                             [len(self.out_data[ii]
-                                 ['action_x'])-1,:].astype(int)))
+                                 ['action_x'])-1, :].astype(int)))
                     self.tree.selection_set(str(len(
                         self.out_data[ii]['action_x'])-1))
                 else:
                     read = True
-    
+
     def reset(self):
-        
+
         ii = self.notebook.index(self.notebook.select())
         msg = messagebox.askyesnocancel(
             'Info', 'Are you sure you want to reset the canvas?')
         if msg:
-            self.canvas[ii].delete(tk.ALL) # Reset canvas
+            self.canvas[ii].delete(tk.ALL)  # Reset canvas
             self.state[ii].set('state')
             self.out_data = {}
             for key in self._class_list[ii]:
                 self.out_data[key] = []
-                    
-            for record in self.tree[ii].get_children(): #Reset treeview
+
+            for record in self.tree[ii].get_children():  # Reset treeview
                 self.tree[ii].delete(record)
-            if self.type[ii] == 'Rotating': 
-                self.create_circle(self.x_ini, self.y_ini, 4, 
+            if self.type[ii] == 'Rotating':
+                self.create_circle(self.x_ini, self.y_ini, 4,
                                    fill="#DDD", outline="", width=4)
-                self.create_circle(self.x_ini, self.y_ini, self.r, 
+                self.create_circle(self.x_ini, self.y_ini, self.r,
                                    fill="", outline="#DDD", width=4)
-    
+
     def dict2mat(self, X):
-        
+
         max_size = 0
         for key in X:
             if len(X[key]) > max_size:
@@ -614,11 +627,11 @@ def dict2mat(self, X):
 
         result = -np.ones((max_size, len(X)))
         for k, key in enumerate(X):
-            result[:len(X[key]), k]  = X[key]
-            
+            result[:len(X[key]), k] = X[key]
+
         return result
-    
-    def angle_calc(self, x, y, circxy = False):
+
+    def angle_calc(self, x, y, circxy=False):
         if (x == self.x_ini) & (y < self.y_ini):
             alpha = math.pi/2
         elif (x == self.x_ini) & (y > self.y_ini):
@@ -632,24 +645,23 @@ def angle_calc(self, x, y, circxy = False):
             return alpha, cx, cy
         else:
             return alpha
-        
+
     def coord_calc(self, alpha):
         cx = self.r * np.cos(alpha) + self.x_ini
         cy = self.r * np.sin(alpha) + self.y_ini
         return cx, cy
 
-        
     def create_circle(self, x, y, r, **kwargs):
-        
+
         return self.canvas[self.notebook.index(
             self.notebook.select())].create_oval(x-r, y-r, x+r, y+r, **kwargs)
-    
+
     def create_circle_arc(self, x, y, r, **kwargs):
-        
+
         if "start" in kwargs and "end" in kwargs:
             kwargs["extent"] = kwargs["end"] - kwargs["start"]
             # if self.clock == 'countclock':
             #     kwargs["extent"] += (kwargs["extent"] < 0) * 360
             del kwargs["end"]
         return self.canvas[self.notebook.index(
-            self.notebook.select())].create_arc(x-r, y-r, x+r, y+r, **kwargs)
\ No newline at end of file
+            self.notebook.select())].create_arc(x-r, y-r, x+r, y+r, **kwargs)
diff --git a/aidesign/UserInteraction/plugins/ManualInput.py b/aidesign/UserInteraction/plugins/ManualInput.py
index d8017b79..dae13713 100644
--- a/aidesign/UserInteraction/plugins/ManualInput.py
+++ b/aidesign/UserInteraction/plugins/ManualInput.py
@@ -1,122 +1,126 @@
-from . import UI
-import tkinter as tk
+from aidesign.UserInteraction.User_Interaction_template import UI
+from aidesign._import_helper import get_lib_parent_dir
+from aidesign._types import DictT, DataInterface, GUICoreInterface
+
 import os
-from PIL import Image, ImageTk
-from tkinter import messagebox, ttk
-from tkinter.filedialog import asksaveasfile
 import numpy as np
 import pandas as pd
+from typing import Tuple, List, Union
+from PIL import Image, ImageTk, PngImagePlugin
+
+import tkinter as tk
+from tkinter import messagebox, ttk
+from tkinter.filedialog import asksaveasfile
 
-_PLUGIN_READABLE_NAMES = {"manual":"default","binary":"alias","classification":"alias"}
+_PLUGIN_READABLE_NAMES = {"manual": "default",
+                          "binary": "alias",
+                          "classification": "alias"}            # type:ignore
 _PLUGIN_MODULE_OPTIONS = {"layer_priority": 2,
-                            "required_children": None}
-_PLUGIN_REQUIRED_SETTINGS = {"image_dir":"str"}
-_PLUGIN_OPTIONAL_SETTINGS = {}
-_PLUGIN_REQUIRED_DATA = {"X","Y"}
-class ManualInput(tk.Frame,UI):
+                          "required_children": None}            # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {"image_dir": "str"}                # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                                  # type:ignore
+_PLUGIN_REQUIRED_DATA = {"X", "Y"}                              # type:ignore
+
+
+class ManualInput(tk.Frame, UI):
     """Method of user interaction for binary or classification data"""
-    def __init__(self, parent, controller, config:dict):
-        self.parent = parent    
-        super().__init__(parent, bg = self.parent['bg'])
-        self.controller = controller
+
+    def __init__(self, parent, controller, config: DictT):
+        self.parent = parent
+        super().__init__(parent, bg=self.parent['bg'])
+        self.controller: GUICoreInterface = controller
         self.controller.title('Manual Input')
+
+        self.dirpath = get_lib_parent_dir()
+
         
-        self.dirpath = os.path.dirname(os.path.realpath(__file__))
-        self.assets_path = os.path.join(self.dirpath,'resources','Assets')
+        self.assets_path = os.path.join(self.dirpath, 'utils', 'resources', 'Assets')
 
-        self._class_list = None        
-        pixels = 550
-        path = os.path.join(self.dirpath,'resources','example_radiography_images')
-        self.N = len(os.listdir(path))
+        self._data_in: DataInterface
+        self._class_list = None
+        self._config = config
+        self.save_path = ''
+        self.saved = True
+        
 
-        self.image_list = []
-        for f in os.listdir(path):
+    def _load_images_from_data(self):
+        self.image_list: List[Union[ImageTk.PhotoImage,Image.Image]] = []
+        pixels = 500
+        for f in self._data_in["X"].keys():
             self.image_list.append(
-                self.expand2square(
-                    Image.open(os.path.join(path, f)), 
-                    (0, 0, 0)).resize((pixels, pixels))) # (0, 0, 0) is the padding colour
-
-        # Frames
-        self.frame1 = tk.Frame(self, bg = self.parent['bg'])
-        frame4 = tk.Frame(self, bg = self.parent['bg'])
-        frame5 = tk.Frame(self, bg = self.parent['bg'])
-        frame6 = tk.Frame(self, bg = self.parent['bg'])
-        
+                ImageTk.PhotoImage(
+                    self.expand2square(self._data_in["X"][f], (0, 0, 0))
+                    .resize((pixels, pixels))))
+
+        # Inital window
+        self.my_label = tk.Label(self, image=self.image_list[0],
+                                 bg=self.parent['bg'])
+        self.my_label.grid(column=0, row=0, rowspan=10, columnspan=3)
+        self.N = len(self._data_in["X"])
+
+        # Status bar in the lower part of the window
+        status = tk.Label(self, text='Image 1 of '+str(self.N), bd=1,
+                          relief=tk.SUNKEN, anchor=tk.E, fg='white',
+                          bg=self.parent['bg'])
+        status.grid(row=20, column=0, columnspan=4, pady=10,
+                    sticky=tk.W+tk.E)
+
+        self.frame1 = tk.Frame(self, bg=self.parent['bg'])
+        frame4 = tk.Frame(self, bg=self.parent['bg'])
+        frame5 = tk.Frame(self, bg=self.parent['bg'])
+        frame6 = tk.Frame(self, bg=self.parent['bg'])
+
         # Buttons initialisation
         self.back_img = ImageTk.PhotoImage(Image.open(
-            os.path.join(self.assets_path,'back_arrow.png')).resize((150, 50)))
+            os.path.join(self.assets_path, 'back_arrow.png')).resize((150, 50)))
         self.forw_img = ImageTk.PhotoImage(Image.open(
-            os.path.join(self.assets_path,'forw_arrow.png')).resize((150, 50)))
+            os.path.join(self.assets_path, 'forw_arrow.png')).resize((150, 50)))
         self.button_back = tk.Button(
-            frame4, image = self.back_img, bg = self.parent['bg'], 
-            state = tk.DISABLED)
-        self.button_back.grid(column = 0,row = 19, padx=(10,0), pady=10, sticky="news")
+            frame4, image=self.back_img, bg=self.parent['bg'],
+            state=tk.DISABLED)
+        self.button_back.grid(column=0, row=19, padx=(
+            10, 0), pady=10, sticky="news")
         self.button_save = tk.Button(
-            frame4, text = 'Save', fg = 'white', bg = self.parent['bg'], height = 3, 
-            width = 20, command = self.save_file)
-        self.button_save.grid(column = 1,row = 19, sticky="news", pady=10)
+            frame4, text='Save', fg='white', bg=self.parent['bg'], height=3,
+            width=20, command=self.save_file)
+        self.button_save.grid(column=1, row=19, sticky="news", pady=10)
         self.button_forw = tk.Button(
-            frame4, image = self.forw_img, bg = self.parent['bg'], 
-            command = lambda: self.forward_back(2))
-        self.button_forw.grid(column = 2,row = 19, sticky="news", pady=10)
-        
+            frame4, image=self.forw_img, bg=self.parent['bg'],
+            command=lambda: self.forward_back(2))
+        self.button_forw.grid(column=2, row=19, sticky="news", pady=10)
+
         tk.Button(
-            frame5, text="Done", 
-            fg = 'white', bg = self.parent['bg'], height = 3, width = 20, 
-            command = self.check_quit).grid(column = 4,row = 19, sticky="news", pady=10)
-        self.config = config
-        self.save_path = ''
-        self.saved = True
+            frame5, text="Done",
+            fg='white', bg=self.parent['bg'],
+            height=3, width=20,
+            command=self.check_quit).grid(column=4, row=19, sticky="news", pady=10)
 
-        # Inital image
-        img = ImageTk.PhotoImage(self.image_list[0])
-        self.my_img = tk.Label(self.frame1, image = img, 
-                               bg = self.parent['bg'])
-        self.my_img.image = img
-        self.my_img.pack(fill = tk.BOTH, expand = True, padx=(10,0), pady=(10,0))
+        img = self.image_list[0]
+        self.my_img = tk.Label(self.frame1, image=img,
+                               bg=self.parent['bg'])
+        self.my_img.pack(fill=tk.BOTH, expand=True, padx=(10, 0), pady=(10, 0))
         self.my_img.bind("<Configure>", self.resizing)
 
-        self.frame1.grid(row = 0, column = 0, sticky="nsew")
+        self.frame1.grid(row=0, column=0, sticky="nsew")
         # self.frame3.grid(row = 0, column = 2, sticky="nsew", pady=10, padx=10)
-        frame4.grid(row = 1, column = 0, sticky="sew")
-        frame5.grid(row = 1, column = 1, sticky="sew")
-        frame6.grid(row = 2, column = 0, columnspan =3, sticky="sew")
-        
+        frame4.grid(row=1, column=0, sticky="sew")
+        frame5.grid(row=1, column=1, sticky="sew")
+        frame6.grid(row=2, column=0, columnspan=3, sticky="sew")
+
         frame4.grid_columnconfigure(tuple(range(3)), weight=1)
         self.grid_rowconfigure(0, weight=3)
         self.grid_rowconfigure(1, weight=1)
         self.grid_columnconfigure(tuple(range(3)), weight=1)
         self.grid_columnconfigure(0, weight=2)
-        
-    def _load_images_from_data(self):
-        self.image_list = []
-        pixels = 500
-        for f in self._data_in["X"].keys():
-            self.image_list.append(
-                ImageTk.PhotoImage(
-                    self.expand2square(self._data_in["X"][f], (0, 0, 0))
-                    .resize((pixels, pixels)))) # (0, 0, 0) is the padding colour
 
-        # Inital window
-        self.my_label = tk.Label(self, image = self.image_list[0], 
-                                 bg = self.parent['bg'])
-        self.my_label.grid(column = 0, row = 0, rowspan = 10, columnspan = 3)
-        self.N = len(self._data_in["X"])
-        
-        # Status bar in the lower part of the window
-        status = tk.Label(self, text='Image 1 of '+str(self.N), bd = 1, 
-                          relief = tk.SUNKEN, anchor = tk.E, fg = 'white', 
-                          bg = self.parent['bg'])
-        status.grid(row=20, column=0, columnspan=4, pady = 10, 
-                    sticky = tk.W+tk.E)
-
-    def set_data_in(self,data_in):
-        req_check = [r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
+    def set_data_in(self, data_in):
+        req_check = [
+            r for r in _PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
         if len(req_check) > 0:
-            raise Exception("Minimal Data Requirements not met"   \
-                            +"\n\t{0} ".format("ManualInput") \
-                            +"requires data: {0}".format(_PLUGIN_REQUIRED_DATA)\
-                            + "\n\tThe following data is missing:"\
+            raise Exception("Minimal Data Requirements not met"
+                            + "\n\t{0} ".format("ManualInput")
+                            + "requires data: {0}".format(_PLUGIN_REQUIRED_DATA)
+                            + "\n\tThe following data is missing:"
                             + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req_check])))
         self._data_in = data_in
         self._load_images_from_data()
@@ -124,7 +128,7 @@ def set_data_in(self,data_in):
 
     def class_list(self):
         """Getter for required _class_list variable
-        
+
         :return: list of class labels
         :rtype: list of strings
         """
@@ -132,87 +136,88 @@ def class_list(self):
 
     def _load_classes_from_data(self):
         """Setter for required _class_list variable
-        
+
         :param value: class labels for binary classification
         :type value: list of strings
         """
         self._class_list = list(self._data_in["Y"]["Class"])
         self.out_data = np.zeros((self.N, len(self._class_list)))
-        
-        frame2 = tk.Frame(self, bg = self.parent['bg'])
-        frame2.grid(row = 0, column = 1, sticky="nsew")
-        
+
+        frame2 = tk.Frame(self, bg=self.parent['bg'])
+        frame2.grid(row=0, column=1, sticky="nsew")
+
         self.button_cl = {}
-        
+
         self.var = {}
-        for i,cl in enumerate(self._class_list):
-            self.var[0,i] = tk.IntVar(value=self.out_data[0,i])
+        for i, cl in enumerate(self._class_list):
+            self.var[0, i] = tk.IntVar(value=self.out_data[0, i])
             self.button_cl[cl] = tk.Checkbutton(
-                frame2, text = cl, fg = 'white', bg = self.parent['bg'], 
-                selectcolor = 'black', height = 3, width = 20, 
-                variable = self.var[0,i], 
-                command=(lambda i=i: self.onPress(0,i)))
-            self.button_cl[cl].grid(column = 4,row = i)
-        
-        #Tree defintion. Output display
+                frame2, text=cl, fg='white', bg=self.parent['bg'],
+                selectcolor='black', height=3, width=20,
+                variable=self.var[0, i],
+                command=(lambda i=i: self.onPress(0, i)))
+            self.button_cl[cl].grid(column=4, row=i)
+
+        # Tree defintion. Output display
         style = ttk.Style()
         style.configure(
-            "Treeview", background = 'white', foreground = 'white', 
-            rowheight = 25, fieldbackground = 'white', 
-            font = self.controller.pages_font)
-        style.configure("Treeview.Heading", font = self.controller.pages_font)
-        style.map('Treeview', background = [('selected', 'grey')])
-        
+            "Treeview", background='white', foreground='white',
+            rowheight=25, fieldbackground='white',
+            font=self.controller.pages_font)
+        style.configure("Treeview.Heading", font=self.controller.pages_font)
+        style.map('Treeview', background=[('selected', 'grey')])
+
         tree_frame = tk.Frame(self)
-        tree_frame.grid(row = 0, column = 2, sticky="nsew", pady=10, padx=10)
-        
-        tree_scrollx = tk.Scrollbar(tree_frame, orient = 'horizontal')
-        tree_scrollx.pack(side = tk.BOTTOM, fill = tk.X)
+        tree_frame.grid(row=0, column=2, sticky="nsew", pady=10, padx=10)
+
+        tree_scrollx = tk.Scrollbar(tree_frame, orient='horizontal')
+        tree_scrollx.pack(side=tk.BOTTOM, fill=tk.X)
         tree_scrolly = tk.Scrollbar(tree_frame)
-        tree_scrolly.pack(side = tk.RIGHT, fill = tk.Y)
-        
-        self.tree = ttk.Treeview(tree_frame, 
-                                 yscrollcommand = tree_scrolly.set, 
-                                 xscrollcommand = tree_scrollx.set)
+        tree_scrolly.pack(side=tk.RIGHT, fill=tk.Y)
+
+        self.tree = ttk.Treeview(tree_frame,
+                                 yscrollcommand=tree_scrolly.set,
+                                 xscrollcommand=tree_scrollx.set)
         self.tree.pack(fill='both', expand=True)
-        
-        tree_scrollx.config(command = self.tree.xview)
-        tree_scrolly.config(command = self.tree.yview)
-        
+
+        tree_scrollx.config(command=self.tree.xview)
+        tree_scrolly.config(command=self.tree.yview)
+
         self.tree['columns'] = self._class_list
-        
+
         # Format columns
-        self.tree.column("#0", width = 80, 
-                minwidth = 50)
+        self.tree.column("#0", width=80,
+                         minwidth=50)
         for n, cl in enumerate(self._class_list):
             self.tree.column(
-                cl, width = int(self.controller.pages_font.measure(str(cl)))+20, 
-                minwidth = 50, anchor = tk.CENTER)
+                cl, width=int(self.controller.pages_font.measure(str(cl)))+20,
+                minwidth=50, anchor=tk.CENTER)
         # Headings
-        self.tree.heading("#0", text = "Image", anchor = tk.CENTER)
+        self.tree.heading("#0", text="Image", anchor=tk.CENTER)
         for cl in self._class_list:
-            self.tree.heading(cl, text = cl, anchor = tk.CENTER)
-        self.tree.tag_configure('odd', foreground = 'black', 
+            self.tree.heading(cl, text=cl, anchor=tk.CENTER)
+        self.tree.tag_configure('odd', foreground='black',
                                 background='#E8E8E8')
-        self.tree.tag_configure('even', foreground = 'black', 
+        self.tree.tag_configure('even', foreground='black',
                                 background='#DFDFDF')
         # Add data
         for n, sample in enumerate(self.out_data):
-            if n%2 == 0:
-                self.tree.insert(parent = '', index = 'end', iid = n, text = n+1, 
-                                 values = tuple(sample.astype(int)), tags = ('even',))
+            if n % 2 == 0:
+                self.tree.insert(parent='', index='end', iid=n, text=n+1,
+                                 values=tuple(sample.astype(int)), tags=('even',))
             else:
-                self.tree.insert(parent = '', index = 'end', iid = n, text = n+1, 
-                                 values = tuple(sample.astype(int)), tags = ('odd',))
-        
+                self.tree.insert(parent='', index='end', iid=n, text=n+1,
+                                 values=tuple(sample.astype(int)), tags=('odd',))
+
         # Select the current row
         self.tree.selection_set(str(int(0)))
-        
+
         # Define double-click on row action
         self.tree.bind("<Double-1>", self.OnDoubleClick)
 
-    def expand2square(self, pil_img, background_color):
-        
+    def expand2square(self,
+                        pil_img :PngImagePlugin.PngImageFile,
+                        background_color :Tuple):
         " Adds padding to make the image a square."
         width, height = pil_img.size
         if width == height:
@@ -222,7 +227,7 @@ def expand2square(self, pil_img, background_color):
             result.paste(pil_img, (0, (width - height) // 2))
             return result
         else:
-            result = Image.new(pil_img.mode, (height, height), 
+            result = Image.new(pil_img.mode, (height, height),
                                background_color)
             result.paste(pil_img, ((height - width) // 2, 0))
             return result
@@ -230,13 +235,14 @@ def expand2square(self, pil_img, background_color):
     def resizing(self, event):
         """ Resizes window to tree view height and buttons width """
         n = int(self.tree.selection()[0])
-        iw, ih  = self.image_list[n].width, self.image_list[n].height
+        iw, ih = self.image_list[n].width, self.image_list[n].height
         iw = iw() if type(iw) is not int else iw
         ih = ih() if type(ih) is not int else ih
 
-        mw, mh  = self.frame1.winfo_width()-14, self.frame1.winfo_height()-14 # Frame border correction
+        mw, mh = self.frame1.winfo_width()-14, self.frame1.winfo_height() - \
+            14  # Frame border correction
         if (iw != mw) and (ih != mh):
-            if iw>ih:
+            if iw > ih:
                 ih = ih*(mw/iw)
                 r = mh/ih if (ih/mh) > 1 else 1
                 iw, ih = mw*r, ih*r
@@ -244,18 +250,20 @@ def resizing(self, event):
                 iw = iw*(mh/ih)
                 r = mw/iw if (iw/mw) > 1 else 1
                 iw, ih = iw*r, mh*r
+            
+            if type(self.image_list[n]) == ImageTk.PhotoImage:
+                _temp_img:Image = ImageTk.getimage(self.image_list[n])
+            else:
+                _temp_img:Image = self.image_list[n]
 
-            self.image_list[n] = self.image_list[n].resize(
-                (int(iw), int(ih)))
-        img = ImageTk.PhotoImage(self.image_list[n])
-        self.my_img.config(image=img)
-        self.my_img.image = img
+            self.image_list[n] = ImageTk.PhotoImage(_temp_img.resize((int(iw), int(ih))))
+        self.my_img.config(image=self.image_list[n])
 
     def check_quit(self):
-        
+
         if not self.saved:
             response = messagebox.askokcancel(
-                "Exit?", 
+                "Exit?",
                 "Do you want to leave the program without saving?")
             if response:
                 self.controller.destroy()
@@ -264,83 +272,83 @@ def check_quit(self):
                 "Exit?",
                 "Are you sure you are finished?")
             self.controller.destroy()
-            
+
     def save_file_as(self):
-        
+
         self.save_path = asksaveasfile(mode='w')
         self.save_file()
-        
+
     def save_file(self):
-        
+
         if self.save_path == '':
-            self.save_path = asksaveasfile(defaultextension = '.txt', 
-                                      filetypes = [('Text file', '.txt'), 
-                                                   ('CSV file', '.csv'), 
-                                                   ('All Files', '*.*')])
-        if self.save_path is not None: # asksaveasfile return `None` if dialog closed with "cancel".
+            self.save_path = asksaveasfile(defaultextension='.txt',
+                                           filetypes=[('Text file', '.txt'),
+                                                      ('CSV file', '.csv'),
+                                                      ('All Files', '*.*')])
+        # asksaveasfile return `None` if dialog closed with "cancel".
+        if self.save_path is not None:
             filedata = pd.DataFrame(
-                self.out_data, columns = self._class_list).to_string()
-            self.save_path.seek(0) # Move to the first row to overwrite it
+                self.out_data, columns=self._class_list).to_string()
+            self.save_path.seek(0)  # Move to the first row to overwrite it
             self.save_path.write(filedata)
-            self.save_path.flush() # Save without closing
+            self.save_path.flush()  # Save without closing
             # typically the above line would do. however this is used to ensure that the file is written
             os.fsync(self.save_path.fileno())
             self.saved = True
-            
+
     def forward_back(self, image_number):
-        
         " Forward button to continue to the next image in the folder."
-        
+
         self.tree.selection_set(str(int(image_number-1)))
         # Print the corresponding image
-        self.resizing((0,0))
-        
+        self.resizing((0, 0))
+
         # Update button commands
-        self.button_forw.config(image = self.forw_img, bg = self.parent['bg'], 
-                            command = lambda: self.forward_back(image_number+1),
-                            text = '', state = tk.NORMAL)
-        self.button_back.config(image = self.back_img, bg = self.parent['bg'], 
-                            command = lambda: self.forward_back(image_number-1),
-                            text = '', state = tk.NORMAL)
+        self.button_forw.config(image=self.forw_img, bg=self.parent['bg'],
+                                command=lambda: self.forward_back(
+                                    image_number+1),
+                                text='', state=tk.NORMAL)
+        self.button_back.config(image=self.back_img, bg=self.parent['bg'],
+                                command=lambda: self.forward_back(
+                                    image_number-1),
+                                text='', state=tk.NORMAL)
         if image_number == self.N:
-            self.button_forw.config(state = tk.DISABLED)
+            self.button_forw.config(state=tk.DISABLED)
         if image_number == 1:
-            self.button_back.config(state = tk.DISABLED)
-        
+            self.button_back.config(state=tk.DISABLED)
+
         # Classes buttons
         # var = {}
-        for i,cl in enumerate(self._class_list):
+        for i, cl in enumerate(self._class_list):
             # print(out_data[image_number-1,i])
-            self.var[image_number-1,i] = tk.IntVar(
-                value = int(self.out_data[image_number-1,i]))
+            self.var[image_number-1, i] = tk.IntVar(
+                value=int(self.out_data[image_number-1, i]))
             # var[i] = tk.IntVar(value = int(self.out_data[image_number-1,i]))
-            # I can not make this be selected when going backwards or forward 
+            # I can not make this be selected when going backwards or forward
             # if it was previously selected.
-            self.button_cl[cl].config(text = cl, fg = 'white', bg = self.parent['bg'], 
-                selectcolor = 'black', height = 3, width = 20, 
-                variable = self.var[image_number-1,i], 
-                command=(lambda i=i: self.onPress(image_number-1,i)))
+            self.button_cl[cl].config(text=cl, fg='white', bg=self.parent['bg'],
+                                      selectcolor='black', height=3, width=20,
+                                      variable=self.var[image_number-1, i],
+                                      command=(lambda i=i: self.onPress(image_number-1, i)))
 
-        # # Status bar    
+        # # Status bar
         # status = tk.Label(
-        #     self, text='Image ' + str(image_number) + ' of '+str(self.N), 
-        #     bd = 1, relief = tk.SUNKEN, anchor = tk.E, fg = 'white', 
+        #     self, text='Image ' + str(image_number) + ' of '+str(self.N),
+        #     bd = 1, relief = tk.SUNKEN, anchor = tk.E, fg = 'white',
         #     bg = self.parent['bg'])
-        # status.grid(row=20, column=0, columnspan=4, pady = 10, 
+        # status.grid(row=20, column=0, columnspan=4, pady = 10,
         #             sticky = tk.W+tk.E)
-            
-    def onPress(self, n,i):
-        
+
+    def onPress(self, n, i):
         "Updates the stored values on clicking the checkbutton."
-        
-        self.out_data[n,i] = not self.out_data[n,i]
-        self.tree.item(self.tree.get_children()[n], text = n+1, 
-                       values = tuple(self.out_data[n,:].astype(int)))
+
+        self.out_data[n, i] = not self.out_data[n, i]
+        self.tree.item(self.tree.get_children()[n], text=n+1,
+                       values=tuple(self.out_data[n, :].astype(int)))
         self.saved = False
-        
+
     def OnDoubleClick(self, event):
-        
         "Moves to the image corresponding to the row clicked on the tree."
-        
+
         item = self.tree.selection()[0]
-        self.forward_back(self.tree.item(item,"text"))
\ No newline at end of file
+        self.forward_back(self.tree.item(item, "text"))
diff --git a/aidesign/utils/import_helper.py b/aidesign/_import_helper.py
similarity index 100%
rename from aidesign/utils/import_helper.py
rename to aidesign/_import_helper.py
diff --git a/aidesign/utils/plugin_helpers.py b/aidesign/_plugin_helpers.py
similarity index 63%
rename from aidesign/utils/plugin_helpers.py
rename to aidesign/_plugin_helpers.py
index 4b5f0df6..9217a9bd 100644
--- a/aidesign/utils/plugin_helpers.py
+++ b/aidesign/_plugin_helpers.py
@@ -1,26 +1,32 @@
+from typing import List, Iterator, Union
+from aidesign._types import DictT
+
 import ast
-import os     
+import os
+
+
 class PluginSpecs(ast.NodeVisitor):
     def __init__(self) -> None:
-        self.module_dirs = {}
-        self.available_plugins = {}
-        self.get_plugin_files()
-        self.get_plugin_specs()
+        self.module_dirs: DictT = {}
+        self.available_plugins: DictT = {}
+        self._get_plugin_files()
+        self._get_plugin_specs()
 
-    def get_plugin_files(self, root_dir=None):
+    def _get_plugin_files(self, root_dir: str = None) -> None:
         if root_dir is None:
-            root_dir = os.path.dirname(os.path.realpath(__file__+"/.."))
+            root_dir = os.path.dirname(os.path.realpath(__file__))
 
-        for mod_folder in self.get_clean_module_dirs(root_dir):
-            mod_path = os.path.join(root_dir,mod_folder,"plugins")
-            plugins = self.get_clean_module_dirs(mod_path)
+        for mod_folder in self._get_clean_module_dirs(root_dir):
+            mod_path = os.path.join(root_dir, mod_folder, "plugins")
+            plugins = self._get_clean_module_dirs(mod_path)
 
             self.module_dirs[mod_folder] = {}
             self.module_dirs[mod_folder]["root"] = mod_path
-            self.module_dirs[mod_folder]["files"] = [f for f in plugins if f.endswith(".py")]
+            self.module_dirs[mod_folder]["files"] = [
+                f for f in plugins if f.endswith(".py")]
 
-    def get_clean_module_dirs(self, root_dir:str=None) -> list:
-        excludes = ["__init__.py","__pycache__","resources"]
+    def _get_clean_module_dirs(self, root_dir: str) -> List[str]:
+        excludes = ["__init__.py", "__pycache__", "resources"]
         if os.path.exists(root_dir):
             mod_dirs = os.listdir(root_dir)
             for ex in excludes:
@@ -30,40 +36,39 @@ def get_clean_module_dirs(self, root_dir:str=None) -> list:
         else:
             return []
 
-    def plugins_generator(self):
+    def _plugins_generator(self) -> Iterator:
         plugin_names = self.module_dirs[self.curr_module]["files"]
         for plugin in plugin_names:
             self.curr_plugin = plugin
-            yield os.path.join(self.module_dirs[self.curr_module]["root"],plugin)
+            yield os.path.join(self.module_dirs[self.curr_module]["root"], plugin)
 
-    def modules_generator(self):
+    def _modules_generator(self):
         for self.curr_module in self.module_dirs.keys():
             self.available_plugins[self.curr_module] = {}
-            plugins = self.plugins_generator()
+            plugins = self._plugins_generator()
             for plugin_dir in plugins:
                 yield plugin_dir
 
-    def get_module_package(self):
+    def _get_module_package(self):
         if __package__:
             parent_package = __package__.split(".")[0]
         else:
             parent_package = "aidesign"
-        scipt_filename = self.curr_plugin.replace(".py","")
-        return "{0}.{1}.plugins.{2}".format(parent_package,self.curr_module,scipt_filename)
+        scipt_filename = self.curr_plugin.replace(".py", "")
+        return "{0}.{1}.plugins.{2}".format(parent_package, self.curr_module, scipt_filename)
 
-    def get_plugin_specs(self):
-        self.modules = self.modules_generator()
+    def _get_plugin_specs(self):
+        self.modules = self._modules_generator()
         for plugin_dir in self.modules:
-            self.available_plugins[self.curr_module]\
-                                    [self.curr_plugin] \
-                                    = {"_PLUGIN_DIR":plugin_dir,
-                                       "_PLUGIN_PACKAGE":self.get_module_package()
-                                    }
+            self.available_plugins[self.curr_module][self.curr_plugin] \
+                = {"_PLUGIN_DIR": plugin_dir,
+                   "_PLUGIN_PACKAGE": self._get_module_package()
+                   }
             with open(plugin_dir, "r") as source:
                 tree = ast.parse(source.read())
             self.visit(tree)
 
-    def visit_ClassDef(self,node):
+    def visit_ClassDef(self, node):
         """Append the name of the main class of the plugin to specs
         Called as a callback from "self.visit(tree)"
         Adds only the first class name - works for now, but may be suboptimal
@@ -76,7 +81,6 @@ def visit_ClassDef(self,node):
             self.available_plugins[self.curr_module][self.curr_plugin][c_tag] = node.name
             ds = ast.get_docstring(node)
             self.available_plugins[self.curr_module][self.curr_plugin][d_tag] = ds
-        
 
     def visit_Assign(self, node):
         """Append the name of all plugin specs to available_plugins dict
@@ -88,12 +92,13 @@ def visit_Assign(self, node):
         if type(node.targets[0]) == ast.Name:
             if "_PLUGIN" in node.targets[0].id:
                 key = node.targets[0].id
-                val = eval(compile(ast.Expression(node.value),"<ast expression>","eval"))
+                val = eval(compile(ast.Expression(node.value),
+                           "<ast expression>", "eval"))
                 self.available_plugins[self.curr_module][self.curr_plugin][key] = val
 
-    def get_option_specs(self, option:str) -> dict:
+    def _get_option_specs(self, option: str) -> dict:
         """Gets either required OR optional settings for each plugin.
-        
+
         :param option: str containing either:
                         "_PLUGIN_OPTIONAL_SETTINGS"
                         OR
@@ -101,52 +106,47 @@ def get_option_specs(self, option:str) -> dict:
 
         :returns output: nested dict of options by [Module][Plugin Class Name]
         """
-        output = {}
+        output : DictT= {}
         for module in self.available_plugins.keys():
             output[module] = {}
             for plugin in self.available_plugins[module].keys():
                 if option in self.available_plugins[module][plugin]:
                     cn = self.available_plugins[module][plugin]["_PLUGIN_CLASS_NAME"]
-                    output[module][cn] = self.available_plugins\
-                                                        [module]\
-                                                        [plugin]\
-                                                        [option]
+                    output[module][cn] = self.available_plugins[module][plugin][option]
         return output
 
-    def find_plugin_by_tag_and_value(self, tag:str, name:str) -> dict:
+    def _find_plugin_by_tag_and_value(self, tag: str, name: str) -> Union[DictT,None]:
         for module in self.available_plugins.keys():
             for plugin in self.available_plugins[module].keys():
                 if tag in self.available_plugins[module][plugin]:
                     if name in self.available_plugins[module][plugin][tag]:
-                        return self.available_plugins\
-                                                [module]\
-                                                [plugin]
+                        return self.available_plugins[module][plugin]
         return None
 
     @property
     def names(self):
-        return self.get_option_specs("_PLUGIN_READABLE_NAMES")
+        return self._get_option_specs("_PLUGIN_READABLE_NAMES")
 
     @property
     def class_names(self):
-        return self.get_option_specs("_PLUGIN_CLASS_NAME")
+        return self._get_option_specs("_PLUGIN_CLASS_NAME")
 
     @property
     def module_options(self):
-        return self.get_option_specs("_PLUGIN_MODULE_OPTIONS")
+        return self._get_option_specs("_PLUGIN_MODULE_OPTIONS")
 
     @property
     def required_settings(self):
-        return self.get_option_specs("_PLUGIN_REQUIRED_SETTINGS")
+        return self._get_option_specs("_PLUGIN_REQUIRED_SETTINGS")
 
     @property
     def class_descriptions(self):
-        return self.get_option_specs("_PLUGIN_CLASS_DESCRIPTION")
+        return self._get_option_specs("_PLUGIN_CLASS_DESCRIPTION")
 
     @property
     def optional_settings(self):
-        return self.get_option_specs("_PLUGIN_OPTIONAL_SETTINGS")
-    
+        return self._get_option_specs("_PLUGIN_OPTIONAL_SETTINGS")
+
     @property
     def available_plugin_names(self):
         names = self.names
@@ -159,10 +159,10 @@ def available_plugin_names(self):
         return output
 
     def find_from_class_name(self, value):
-        return self.find_plugin_by_tag_and_value("_PLUGIN_CLASS_NAME",value)
+        return self._find_plugin_by_tag_and_value("_PLUGIN_CLASS_NAME", value)
 
     def find_from_readable_name(self, value):
-        return self.find_plugin_by_tag_and_value("_PLUGIN_READABLE_NAMES",value)
+        return self._find_plugin_by_tag_and_value("_PLUGIN_READABLE_NAMES", value)
 
     def print(self, value):
         from pprint import PrettyPrinter
@@ -175,4 +175,4 @@ def print(self, value):
     ps.print(ps.available_plugin_names)
     # ps.print(ps.names)
     # ps.print(ps.class_descriptions)
-    # print(list(ps.class_descriptions()['GUI'].values()))
\ No newline at end of file
+    # print(list(ps.class_descriptions()['GUI'].values()))
diff --git a/aidesign/_plugin_templates.py b/aidesign/_plugin_templates.py
new file mode 100644
index 00000000..27e51831
--- /dev/null
+++ b/aidesign/_plugin_templates.py
@@ -0,0 +1,209 @@
+from aidesign._types import DataInterface
+from abc import ABC, abstractmethod
+
+import numpy as np
+
+
+class PluginTemplate:
+    def __init__(self, plugin_globals: dict) -> None:
+        """Instantiates data containers and parses plugin-specific options
+
+        param: plugin_globals:dict dictionary representing the global symbol table of the plugin script
+        """
+        self.X = None
+        self.Y = None
+        self.X_tst = None
+        self.Y_tst = None
+
+        self._PLUGIN_READABLE_NAMES: dict
+        self._PLUGIN_MODULE_OPTIONS: dict
+        self._PLUGIN_REQUIRED_SETTINGS: dict
+        self._PLUGIN_OPTIONAL_SETTINGS: dict
+        self._PLUGIN_REQUIRED_DATA: dict
+        self._PLUGIN_OPTIONAL_DATA: dict
+
+        self._parse_plugin_requirements(plugin_globals)
+
+    def configure(self, config: dict) -> None:
+        """Sets and parses plugin configurations options
+        :param config: dict of internal tags set in the XML config file 
+        """
+        self._config = config
+        self._parse_config()
+
+    def set_data_in(self, data_in: DataInterface) -> None:
+        """Sets and parses incoming data
+        :param data_in: saves data as class variable
+                        expected type: aidesign.Data.Data_core.Data
+        """
+        req_check = [
+            r for r in self._PLUGIN_REQUIRED_DATA if r not in data_in.keys()]
+        if len(req_check) > 0:
+            self._data_missing_error(req_check)
+        self._data_in = data_in
+
+    def _parse_plugin_requirements(self, plugin_globals: dict):
+        """Get all plugin options from child plugin.
+        """
+        for key in plugin_globals.keys():
+            if key.startswith("_PLUG"):
+                self.__setattr__(key, plugin_globals[key])
+        self._get_plugin_deafult_name(plugin_globals)
+
+    def _get_plugin_deafult_name(self, plugin_globals: dict):
+        """Parse self._PLUGIN_READABLE_NAMES for default name and set to self.default_name"""
+        self.default_name = None
+        for name in self._PLUGIN_READABLE_NAMES:
+            if self._PLUGIN_READABLE_NAMES[name] == "default":
+                if self.default_name is not None:
+                    print("Multiple deafult names specified in " +
+                          plugin_globals["__file__"])
+                self.default_name = name
+
+    def _data_missing_error(self, req):
+        """If incoming data does not match required data, raise Exception and print"""
+        raise Exception("Minimal Data Requirements not met"
+                        + "\n\t{0} ".format(self.default_name)
+                        + "requires data: {0}".format(self._PLUGIN_REQUIRED_DATA)
+                        + "\n\tThe following data is missing:"
+                        + "\n\t\u2022 {}".format(",\n\t\u2022 ".join([*req])))
+
+    def _parse_config(self):
+        """Parse incoming data and args, sets them as class variables"""
+        self.X = self._data_in["X"]
+        self.Y = np.array(self._get_data_if_exist(self._data_in, "Y")).ravel()
+        self.X_tst = self._get_data_if_exist(self._data_in, "X_test")
+        self.Y_tst = np.array(self._get_data_if_exist(
+            self._data_in, "Y_test")).ravel()
+        self._clean_options()
+
+    def _get_data_if_exist(self, data_dict: dict, key: str, default=None):
+        """Returns data from incoming data if exists, else returns None
+
+        :param data_dict:dict of all incoming data
+        :param key:str: name of data being searched for
+
+        :returns result:dict OR None: dict of data if exists, else None
+        """
+        return data_dict[key] if key in data_dict.keys() and data_dict[key] is not None else default
+
+    def _reshape(self, data, shape):
+        """Reshapes data to shape. Primarily used for arrays with shape (n,)
+
+        :param data: data to reshape
+        :param shape: shape to reshape to
+
+        :returns reshaped data: reshaped data
+        """
+        return data.reshape(shape[0], shape[1])
+
+    def _clean_options(self):
+        """Parses incoming plugin options in self._config["options"] 
+                and modifies DataInterface in-place
+                str options which only contain numeric data are converted to float OR int
+        """
+        for key, val in self._config["options"].items():
+            """ 
+            TODO: Maybe, if list -> cv
+            """
+            if type(val) == str and val.replace('.', '').replace(',', '').isnumeric():
+                val = float(val)
+                if val.is_integer():
+                    val = int(val)
+            self._config["options"][key] = val
+
+    def _test(self, data: DataInterface) -> DataInterface:
+        """Run debug tests on data operations
+        TODO: Investigate if all plugins need a score and predict method
+        """
+        if self._PLUGIN_MODULE_OPTIONS['Type'] == 'classification':
+            print('Training accuracy: %.2f%%' %
+                  (self.score(self.X, self.Y)*100))  # type: ignore
+            if self.Y_tst is not None:
+                print('Test accuracy: %.2f%%' %
+                      (self.score(self.X_tst, self.Y_tst)*100))
+            if self.X_tst is not None:
+                data.append_data_column("Y_pred", self.predict(self.X_tst))
+            return data
+        elif self._PLUGIN_MODULE_OPTIONS['Type'] == 'regression':
+            print('Training R2 score: %.3f' %
+                  (self.score(self.X, self.Y)))  # type: ignore
+            if self.Y_tst is not None:
+                print('Training R2 score: %.3f' %
+                      (self.score(self.X_tst, self.Y_tst)))
+            if self.X_tst is not None:
+                data.append_data_column("Y_pred", self.predict(self.X_tst))
+            return data
+        elif self._PLUGIN_MODULE_OPTIONS['Type'] == 'clustering':
+            print('Clustering completed')
+            if self.X_tst is not None:
+                data.append_data_column("Y_pred", self.predict(self.X_tst))
+            return data
+        else:
+            return data
+
+
+class DataProcessingT(PluginTemplate, ABC):
+    def __init__(self, plugin_globals: dict) -> None:
+        super().__init__(plugin_globals)
+        self._options_to_ignore = ["Data"]
+
+    def configure(self, config: dict):
+        """Extended from PluginTemplate.configure"""
+        super().configure(config)
+        if type(self.X) is None and type(self.Y) is None:
+            print('Invalid Data name. Indicate whether to use `X` or `Y`')
+
+    def _clean_solver_options(self):
+        """XML tags that specify data to be processed should not be sent to solver
+        TODO: in future, reverse the direction of this function - instead pull all options
+                that are directly required by the solver
+        """
+        return {i[0]: i[1]
+                for i in self._config["options"].items()
+                if i[0] not in self._options_to_ignore}
+
+    @abstractmethod
+    def fit(self):
+        pass
+
+    @abstractmethod
+    def transform(self, data: DataInterface) -> DataInterface:
+        pass
+
+
+class ModellingPluginT(PluginTemplate, ABC):
+    def __init__(self, plugin_globals: dict) -> None:
+        super().__init__(plugin_globals)
+
+    @abstractmethod
+    def solve(self):
+        pass
+
+    @abstractmethod
+    def predict(self):
+        pass
+
+    @abstractmethod
+    def score(self):
+        pass
+
+
+class UI(PluginTemplate, ABC):
+    @property
+    def class_list(self):
+        pass
+        # """Set the class list"""
+
+    @class_list.setter  # type: ignore
+    @abstractmethod
+    def class_list(self, value):
+        pass
+
+    @abstractmethod
+    def save_file(self):
+        pass
+
+    @abstractmethod
+    def save_file_as(self):
+        pass
diff --git a/aidesign/_types.py b/aidesign/_types.py
new file mode 100644
index 00000000..5c11fed1
--- /dev/null
+++ b/aidesign/_types.py
@@ -0,0 +1,124 @@
+from typing import Any, Protocol, KeysView, Dict, TypeVar
+from pandas.core.frame import DataFrame
+from tkinter.font import Font
+
+DataInterfaceT = TypeVar("DataInterfaceT", bound="DataInterface")
+DictT = Dict[str, Dict]
+
+
+class PluginOptions(Protocol):
+    _PLUGIN_READABLE_NAMES: dict
+    _PLUGIN_MODULE_OPTIONS: dict
+    _PLUGIN_REQUIRED_SETTINGS: dict
+    _PLUGIN_OPTIONAL_SETTINGS: dict
+    _PLUGIN_REQUIRED_DATA: dict
+    _PLUGIN_OPTIONAL_DATA: dict
+
+
+class DataInterface(Protocol):
+    def __init__(self) -> None:
+        ...
+
+    def import_data(self, config: dict) -> None:
+        ...
+
+    def import_data_from_config(self, config: dict) -> None:
+        ...
+
+    def append_data_column(self, col_name: str, data=None) -> None:
+        ...
+
+    def keys(self) -> KeysView:
+        ...
+
+    def copy(self: DataInterfaceT) -> DataInterfaceT:
+        ...
+
+    def __getitem__(self, key: str) -> DataFrame:
+        ...
+
+
+class PluginInterface(Protocol):
+    def configure(self, config: dict) -> None:
+        ...
+
+    def set_data_in(self, data_in: DataInterface) -> None:
+        ...
+
+
+class DataProcessingPluginInterface(PluginInterface, Protocol):
+    def fit(self):
+        ...
+
+    def transform(self, data: DataInterfaceT) -> DataInterface:
+        ...
+
+
+class PluginSpecsInterface(Protocol):
+    @property
+    def names(self):
+        ...
+
+    @property
+    def class_names(self):
+        ...
+
+    @property
+    def module_options(self):
+        ...
+
+    @property
+    def required_settings(self):
+        ...
+
+    @property
+    def class_descriptions(self):
+        ...
+
+    @property
+    def optional_settings(self):
+        ...
+
+    @property
+    def available_plugin_names(self):
+        ...
+
+    def find_from_class_name(self, value):
+        ...
+
+    def find_from_readable_name(self, value):
+        ...
+
+    def print(self, value):
+        ...
+
+
+class ModuleInterface(Protocol):
+    def set_avail_plugins(self, avail_plugins: PluginSpecsInterface):
+        ...
+
+    def set_data_in(self, data_in: DataInterface):
+        ...
+
+    def set_options(self, module_config: DictT):
+        ...
+
+    def launch(self):
+        ...
+
+    def get_result(self) -> DataInterface:
+        ...
+
+class GUICoreInterface(ModuleInterface,Protocol):
+    title: Any #mypy bug prevents proper typing
+    pages_font: Font
+
+    def destroy(self) -> None:
+        ...
+
+    def set_gui(self) -> None:
+        ...
+
+class InputDataCoreInterface(ModuleInterface,Protocol):
+    def load_data_from_file(self, filename:str, data_id:str) -> None:
+        ...
\ No newline at end of file
diff --git a/aidesign/utils/AID_core.py b/aidesign/utils/AID_core.py
index 542b8376..824724ca 100644
--- a/aidesign/utils/AID_core.py
+++ b/aidesign/utils/AID_core.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 from aidesign.GUI.GUI_core import GUI
 
 class AID(GUI):
diff --git a/aidesign/utils/UserInterfaceClass.py b/aidesign/utils/UserInterfaceClass.py
index aa6ee9b7..adf7260b 100644
--- a/aidesign/utils/UserInterfaceClass.py
+++ b/aidesign/utils/UserInterfaceClass.py
@@ -1,30 +1,8 @@
-from abc import ABC, abstractmethod
-
-"""Defines the parent class for all potential methods of user interface for the framework
-
-Todo: declare what is commonly needed for this module and add below (WIP)
-"""
-
-class UI(ABC):
-    @property
-    def class_list(self):
-        pass
-        # """Set the class list"""
-        
-    @class_list.setter
-    @abstractmethod
-    def class_list(self,value):
-        pass
-    
-
-    @abstractmethod
-    def save_file(self):
-        pass
-
-    @abstractmethod
-    def save_file_as(self):
-        pass
-
-    # @abstractmethod
-    # def on_press(self):
-    #     pass
+from aidesign.UserInteraction.User_Interaction_template import UI
+
+class UserInterfaceClass(UI):
+    """UserInterfaceClass directly duplicates:
+        aidesign.UserInteraction.User_Interaction_template
+    """
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
\ No newline at end of file
diff --git a/aidesign/utils/plugins/MainPage.py b/aidesign/utils/plugins/MainPage.py
index e00f4b4b..1b26a262 100644
--- a/aidesign/utils/plugins/MainPage.py
+++ b/aidesign/utils/plugins/MainPage.py
@@ -7,189 +7,198 @@
 
 from aidesign.Data.xml_handler import XML_handler
 from aidesign.utils.plugins.dataLoader import dataLoader
+from aidesign._import_helper import get_lib_parent_dir
+
+_PLUGIN_READABLE_NAMES = {"main": "default",
+                          "main page": "alias",
+                          "launch page": "alias"}                               # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"layer_priority": 1,
+                          "required_children": ['aidCanvas', 'pluginCanvas']}   # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                                  # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                                                  # type:ignore
+
 
-_PLUGIN_READABLE_NAMES = {"main":"default","main page":"alias","launch page":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"layer_priority": 1,"required_children": ['aidCanvas', 'pluginCanvas']}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {}
 class MainPage(tk.Frame):
     """Splash and Menu page used as GUI entry-point"""
-    def __init__(self, parent, controller, config:dict):
+
+    def __init__(self, parent, controller, config: dict):
         " Here we define the main frame displayed upon opening the program."
         " This leads to the different methods to provide feedback."
-        super().__init__(parent, bg = parent['bg'])
+        super().__init__(parent, bg=parent['bg'])
         self.controller = controller
         self.controller.title('AI Assisted Framework Design')
-        
+
         self.controller.output_type = 'regression'
+        self.out_data = pd.DataFrame()
         self.bg = parent['bg']
-        
-        script_dir = os.path.dirname(__file__)
+
+        self.script_dir = get_lib_parent_dir()
         self.my_img1 = ImageTk.PhotoImage(
-                            Image.open(
-                                os.path.join(
-                                    script_dir,
-                                    'resources',
-                                    'Assets',
-                                    'AIFRED.png')
-                                    ).resize((600, 300))
-                                )
-        
-        self.grid_rowconfigure(tuple(range(3)), weight=1)
+            Image.open(
+                os.path.join(
+                    self.script_dir,
+                    'utils',
+                    'resources',
+                    'Assets',
+                    'AIFRED.png')
+            ).resize((600, 300))
+        )
+
         self.grid_columnconfigure(0, weight=1)
-        
-        frame1 = tk.Frame(self, bg = self.bg)
-        frame2 = tk.Frame(self, bg = self.bg)
-        frame3 = tk.Frame(self, bg = self.bg)
-        
-        self.my_label = tk.Label(frame1, 
-                                    image = self.my_img1,
-                                    bg = parent['bg'])
-
-        self.my_label.grid(column = 0,
-                            row = 0,
-                            rowspan = 10,
-                            columnspan = 4,
-                            pady = 10,
-                            sticky = tk.NE)
-        
-        my_label = tk.Label(frame2, 
-                                text = 
-                                'Indicate the data folder and define the pipeline.',
-                                pady= 10,
-                                font = controller.title_font,
-                                bg = parent['bg'],
-                                fg = 'white')
-        my_label.grid(column = 0,
-                            row = 11,
-                            columnspan = 4)
-        
+
+        frame1 = tk.Frame(self, bg=self.bg)
+        frame2 = tk.Frame(self, bg=self.bg)
+        frame3 = tk.Frame(self, bg=self.bg)
+
+        self.my_label = tk.Label(frame1,
+                                 image=self.my_img1,
+                                 bg=parent['bg'])
+
+        self.my_label.grid(column=0,
+                           row=0,
+                           rowspan=10,
+                           columnspan=4,
+                           pady=10,
+                           sticky=tk.NE)
+
+        my_label = tk.Label(frame2,
+                            text='Indicate the data folder and define the pipeline.',
+                            pady=10,
+                            font=controller.title_font,
+                            bg=parent['bg'],
+                            fg='white')
+        my_label.grid(column=0,
+                      row=11,
+                      columnspan=4)
+
         tk.Button(frame3,
-                    text = 'Data file',
-                    fg = 'white',
-                    font = controller.title_font, 
-                    bg = parent['bg'],
-                    height = 3,
-                    width = 20, 
-                    command = self.upload_data_file,
-                    ).grid(column = 0, row = 12)
+                  text='Data file',
+                  fg='white',
+                  font=controller.title_font,
+                  bg=parent['bg'],
+                  height=3,
+                  width=20,
+                  command=self.upload_data_file,
+                  ).grid(column=0, row=12)
         tk.Button(frame3,
-                    text = 'Data folder',
-                    fg = 'white',
-                    font = controller.title_font, 
-                    bg = parent['bg'],
-                    height = 3,
-                    width = 20, 
-                    command = self.upload_data_folder,
-                    ).grid(column = 1, row = 12)
-        self.controller.Datalabel = tk.Label(frame3, 
-                                text = 'Incomplete',
-                                pady= 10,
-                                padx= 10,
-                                font = controller.title_font,
-                                bg = parent['bg'],
-                                fg = 'white')
-        self.controller.Datalabel.grid(column = 2,
-                            row = 12)
-        
+                  text='Data folder',
+                  fg='white',
+                  font=controller.title_font,
+                  bg=parent['bg'],
+                  height=3,
+                  width=20,
+                  command=self.upload_data_folder,
+                  ).grid(column=1, row=12)
+        self.controller.Datalabel = tk.Label(frame3,
+                                             text='Incomplete',
+                                             pady=10,
+                                             padx=10,
+                                             font=controller.title_font,
+                                             bg=parent['bg'],
+                                             fg='white')
+        self.controller.Datalabel.grid(column=2,
+                                       row=12)
+
         self.interactButton = tk.Button(frame3,
-                    text = 'Interact with canvas',
-                    fg = 'white',
-                    font = controller.title_font,
-                    bg = parent['bg'],
-                    height = 3,
-                    width = 20, 
-                    state = tk.DISABLED, 
-                    command = lambda: self.canvas("aidCanvas")
-                    )
-        self.interactButton.grid(column = 0, row = 13)
+                                        text='Interact with canvas',
+                                        fg='white',
+                                        font=controller.title_font,
+                                        bg=parent['bg'],
+                                        height=3,
+                                        width=20,
+                                        state=tk.DISABLED,
+                                        command=lambda: self.canvas(
+                                            "aidCanvas")
+                                        )
+        self.interactButton.grid(column=0, row=13)
 
         self. uploadButton = tk.Button(frame3,
-                    text = 'Upload XML file',
-                    fg = 'white',
-                    font = controller.title_font, 
-                    bg = parent['bg'],
-                    height = 3,
-                    width = 20, 
-                    state = tk.DISABLED, 
-                    command = self.upload_xml,
-                    )
-        self. uploadButton.grid(column = 1, row = 13)
-        
-        self.controller.XMLlabel = tk.Label(frame3, 
-                                text = 'Incomplete',
-                                pady= 10,
-                                padx= 10,
-                                font = controller.title_font,
-                                bg = parent['bg'],
-                                fg = 'white')
-        self.controller.XMLlabel.grid(column = 2,
-                            row = 13)
+                                       text='Upload XML file',
+                                       fg='white',
+                                       font=controller.title_font,
+                                       bg=parent['bg'],
+                                       height=3,
+                                       width=20,
+                                       state=tk.DISABLED,
+                                       command=self.upload_xml,
+                                       )
+        self. uploadButton.grid(column=1, row=13)
+
+        self.controller.XMLlabel = tk.Label(frame3,
+                                            text='Incomplete',
+                                            pady=10,
+                                            padx=10,
+                                            font=controller.title_font,
+                                            bg=parent['bg'],
+                                            fg='white')
+        self.controller.XMLlabel.grid(column=2,
+                                      row=13)
         self.PluginButton = tk.Button(frame3,
-                    text = 'Modules plugins',
-                    fg = 'white',
-                    font = controller.title_font, 
-                    bg = parent['bg'],
-                    height = 3,
-                    width = 20, 
-                    state = tk.DISABLED,
-                    command = lambda: self.canvas("pluginCanvas"),
-                    )
-        self.PluginButton.grid(column = 0, row = 14)
+                                      text='Modules plugins',
+                                      fg='white',
+                                      font=controller.title_font,
+                                      bg=parent['bg'],
+                                      height=3,
+                                      width=20,
+                                      state=tk.DISABLED,
+                                      command=lambda: self.canvas(
+                                          "pluginCanvas"),
+                                      )
+        self.PluginButton.grid(column=0, row=14)
         self.RunButton = tk.Button(frame3,
-                    text = 'Run Pipeline',
-                    fg = 'white',
-                    font = controller.title_font, 
-                    bg = parent['bg'],
-                    height = 3,
-                    width = 20, 
-                    state = tk.DISABLED,
-                    command = self.controller.destroy,
-                    )
-        self.RunButton.grid(column = 1, row = 14)
-        
+                                   text='Run Pipeline',
+                                   fg='white',
+                                   font=controller.title_font,
+                                   bg=parent['bg'],
+                                   height=3,
+                                   width=20,
+                                   state=tk.DISABLED,
+                                   command=self.controller.destroy,
+                                   )
+        self.RunButton.grid(column=1, row=14)
+
         self.controller.XML = tk.IntVar()
         self.controller.Data = tk.IntVar()
         self.controller.Plugin = tk.IntVar()
         self.controller.XML.set(False)
         self.controller.Data.set(False)
         self.controller.Plugin.set(False)
-        
+
         self.controller.XML.trace('w', self.trace_XML)
         self.controller.Data.trace('w', self.trace_Data)
         self.controller.Plugin.trace('w', self.trace_Plugin)
-        
+
         frame1.grid(column=0, row=0, sticky="n")
         frame2.grid(column=0, row=1, sticky="n")
         frame3.grid(column=0, row=2, sticky="n")
-        
+
     def set_data_in(self, _):
         pass
 
-    def trace_XML(self,*args):
+    def trace_XML(self, *args):
         """ Checks if XML variable has been updated
         """
         if self.controller.XML.get():
-            self.controller.XMLlabel.config(text = 'Done!', fg = 'green')
-            self.PluginButton.config(state = 'normal')
+            self.controller.XMLlabel.config(text='Done!', fg='green')
+            self.PluginButton.config(state='normal')
             if self.controller.Plugin.get():
-                self.RunButton.config(state = 'normal')
+                self.RunButton.config(state='normal')
 
-    def trace_Data(self,*args):
+    def trace_Data(self, *args):
         """ Checks if Data variable has been updated
         """
         if self.controller.Data.get():
-            self.controller.Datalabel.config(text = 'Done!', fg = 'green')
-            self.interactButton.config(state = 'normal')
-            self.uploadButton.config(state = 'normal')
+            self.controller.Datalabel.config(text='Done!', fg='green')
+            self.interactButton.config(state='normal')
+            self.uploadButton.config(state='normal')
 
-    def trace_Plugin(self,*args):
+    def trace_Plugin(self, *args):
         """ Checks if Plugin variable has been updated
         """
         if self.controller.Plugin.get():
-            self.RunButton.config(state = 'normal')
+            self.RunButton.config(state='normal')
 
-    def canvas(self,name: str):
+    def canvas(self, name: str):
         """ Shows the canvas frame.
         :param name: string type of name of desired canvas.
         """
@@ -197,65 +206,65 @@ def canvas(self,name: str):
 
     def upload_xml(self):
         """ Indicates the XML file containng the desired pipeline """
-        filename = askopenfilename(initialdir = os.getcwd(), 
-                                   title = 'Select a file', 
-                                   defaultextension = '.xml', 
-                                   filetypes = [('XML file', '.xml'), 
-                                                ('All Files', '*.*')])
+        filename = askopenfilename(initialdir=os.getcwd(),
+                                   title='Select a file',
+                                   defaultextension='.xml',
+                                   filetypes=[('XML file', '.xml'),
+                                              ('All Files', '*.*')])
         if filename is not None and len(filename) > 0:
-            self.controller._append_to_output("xml_filename",filename)
+            self.controller._append_to_output("xml_filename", filename)
             self.controller.XML.set(True)
 
     def upload_data_file(self):
         """ Opens a window to indicate the path to the data. """
-        
+
         self.newWindow = tk.Toplevel(self.controller)
         # Window options
         self.newWindow.title('Data upload')
-        script_dir = os.path.dirname(__file__)
-        self.tk.call('wm','iconphoto', self.newWindow, ImageTk.PhotoImage(
-            file = os.path.join(os.path.join(
-                script_dir, 
-                'resources', 
-                'Assets', 
+        self.tk.call('wm', 'iconphoto', self.newWindow, ImageTk.PhotoImage(
+            file=os.path.join(os.path.join(
+                self.script_dir,
+                'utils',
+                'resources',
+                'Assets',
                 'AIDIcon.ico'))))
         # self.newWindow.geometry("600x200")
-        
+
         frame1 = tk.Frame(self.newWindow)
         self.label_list = []
         self.var = ['X', 'Y', 'X_test', 'Y_test']
         self.filenames = ['']*len(self.var)
-        for r,v in enumerate(self.var):
+        for r, v in enumerate(self.var):
             v = v + '*' if r == 0 else v
-            tk.Label(frame1, text = v,
-                                    pady= 10,
-                                    padx= 10,
-                                    fg = 'black'
-                                    ).grid(column = 0, row = r)
-            tk.Button(frame1, 
-                      text="Browse", 
-                      command = lambda a = r: self.upload_file(a)
-                      ).grid(column = 1, row = r)
-            tk.Button(frame1, 
-                      text="Delete", 
-                      command = lambda a = r: self.delete_file(a)
-                      ).grid(column = 2, row = r)
-            self.label_list.append(tk.Label(frame1, text = ' '*120,
-                                    pady= 10,
-                                    padx= 10,
-                                    fg = 'black'
-                                    ))
-            self.label_list[-1].grid(column = 3, row = r)
+            tk.Label(frame1, text=v,
+                     pady=10,
+                     padx=10,
+                     fg='black'
+                     ).grid(column=0, row=r)
+            tk.Button(frame1,
+                      text="Browse",
+                      command=lambda a=r: self.upload_file(a)
+                      ).grid(column=1, row=r)
+            tk.Button(frame1,
+                      text="Delete",
+                      command=lambda a=r: self.delete_file(a)
+                      ).grid(column=2, row=r)
+            self.label_list.append(tk.Label(frame1, text=' '*120,
+                                            pady=10,
+                                            padx=10,
+                                            fg='black'
+                                            ))
+            self.label_list[-1].grid(column=3, row=r)
         frame2 = tk.Frame(self.newWindow)
-        tk.Label(frame2, text = '* This data file is mandatory.',
-                                pady= 10,
-                                padx= 10,
-                                fg = 'black'
-                                ).pack(side = tk.LEFT)
-        tk.Button(frame2, 
-                      text="Done", 
-                      command = self.start_dataloader
-                      ).pack(side = tk.RIGHT, padx=(0,5))
+        tk.Label(frame2, text='* This data file is mandatory.',
+                 pady=10,
+                 padx=10,
+                 fg='black'
+                 ).pack(side=tk.LEFT)
+        tk.Button(frame2,
+                  text="Done",
+                  command=self.start_dataloader
+                  ).pack(side=tk.RIGHT, padx=(0, 5))
         frame1.grid(column=0, row=0, sticky="nsew")
         frame2.grid(column=0, row=1, sticky="nsew")
 
@@ -263,88 +272,96 @@ def upload_file(self, r):
         """ Asks for a file and stores the path and displays it.
         :param r: int type of data variable number.
         """
-        filename = askopenfilename(initialdir = os.getcwd(), 
-                                   title = 'Select a file', 
-                                   defaultextension = '.csv', 
-                                   filetypes = [('CSV', '.csv'), 
-                                                ('All Files', '*.*')])
+        filename = askopenfilename(initialdir=os.getcwd(),
+                                   title='Select a file',
+                                   defaultextension='.csv',
+                                   filetypes=[('CSV', '.csv'),
+                                              ('All Files', '*.*')])
         self.filenames[r] = filename
         width = 63
-        filename =  '...' + filename[-width+3:] if filename and len(filename) > width else filename
-        self.label_list[r].config(text = filename)
+        filename = '...' + \
+            filename[-width +
+                     3:] if filename and len(filename) > width else filename
+        self.label_list[r].config(text=filename)
 
     def delete_file(self, r):
         """ Deletes the specified file from storage and display.
         :param r: int type of data variable number.
         """
         self.filenames[r] = ''
-        self.label_list[r].config(text = '')
+        self.label_list[r].config(text='')
 
     def start_dataloader(self):
         """ Reads all the selected files, loads the data and passes it to 
         dataLoader.
         """
-        
+
         # s = XML_handler()
         # s.new_config_file(self.save_path.name)
         # s.filename = self.save_path.name
-        
+
         # self.s = XML_handler()
         # self.s.new_config_file()
-        
+
         # self.s._print_xml_config()
         # self.s.load_XML(self.controller.output["xml_filename"])
-        
+
         data = {}
         isVar = [0] * len(self.var)
         if len(self.label_list[0].cget("text")) > 0:
-            for i,filename in enumerate(self.filenames):
+            for i, filename in enumerate(self.filenames):
                 variable = self.var[i]
                 if filename is not None and len(filename) > 0:
                     if filename.lower().endswith(('.csv')):
-                        data[variable] = pd.read_csv(filename) #Infers by default, should it be None?
+                        # Infers by default, should it be None?
+                        data[variable] = pd.read_csv(filename)
                         isVar[i] = 1
-                        self.controller.s.append_input_data(variable,filename)
+                        self.controller.s.append_input_data(variable, filename)
                         if i == 0:
                             self.controller.Data.set(True)
                         if any(isVar[1::2]) and (
                                 len(pd.unique(data[variable].to_numpy().flatten())) < len(data[variable].to_numpy().flatten())*0.2):
-                            self.controller.output_type = 'classification'*all([float(i).is_integer() for i in data[variable].to_numpy().flatten()])
+                            self.controller.output_type = 'classification' * \
+                                all([float(i).is_integer()
+                                    for i in data[variable].to_numpy().flatten()])
             if not any(isVar[1::2]):
                 self.controller.output_type = 'unsupervised'
             self.newWindow.destroy()
             dataLoader(self.controller, data)
         else:
-            tk.messagebox.showwarning(title = 'Error - X not specified',
-                                      message = 'You need to specify X before proceeding.')
+            tk.messagebox.showwarning(title='Error - X not specified',
+                                      message='You need to specify X before proceeding.')
 
     def upload_data_folder(self):
         """ Stores the directory containing the data that will be later loaded 
         """
-        folder = askdirectory(initialdir = os.getcwd(),
-                                    title = 'Select a folder',
-                                    mustexist = True)
+        folder = askdirectory(initialdir=os.getcwd(),
+                              title='Select a folder',
+                              mustexist=True)
         if folder is not None and len(folder) > 0:
-            onlyfiles = [f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))]
+            onlyfiles = [f for f in os.listdir(
+                folder) if os.path.isfile(os.path.join(folder, f))]
             self.upload_data_file()
             for file in onlyfiles:
                 name = file.lower()
                 filename = os.path.join(folder, file)
                 width = 63
-                filenm =  '...' + filename[-width+3:] if filename and len(filename) > width else filename
+                filenm = '...' + \
+                    filename[-width +
+                             3:] if filename and len(filename) > width else filename
                 if name.endswith(('.csv')):
                     name = ''.join(ch for ch in name if ch.isalnum())
                     if 'test' in name or 'tst' in name:
                         if name[0] == 'x':
                             self.filenames[2] = filename
-                            self.label_list[2].config(text = filenm)
+                            self.label_list[2].config(text=filenm)
                         elif name[0] == 'y':
                             self.filenames[3] = filename
-                            self.label_list[3].config(text = filenm)
+                            self.label_list[3].config(text=filenm)
                     else:
                         if name[0] == 'x':
                             self.filenames[0] = filename
-                            self.label_list[0].config(text = filenm)
+                            self.label_list[0].config(text=filenm)
                         elif name[0] == 'y':
                             self.filenames[1] = filename
-                            self.label_list[1].config(text = filenm)
+                            self.label_list[1].config(text=filenm)
diff --git a/aidesign/utils/plugins/aidCanvas.py b/aidesign/utils/plugins/aidCanvas.py
index f49607b7..93cce772 100644
--- a/aidesign/utils/plugins/aidCanvas.py
+++ b/aidesign/utils/plugins/aidCanvas.py
@@ -1,161 +1,167 @@
-# Import the required libraries
-import tkinter as tk
-from PIL import Image, ImageTk
+from aidesign.Data.xml_handler import XML_handler
+from aidesign._types import DictT
+
 import os
 import numpy as np
 import pandas as pd
-from tkinter.filedialog import asksaveasfile, askopenfile, askopenfilename
+from typing import List, Literal,Tuple
+
+from PIL import Image, ImageTk
+import tkinter as tk
 from tkinter import messagebox
+from tkinter.filedialog import asksaveasfile, askopenfile, askopenfilename
+from aidesign._import_helper import get_lib_parent_dir
 
-from aidesign.Data.xml_handler import XML_handler
 
-_PLUGIN_READABLE_NAMES = {"aid_canvas":"default","aidCanvas":"alias","aid":"alias","AID":"alias"}
+_PLUGIN_READABLE_NAMES = {"aid_canvas": "default",
+                          "aidCanvas": "alias",
+                          "aid": "alias",
+                          "AID": "alias"}               # type:ignore
 _PLUGIN_MODULE_OPTIONS = {"layer_priority": 2,
-                            "required_children": None,}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {}
+                          "required_children": None}    # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                          # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                          # type:ignore
+
+
 class aidCanvas(tk.Frame):
     """Canvas for graphical specification of pipeline modules"""
-    
-    def __init__(self, parent, controller, config:dict):
-        
+
+    def __init__(self, parent, controller, config: dict):
         " Here we define the main frame displayed upon opening the program."
         " This leads to the different methods to provide feedback."
-        
-        super().__init__(parent, bg = parent['bg'])
+
+        super().__init__(parent, bg=parent['bg'])
         self.bg = parent['bg']
         self.controller = controller
-         
-        script_dir = os.path.dirname(__file__)
-        self.tk.call('wm','iconphoto', self.controller._w, ImageTk.PhotoImage(
-            file = os.path.join(os.path.join(
-                script_dir, 
-                'resources', 
-                'Assets', 
+
+        script_dir = get_lib_parent_dir()
+        self.tk.call('wm', 'iconphoto', self.controller._w, ImageTk.PhotoImage(
+            file=os.path.join(os.path.join(
+                script_dir,
+                'utils',
+                'resources',
+                'Assets',
                 'AIDIcon.ico'))))
         self.my_img1 = ImageTk.PhotoImage(Image.open(os.path.join(
-                script_dir, 
-                'resources', 
-                'Assets', 
-                'AIDIcon_name.png')).resize((250, 200)))
-        
-        self.grid_rowconfigure(tuple(range(2)), weight=1)
+            script_dir,
+            'utils',
+            'resources',
+            'Assets',
+            'AIDIcon_name.png')).resize((250, 200)))
+
+        # self.grid_rowconfigure(tuple(range(2)), weight=1)
+        self.grid_rowconfigure(1, weight=1)
         self.grid_columnconfigure(0, weight=1)
-        
-        frame1 = tk.Frame(self, bg = self.bg)
-        frame2 = tk.Frame(self, bg = self.bg)
-        frame3 = tk.Frame(self, bg = self.bg)
-        frame4 = tk.Frame(self, bg = self.bg)
-        
-        self.my_label = tk.Label(frame2, image = self.my_img1, bg = parent['bg'])
-        self.my_label.grid(column = 5, row = 0)
-        
+
+        frame1 = tk.Frame(self, bg=self.bg)
+        frame2 = tk.Frame(self, bg=self.bg)
+        frame3 = tk.Frame(self, bg=self.bg)
+        frame4 = tk.Frame(self, bg=self.bg)
+
+        self.my_label = tk.Label(frame2, image=self.my_img1, bg=parent['bg'])
+        self.my_label.grid(column=5, row=0)
+
         # Create canvas
         self.width, self.height = 600, 600
-        self.canvas = tk.Canvas(frame1, width=self.width, 
-            height=self.height, background="white")
-        # self.canvas = ResizingCanvas(frame1, width=self.width, 
+        self.canvas = tk.Canvas(frame1, width=self.width,
+                                height=self.height, background="white")
+        # self.canvas = ResizingCanvas(frame1, width=self.width,
         #     height=self.height, background="white")
-        self.canvas.pack(fill = tk.BOTH, expand = True, padx=(10,0), pady=10)
-        
-        self.canvas.startxy = []
+        self.canvas.pack(fill=tk.BOTH, expand=True, padx=(10, 0), pady=10)
+
+        self.canvas_startxy: List[Tuple] = []
         self.out_data = pd.DataFrame()
-        self.connections = {}
+        self.connections: DictT = {}
         self.modules = 0
-        self.module_list = []
-        self.module_names = []
-        
+        self.module_list: List[str] = []
+        self.module_names: List[str] = []
+
         # Create module
         self.w, self.h = 100, 50
         self.cr = 4
-        
-        #Initialiser module
-        self.add_module('Initialiser', self.width/2, self.h, ini = True)
-        self.add_module('Output', self.width/2, self.height - self.h, out = True)
-        
+
+        # Initialiser module
+        self.add_module('Initialiser', self.width/2, self.h, ini=True)
+        self.add_module('Output', self.width/2, self.height - self.h, out=True)
+
         self.draw = False
         self.canvas.bind("<B1-Motion>", self.on_drag)
         self.canvas.bind('<Button-1>', self.select)
-        self.l = 0 #number of loops
+        self.l = 0  # number of loops
         self.drawLoop = False
-        self.loops = []
-        
+        self.loops: List[DictT] = []
+
         # self.canvas.bind("<ButtonPress-1>", self.on_button_press)
         self.canvas.bind("<ButtonRelease-1>", self.on_button_release)
-        
-        
+
         # for m, module in enumerate(modules):
         tk.Button(
-            frame4, text = 'Data processing', fg = 'white', bg = parent['bg'],
-            height = 3, width = 25, font = self.controller.pages_font,
-            command = lambda: self.add_module('Data Processing', 
-                                              self.width/2, 
-                                              self.height/2)
-            ).grid(column = 5, row = 1, padx=(0,10), sticky="news")
+            frame4, text='Data processing', fg='white', bg=parent['bg'],
+            height=3, width=25, font=self.controller.pages_font,
+            command=lambda: self.add_module('Data Processing',
+                                            self.width/2,
+                                            self.height/2)
+        ).grid(column=5, row=1, padx=(0, 10), sticky="news")
         tk.Button(
-            frame4, text = 'Modelling', fg = 'white', bg = parent['bg'],
-            height = 3, width = 25, font = self.controller.pages_font,
-            command = lambda: self.add_module('Modelling', 
-                                              self.width/2, 
-                                              self.height/2)
-            ).grid(column = 5, row = 2, padx=(0,10), sticky="news")
+            frame4, text='Modelling', fg='white', bg=parent['bg'],
+            height=3, width=25, font=self.controller.pages_font,
+            command=lambda: self.add_module('Modelling',
+                                            self.width/2,
+                                            self.height/2)
+        ).grid(column=5, row=2, padx=(0, 10), sticky="news")
         tk.Button(
-            frame4, text = 'Decision making', fg = 'white', bg = parent['bg'],
-            height = 3, width = 25, font = self.controller.pages_font,
-            command = lambda: self.add_module('Decision Making', 
-                                              self.width/2, 
-                                              self.height/2)
-            ).grid(column = 5, row = 3, padx=(0,10), sticky="news")
+            frame4, text='Decision making', fg='white', bg=parent['bg'],
+            height=3, width=25, font=self.controller.pages_font,
+            command=lambda: self.add_module('Decision Making',
+                                            self.width/2,
+                                            self.height/2)
+        ).grid(column=5, row=3, padx=(0, 10), sticky="news")
         tk.Button(
-            frame4, text = 'User Interaction', fg = 'white', bg = parent['bg'],
-            height = 3, width = 25, font = self.controller.pages_font,
-            command = lambda: self.add_module('User Interaction', 
-                                              self.width/2, 
-                                              self.height/2)
-            ).grid(column = 5, row = 4, padx=(0,10), sticky="news")
+            frame4, text='User Interaction', fg='white', bg=parent['bg'],
+            height=3, width=25, font=self.controller.pages_font,
+            command=lambda: self.add_module('User Interaction',
+                                            self.width/2,
+                                            self.height/2)
+        ).grid(column=5, row=4, padx=(0, 10), sticky="news")
         tk.Button(
-            frame4, text = 'Input data', fg = 'white', bg = parent['bg'],
-            height = 3, width = 25, font = self.controller.pages_font,
-            command = lambda: self.add_module('Input Data', 
-                                              self.width/2, 
-                                              self.height/2)
-            ).grid(column = 5, row = 5, padx=(0,10), sticky="news")
+            frame4, text='Input data', fg='white', bg=parent['bg'],
+            height=3, width=25, font=self.controller.pages_font,
+            command=lambda: self.add_module('Input Data',
+                                            self.width/2,
+                                            self.height/2)
+        ).grid(column=5, row=5, padx=(0, 10), sticky="news")
         tk.Button(
-            frame4, text = 'Delete selection', fg = 'white', bg = parent['bg'],
-            height = 3, width = 25, font = self.controller.pages_font,
-            command = self.delete_sel).grid(column = 5, row = 6, sticky="news"
-                                            , padx=(0,10), pady=(0,10))
-        
+            frame4, text='Delete selection', fg='white', bg=parent['bg'],
+            height=3, width=25, font=self.controller.pages_font,
+            command=self.delete_sel).grid(column=5, row=6, sticky="news", padx=(0, 10), pady=(0, 10))
+
         tk.Button(
-            frame3, text = 'Upload', fg = 'white', bg = parent['bg'], 
-            height = 3, width = 15, font = self.controller.pages_font, 
-            command = self.upload).grid(column = 0, row = 10, sticky="news", 
-                                        padx=(10,0), pady=(0,10))
+            frame3, text='Upload', fg='white', bg=parent['bg'],
+            height=3, width=15, font=self.controller.pages_font,
+            command=self.upload).grid(column=0, row=10, sticky="news",
+                                      padx=(10, 0), pady=(0, 10))
         tk.Button(
-            frame3, text = 'Save', fg = 'white', bg = parent['bg'],
-            height = 3, width = 15, font = self.controller.pages_font, 
-            command = self.save_file).grid(column = 1, row = 10, sticky="news", pady=(0,10))
+            frame3, text='Save', fg='white', bg=parent['bg'],
+            height=3, width=15, font=self.controller.pages_font,
+            command=self.save_file).grid(column=1, row=10, sticky="news", pady=(0, 10))
         tk.Button(
-            frame3, text = 'Reset', fg = 'white', bg = parent['bg'], 
-            height = 3, width = 15, font = self.controller.pages_font, 
-            command = self.reset).grid(column = 2, row = 10, sticky="news", pady=(0,10))
+            frame3, text='Reset', fg='white', bg=parent['bg'],
+            height=3, width=15, font=self.controller.pages_font,
+            command=self.reset).grid(column=2, row=10, sticky="news", pady=(0, 10))
         tk.Button(
-            frame3, text = 'Done', fg = 'white', bg = parent['bg'], 
-            height = 3, width = 15, font = self.controller.pages_font, 
-            command = self.check_quit).grid(column = 3, row = 10, sticky="news", pady=(0,10))
-        
+            frame3, text='Done', fg='white', bg=parent['bg'],
+            height=3, width=15, font=self.controller.pages_font,
+            command=self.check_quit).grid(column=3, row=10, sticky="news", pady=(0, 10))
+
         self.save_path = ''
         self.saved = True
 
-        frame1.grid(column=0, row=0, rowspan = 2, sticky="nsew")
+        frame1.grid(column=0, row=0, rowspan=2, sticky="nsew")
         frame2.grid(column=1, row=0, sticky="ne")
         frame3.grid(column=0, row=2, sticky="swe")
         frame4.grid(column=1, row=1, sticky="nse")
-        
-        frame3.grid_columnconfigure(tuple(range(4)), weight=1)
-        frame4.grid_rowconfigure(tuple(range(6)), weight=1)
 
-    def class_list(self,value):
+    def class_list(self, value):
         """ Temporary fix """
         return value
 
@@ -178,16 +184,16 @@ def on_return_display(self, event):
             x = self.curX - text_w/2
         self.loops[-1][key] = condition
         self.canvas.create_text(
-            x, 
-            self.start_y + 20, 
-            font = self.controller.pages_font, 
-            text = condition, 
-            tags = ('loop-'+str(self.l-1), key+'-'+str(self.l-1)), 
-            justify = tk.CENTER)
-        self.canvas.tag_bind(key+'-'+str(self.l-1), 
-                         "<Double-1>", self.OnDoubleClick)
+            x,
+            self.start_y + 20,
+            font=self.controller.pages_font,
+            text=condition,
+            tags=('loop-'+str(self.l-1), key+'-'+str(self.l-1)),
+            justify=tk.CENTER)
+        self.canvas.tag_bind(key+'-'+str(self.l-1),
+                             "<Double-1>", self.OnDoubleClick)
         self.loopDisp = not self.loopDisp
-    
+
     def on_button_release(self, event):
         """ Finishes drawing the rectangle for loop definition. 
             Once the rectangle is drawn, it stores the information of the 
@@ -195,63 +201,65 @@ def on_button_release(self, event):
         """
         if self.drawLoop:
             self.drawLoop = False
-            
+
             if (abs(self.start_x - self.curX) > 10) and (
                     abs(self.start_y - self.curY) > 10):
                 self.l += 1
                 # Identify modules in area
-                loopMod = self.canvas.find_enclosed(self.start_x, self.start_y, 
+                loopMod = self.canvas.find_enclosed(self.start_x, self.start_y,
                                                     self.curX, self.curY)
                 self.loops.append({'type': None,
                                    'condition': None,
-                                   'mod': [], 
-                                   'coord': (self.start_x, self.start_y, 
+                                   'mod': [],
+                                   'coord': (self.start_x, self.start_y,
                                              self.curX, self.curY)})
                 for mod in loopMod:
                     aux = self.canvas.itemcget(mod, 'tags').split(' ')[1]
                     if len(aux) == 2 and aux[0] == 't':
                         self.loops[-1]['mod'].append(self.canvas.itemcget(
                             mod, 'text'))
-                
-                # Ask for loop definition and condition and display               
-                self.entry1 = tk.Entry(self.canvas, justify='center', 
-                                      font = self.controller.pages_font)
+
+                # Ask for loop definition and condition and display
+                self.entry1 = tk.Entry(self.canvas, justify='center',
+                                       font=self.controller.pages_font)
                 text = 'For/While'
                 self.loopDisp = True
                 self.entry1.insert(0, text)
                 # self.entry['selectbackground'] = '#d0d4d9'
                 self.entry1['exportselection'] = False
-        
+
                 self.entry1.focus()
                 self.entry1.bind("<Return>", self.on_return_display)
-                self.entry1.bind("<Escape>", lambda *ignore: self.entry1.destroy())
-                
+                self.entry1.bind(
+                    "<Escape>", lambda *ignore: self.entry1.destroy())
+
                 self.entry1.place(
-                    x = self.start_x + 10,
-                    y = self.start_y + 20, 
-                    anchor = tk.W,
-                    width = self.controller.pages_font.measure(text)+10)
+                    x=self.start_x + 10,
+                    y=self.start_y + 20,
+                    anchor=tk.W,
+                    width=self.controller.pages_font.measure(text)+10)
                 text = 'Condition'
-                self.entry2 = tk.Entry(self.canvas, justify='center', 
-                                      font = self.controller.pages_font)
+                self.entry2 = tk.Entry(self.canvas, justify='center',
+                                       font=self.controller.pages_font)
                 self.entry2.insert(0, text)
                 self.entry2['exportselection'] = False
-        
+
                 self.entry2.bind("<Return>", self.on_return_display)
-                self.entry2.bind("<Escape>", lambda *ignore: self.entry.destroy())
-                
+                self.entry2.bind(
+                    "<Escape>", lambda *ignore: self.entry.destroy())
+
                 self.entry2.place(
-                    x = self.curX-self.controller.pages_font.measure(text)-20,
-                    y = self.start_y + 20, 
-                    anchor = tk.W,
-                    width = self.controller.pages_font.measure(text)+10)
-                
+                    x=self.curX-self.controller.pages_font.measure(text)-20,
+                    y=self.start_y + 20,
+                    anchor=tk.W,
+                    width=self.controller.pages_font.measure(text)+10)
+
                 self.saved = False
             else:
                 self.canvas.delete('loop-'+str(self.l))
         else:
             self.updateLoops()
-    
+
     def updateLoops(self):
         """ Checks if, after some movement, a module is included in an existing
             loop
@@ -267,7 +275,7 @@ def updateLoops(self):
                     self.loops[l]['mod'].append(self.canvas.itemcget(
                         mod, 'text'))
         print('Updated loop info:', self.loops)
-        
+
     def select(self, event):
         """ Selects the module at the mouse location. """
         self.selected = self.canvas.find_overlapping(
@@ -277,7 +285,7 @@ def select(self, event):
                 self.canvas.selected = self.selected[-2]
             else:
                 self.canvas.selected = self.selected[-1]
-            # self.canvas.startxy = (event.x, event.y)
+            # self.canvas_startxy = (event.x, event.y)
         else:
             self.canvas.selected = None
             # save mouse drag start position
@@ -286,60 +294,61 @@ def select(self, event):
             self.curX = self.start_x
             self.curY = self.start_y
             self.selected = self.canvas.find_overlapping(
-                        event.x-5, event.y-5, event.x+5, event.y+5)
-    
+                event.x-5, event.y-5, event.x+5, event.y+5)
+
             # create rectangle if not yet exist
             if not self.selected:
-                self.rect = self.canvas.create_rectangle(event.x, event.y, 
-                                             event.x, event.y, 
-                                             outline = '#4ff07a',
-                                             tag = 'loop-'+str(self.l))
-                                             # ,fill = '#b3ffc7')
+                self.rect = self.canvas.create_rectangle(event.x, event.y,
+                                                         event.x, event.y,
+                                                         outline='#4ff07a',
+                                                         tag='loop-'+str(self.l))
+                # ,fill = '#b3ffc7')
                 self.drawLoop = True
                 if self.l == 0:
                     self.canvas.tag_lower('loop-'+str(self.l))
                 else:
-                    self.canvas.tag_raise('loop-'+str(self.l), 'loop-'+str(self.l-1))
-        
+                    self.canvas.tag_raise(
+                        'loop-'+str(self.l), 'loop-'+str(self.l-1))
+
         if len(self.canvas.gettags("current")) > 0:
             if(len(self.canvas.gettags("current")[0].split('-')) > 1) and (
-                self.canvas.gettags("current")[0].split('-')[0] == 'loop'):
+                    self.canvas.gettags("current")[0].split('-')[0] == 'loop'):
                 self.isLoop = True
                 self.m = int(self.canvas.gettags("current")[0].split('-')[1])
             else:
                 self.isLoop = False
                 self.m = int(self.canvas.gettags("current")[0][1:])
-    
+
     def on_drag(self, event):
         """ Uses the mouse location to move the module and its text. 
         At the same time, it looks up if there are any connection to this
         module and subsequently moves the connection."""
-        
+
         if self.drawLoop:
             self.curX = self.canvas.canvasx(event.x)
             self.curY = self.canvas.canvasy(event.y)
-    
+
             # w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
             # if event.x > 0.9*w:
-            #     self.canvas.xview_scroll(1, 'units') 
+            #     self.canvas.xview_scroll(1, 'units')
             # elif event.x < 0.1*w:
             #     self.canvas.xview_scroll(-1, 'units')
             # if event.y > 0.9*h:
-            #     self.canvas.yview_scroll(1, 'units') 
+            #     self.canvas.yview_scroll(1, 'units')
             # elif event.y < 0.1*h:
             #     self.canvas.yview_scroll(-1, 'units')
-    
+
             # expand rectangle as you drag the mouse
-            self.canvas.coords('loop-'+str(self.l), self.start_x, 
+            self.canvas.coords('loop-'+str(self.l), self.start_x,
                                self.start_y, self.curX, self.curY)
         else:
             self.select(event)
-            
+
             self.m = int(self.canvas.gettags("current")[0][1:])
-            dx = event.x - self.canvas.startxy[self.m][0]
-            dy = event.y - self.canvas.startxy[self.m][1]
+            dx = event.x - self.canvas_startxy[self.m][0]
+            dy = event.y - self.canvas_startxy[self.m][1]
             # if self.isLoop:
-                # self.canvas.move('loop-'+str(self.m), dx, dy) 
+            # self.canvas.move('loop-'+str(self.m), dx, dy)
             # else:
             # module
             self.canvas.move('o'+str(self.m), dx, dy)
@@ -348,35 +357,35 @@ def on_drag(self, event):
             if any(self.out_data.iloc[self.m].values) or any(
                     self.out_data[self.out_data.columns[self.m]].values):
                 out = np.arange(self.out_data.values.shape[0])[
-                    self.out_data.values[self.m, :]==1]
+                    self.out_data.values[self.m, :] == 1]
                 for o in out:
                     xycoord_i = self.canvas.coords(
                         self.connections[o][self.m].split('-')[0])
                     xycoord_o = self.canvas.coords(
                         self.connections[o][self.m].split('-')[1])
-                    self.canvas.coords(self.connections[o][self.m], 
-                            (xycoord_i[0] + self.cr, 
-                             xycoord_i[1] + self.cr, 
-                             xycoord_o[0] + self.cr, 
-                             xycoord_o[1] + self.cr))
+                    self.canvas.coords(self.connections[o][self.m],
+                                       (xycoord_i[0] + self.cr,
+                                        xycoord_i[1] + self.cr,
+                                        xycoord_o[0] + self.cr,
+                                        xycoord_o[1] + self.cr))
                 inp = np.arange(self.out_data.values.shape[0])[
-                    self.out_data.values[:, self.m]==1]
+                    self.out_data.values[:, self.m] == 1]
                 for i in inp:
                     xycoord_i = self.canvas.coords(
                         self.connections[self.m][i].split('-')[0])
                     xycoord_o = self.canvas.coords(
                         self.connections[self.m][i].split('-')[1])
-                    self.canvas.coords(self.connections[self.m][i], 
-                            (xycoord_i[0] + self.cr, 
-                             xycoord_i[1] + self.cr, 
-                             xycoord_o[0] + self.cr, 
-                             xycoord_o[1] + self.cr))
+                    self.canvas.coords(self.connections[self.m][i],
+                                       (xycoord_i[0] + self.cr,
+                                        xycoord_i[1] + self.cr,
+                                        xycoord_o[0] + self.cr,
+                                        xycoord_o[1] + self.cr))
             # Update module location
-            self.canvas.startxy[self.m] = (event.x, event.y)
-    
+            self.canvas_startxy[self.m] = (event.x, event.y)
+
     def module_out(self, name):
         """ Updates the output DataFrame.
-        
+
         :param name: name of the model
         :type name: str
         """
@@ -386,180 +395,180 @@ def module_out(self, name):
             name_list.append(name + '-' + str(len(m_num)))
         else:
             name_list.append(name)
-        self.canvas.itemconfig('t'+str(self.modules), text = name_list[-1])
+        self.canvas.itemconfig('t'+str(self.modules), text=name_list[-1])
         values = self.out_data.values
         values = np.vstack((
-                    np.hstack((values, np.zeros((values.shape[0],1)))),
-                    np.zeros((1, values.shape[0]+1))))
-        self.out_data = pd.DataFrame(values, 
-                                     columns = name_list, 
-                                     index = name_list)
+            np.hstack((values, np.zeros((values.shape[0], 1)))),
+            np.zeros((1, values.shape[0]+1))))
+        self.out_data = pd.DataFrame(values,
+                                     columns=name_list,
+                                     index=name_list)
         self.module_names.append(name_list[-1])
 
-    def add_module(self, boxName, x, y, ini = False, out = False):
+    def add_module(self, boxName, x, y, ini=False, out=False):
         """ Creates a rectangular module with the corresponding text inside.
-        
+
         :param boxName: name of the model
         :type boxName: str
         """
         if not ini and not out:
             tag = ('o'+str(self.modules),)
-        else: #Make initialisation and output unmoveable
+        else:  # Make initialisation and output unmoveable
             tag = ('n0',)
         text_w = self.controller.pages_font.measure(boxName+'-00') + 10
         self.canvas.create_rectangle(
-            x - text_w/2 , 
-            y - self.h/2, 
-            x + text_w/2, 
-            y + self.h/2, 
-            tags = tag + ('p'+str(self.modules),), 
-            fill = self.bg, width = 3,
-            activefill = '#dbaa21')
+            x - text_w/2,
+            y - self.h/2,
+            x + text_w/2,
+            y + self.h/2,
+            tags=tag + ('p'+str(self.modules),),
+            fill=self.bg, width=3,
+            activefill='#dbaa21')
         self.canvas.create_text(
-            x, 
-            y, 
-            font = self.controller.pages_font, 
-            text = boxName, 
-            tags = tag + ('t'+str(self.modules),), 
-            fill = '#d0d4d9', 
-            justify = tk.CENTER)
-        self.canvas.tag_bind('t'+str(self.modules), 
+            x,
+            y,
+            font=self.controller.pages_font,
+            text=boxName,
+            tags=tag + ('t'+str(self.modules),),
+            fill='#d0d4d9',
+            justify=tk.CENTER)
+        self.canvas.tag_bind('t'+str(self.modules),
                              "<Double-1>", self.OnDoubleClick)
         if not out:
             self.canvas.create_oval(
-                x - self.cr, 
-                y + self.h/2 - self.cr, 
-                x + self.cr, 
-                y + self.h/2 + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('d'+str(self.modules),))
-            self.canvas.tag_bind('d'+str(self.modules), 
+                x - self.cr,
+                y + self.h/2 - self.cr,
+                x + self.cr,
+                y + self.h/2 + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('d'+str(self.modules),))
+            self.canvas.tag_bind('d'+str(self.modules),
                                  "<Button-1>", self.join_modules)
-            
+
         if not ini:
             self.canvas.create_oval(
-                x - self.cr, 
-                y - self.h/2 - self.cr, 
-                x + self.cr, 
-                y - self.h/2 + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('u'+str(self.modules),))
-            self.canvas.tag_bind('u'+str(self.modules), 
+                x - self.cr,
+                y - self.h/2 - self.cr,
+                x + self.cr,
+                y - self.h/2 + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('u'+str(self.modules),))
+            self.canvas.tag_bind('u'+str(self.modules),
                                  "<Button-1>", self.join_modules)
-        
+
         if not out and not ini:
             self.canvas.create_oval(
-                x - text_w/2 - self.cr, 
-                y - self.cr, 
-                x - text_w/2 + self.cr, 
-                y + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('l'+str(self.modules),))
-            self.canvas.tag_bind('l'+str(self.modules), 
+                x - text_w/2 - self.cr,
+                y - self.cr,
+                x - text_w/2 + self.cr,
+                y + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('l'+str(self.modules),))
+            self.canvas.tag_bind('l'+str(self.modules),
                                  "<Button-1>", self.join_modules)
-        
+
             self.canvas.create_oval(
-                x + text_w/2 - self.cr, 
-                y - self.cr, 
-                x + text_w/2 + self.cr, 
-                y + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('r'+str(self.modules),))
-            self.canvas.tag_bind('r'+str(self.modules), 
+                x + text_w/2 - self.cr,
+                y - self.cr,
+                x + text_w/2 + self.cr,
+                y + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('r'+str(self.modules),))
+            self.canvas.tag_bind('r'+str(self.modules),
                                  "<Button-1>", self.join_modules)
-        self.canvas.startxy.append((x, 
+        self.canvas_startxy.append((x,
                                     y))
         self.connections[self.modules] = {}
         self.module_out(boxName)
         self.module_list.append(boxName)
         self.modules += 1
         self.saved = False
-    
+
     def on_return_rename(self, event):
         """ This function renames a module to the input specified through 
         an entry widget. It then updates the corresponding rows and columns in
         the stored data."""
-        
+
         moduleName = self.entry.get()
         if (moduleName in list(self.module_names)) and not(
                 moduleName == list(self.module_names)[self.m]):
             messagebox.showwarning("Error", "This module already exists.")
         else:
-            self.canvas.itemconfig('t'+str(self.m), text = moduleName)
+            self.canvas.itemconfig('t'+str(self.m), text=moduleName)
             x0, y0, x1, y1 = self.canvas.coords('p'+str(self.m))
             text_w = self.controller.pages_font.measure(moduleName) + 20
             shift = (text_w-x1+x0)/2
-            self.canvas.coords('p'+str(self.m), # Resize rectangle
-                               x0 - shift, 
-                               y0, 
-                               x1 + shift, 
+            self.canvas.coords('p'+str(self.m),  # Resize rectangle
+                               x0 - shift,
+                               y0,
+                               x1 + shift,
                                y1)
             x0, y0, x1, y1 = self.canvas.coords('l'+str(self.m))
-            self.canvas.coords('l'+str(self.m), # Move left circle
-                                x0 - shift, 
-                                y0, 
-                                x1 - shift, 
-                                y1)
+            self.canvas.coords('l'+str(self.m),  # Move left circle
+                               x0 - shift,
+                               y0,
+                               x1 - shift,
+                               y1)
             x0, y0, x1, y1 = self.canvas.coords('r'+str(self.m))
-            self.canvas.coords('r'+str(self.m), # Move left circle
-                                x0 + shift, 
-                                y0, 
-                                x1 + shift, 
-                                y1)
+            self.canvas.coords('r'+str(self.m),  # Move left circle
+                               x0 + shift,
+                               y0,
+                               x1 + shift,
+                               y1)
             self.entry.destroy()
             self.module_names[self.m] = moduleName
             self.saved = False
-            
+
     def on_return_editLoop(self, event):
         """ This function renames loop conditions and updates the loop
         information."""
-        
+
         newText = self.entry.get()
         conditions = self.canvas.itemcget(
-                        self.selected[0], 'tags').split(' ')[1].split('-')
-        self.canvas.itemconfig(self.selected[0], text = newText)
+            self.selected[0], 'tags').split(' ')[1].split('-')
+        self.canvas.itemconfig(self.selected[0], text=newText)
         self.entry.destroy()
         self.loops[int(conditions[1])][conditions[0]] = newText
         self.saved = False
-        
+
     def OnDoubleClick(self, event):
-        
         """ Executed when text is double clicked.
         Opens an entry box to edit the module name and updates the display and
         the stored data. """
-        
+
         self.selected = self.canvas.find_overlapping(
             event.x-5, event.y-5, event.x+5, event.y+5)
-        
+
         # If not loop text
         if len(self.canvas.itemcget(
                 self.selected[0], 'tags').split(' ')[-2].split('-')) < 2:
-            
-            x0, y0, x1, y1 = self.canvas.coords(self.canvas.gettags("current")[0])
+
+            x0, y0, x1, y1 = self.canvas.coords(
+                self.canvas.gettags("current")[0])
             if hasattr(self, 'entry'):
                 self.entry.destroy()
-            
+
             entryText = self.canvas.itemcget('t'+str(self.m), 'text')
-            
-            self.entry = tk.Entry(self.canvas, justify='center', 
-                                  font = self.controller.pages_font)
-    
+
+            self.entry = tk.Entry(self.canvas, justify='center',
+                                  font=self.controller.pages_font)
+
             self.entry.insert(
                 0, entryText)
             self.entry['selectbackground'] = '#d0d4d9'
             self.entry['exportselection'] = False
-    
+
             self.entry.focus_force()
             self.entry.bind("<Return>", self.on_return_rename)
             self.entry.bind("<Escape>", lambda *ignore: self.entry.destroy())
-            
-            self.entry.place(x = x0, 
-                              y = y0 + (y1-y0)/2, 
-                              anchor = tk.W, width = x1 - x0)
+
+            self.entry.place(x=x0,
+                             y=y0 + (y1-y0)/2,
+                             anchor=tk.W, width=x1 - x0)
         # If loop text
         else:
             x0, y0 = self.canvas.coords(self.selected[0])
@@ -569,22 +578,22 @@ def OnDoubleClick(self, event):
             # condition = self.canvas.itemcget(
             #             self.selected[0], 'tags').split(' ')[-2]#.split('-')[0]
             entryText = self.canvas.itemcget(self.selected[0], 'text')
-            
-            self.entry = tk.Entry(self.canvas, justify='center', 
-                                  font = self.controller.pages_font)
+
+            self.entry = tk.Entry(self.canvas, justify='center',
+                                  font=self.controller.pages_font)
             self.entry.insert(0, entryText)
             # self.entry1['selectbackground'] = '#d0d4d9'
             self.entry['exportselection'] = False
-    
+
             self.entry.focus()
             self.entry.bind("<Return>", self.on_return_editLoop)
             self.entry.bind("<Escape>", lambda *ignore: self.entry.destroy())
             text_w = self.controller.pages_font.measure(entryText) + 20
-            self.entry.place(x = x0 - text_w/2, 
-                          y = y0, 
-                          anchor = tk.W, 
-                          width = text_w)
-                
+            self.entry.place(x=x0 - text_w/2,
+                             y=y0,
+                             anchor=tk.W,
+                             width=text_w)
+
     def delete_sel(self):
         """ Deletes last selected module"""
         if self.canvas.selected:
@@ -593,31 +602,31 @@ def delete_sel(self):
                 self.loops[self.m] = -1
             elif not(self.m == 0):
                 self.canvas.delete('o'+str(self.m))
-                
+
                 col = self.out_data.columns[self.m]
                 self.out_data[col] = 0
                 self.out_data.loc[col] = 0
                 self.saved = False
                 self.module_names[self.m] = None
-                
+
     def join_modules(self, event):
         """ Draws a connecting line between two connecting circles. """
         if self.draw:
             tags = self.canvas.gettags(self.canvas.find_overlapping(
-            event.x-self.cr, event.y-self.cr, 
-            event.x+self.cr, event.y+self.cr)[-1])
+                event.x-self.cr, event.y-self.cr,
+                event.x+self.cr, event.y+self.cr)[-1])
             tag2 = tags[-2] if tags[-1] == "current" else tags[-1]
             if tag2[0] in ['d', 'l', 'u', 'r'] and int(
                     tag2[1:]) != int(self.tag[1:]):
                 self.canvas.create_line(
-                            self.canvas.linestartxy[0] + self.cr, 
-                            self.canvas.linestartxy[1] + self.cr, 
-                            self.canvas.coords(tag2)[0] + self.cr, 
-                            self.canvas.coords(tag2)[1] + self.cr,
-                            fill = "red", 
-                            arrow = tk.LAST, 
-                            tags = ('o'+str(int(self.tag[1:])), 
-                                  'o'+str(int(tag2[1:])), self.tag + '-' + tag2))
+                    self.canvas.linestartxy[0] + self.cr,
+                    self.canvas.linestartxy[1] + self.cr,
+                    self.canvas.coords(tag2)[0] + self.cr,
+                    self.canvas.coords(tag2)[1] + self.cr,
+                    fill="red",
+                    arrow=tk.LAST,
+                    tags=('o'+str(int(self.tag[1:])),
+                          'o'+str(int(tag2[1:])), self.tag + '-' + tag2))
                 self.out_data.iloc[int(self.tag[1:])][int(tag2[1:])] = 1
                 self.connections[int(tag2[1:])][
                     int(self.tag[1:])] = self.tag + '-' + tag2
@@ -625,54 +634,58 @@ def join_modules(self, event):
             self.saved = False
         else:
             tags = self.canvas.gettags(self.canvas.find_overlapping(
-            event.x-self.cr, event.y-self.cr, 
-            event.x+self.cr, event.y+self.cr)[-1])
+                event.x-self.cr, event.y-self.cr,
+                event.x+self.cr, event.y+self.cr)[-1])
             self.tag = tags[-2] if tags[-1] == "current" else tags[-1]
-            self.canvas.linestartxy = self.canvas.coords(self.tag)#(event.x, event.y)
+            self.canvas.linestartxy = self.canvas.coords(
+                self.tag)  # (event.x, event.y)
             self.draw = True
 
     def save_file_as(self):
-        
+
         self.save_path = asksaveasfile(mode='w')
         self.save_file()
-        
+
     def save_file(self):
-        
+
         if self.save_path == '':
-            self.save_path = asksaveasfile(defaultextension = '.xml', filetypes = 
-                                      [('XML file', '.xml'), 
-                                       ('All Files', '*.*')])
-        if self.save_path is not None: # asksaveasfile return `None` if dialog closed with "cancel".
+            self.save_path = asksaveasfile(defaultextension='.xml', filetypes=[('XML file', '.xml'),
+                                                                               ('All Files', '*.*')])
+        # asksaveasfile return `None` if dialog closed with "cancel".
+        if self.save_path is not None:
             # Transform input for output file
             data = self.out_data.copy()
-            if np.sum(data.values[0,:]) == 0:
+            if np.sum(data.values[0, :]) == 0:
                 messagebox.showwarning(
                     "Error", "Initialiser is not connected to any module.")
-            elif np.sum(data.values[:,1]) == 0:
+            elif np.sum(data.values[:, 1]) == 0:
                 messagebox.showwarning(
                     "Error", "Output is not connected to any module.")
-            idx = (np.sum(data.values, axis = 0) 
-                   + np.sum(data.values, axis = 1)) < 1
+            idx = (np.sum(data.values, axis=0)
+                   + np.sum(data.values, axis=1)) < 1
             col = np.array(data.columns)
             for c in np.arange(len(idx))[idx]:
                 data = data.drop(columns=col[c], index=col[c])
-                
+
             loop_modules = np.unique([v for a in self.loops for v in a['mod']])
             out_loops = np.zeros_like(self.loops)
-            
+
             values = data.values.astype(bool)
             mn = np.array(self.module_names)
             mn_id = [m is not None for m in mn]
             mn = mn[mn_id]
 
-            # to avoid numpy bug during elementwise comparison of lists 
-            loop_modules.dtype = mn.dtype if len(loop_modules) == 0 else loop_modules.dtype
+            # to avoid numpy bug during elementwise comparison of lists
+            loop_modules.dtype = mn.dtype if len(
+                loop_modules) == 0 else loop_modules.dtype
 
             # self.s = XML_handler()
             # self.controller.s.new_config_file(self.save_path.name)
-            
-            self.controller.s.update_module_coords(self.module_list[0],[self.canvas.startxy[0], 0, self.connections[0]])
-            self.controller.s.append_module_relationships(self.module_list[0],list(mn[values[:,0]]),list(mn[values[0,:]]))
+
+            self.controller.s.update_module_coords(
+                self.module_list[0], [self.canvas_startxy[0], 0, self.connections[0]])
+            self.controller.s.append_module_relationships(
+                self.module_list[0], list(mn[values[:, 0]]), list(mn[values[0, :]]))
             self.controller.s.filename = self.save_path.name
             # self.controller.s.append_pipeline_module(self.module_list[0], # Initialiser
             #                       mn[0],
@@ -686,50 +699,56 @@ def save_file(self):
                 if (i > 1) and mnn:
                     xml_parent = None
                     parent_loops = []
-                    if mn[i] in loop_modules: # Model in any loop
+                    if mn[i] in loop_modules:  # Model in any loop
                         for x, loop in enumerate(self.loops):
-                            if mn[i] in loop['mod']: # Model in this loop
-                                if not out_loops[x]: # Loop already defined
+                            if mn[i] in loop['mod']:  # Model in this loop
+                                if not out_loops[x]:  # Loop already defined
                                     self.controller.s.append_pipeline_loop(self.loops[x]['type'],
-                                                self.loops[x]['condition'],
-                                                "loop"+str(x),
-                                                parent_loops,
-                                                list(loop['mod']),
-                                                xml_parent,
-                                                self.loops[x]['coord']
-                                                )
+                                                                           self.loops[x]['condition'],
+                                                                           "loop" +
+                                                                           str(x),
+                                                                           parent_loops,
+                                                                           list(
+                                                                               loop['mod']),
+                                                                           xml_parent,
+                                                                           self.loops[x]['coord']
+                                                                           )
                                     out_loops[x] = 1
-                                xml_parent = "loop"+str(x) # parent is the last loop
+                                # parent is the last loop
+                                xml_parent = "loop"+str(x)
                                 parent_loops.append("loop"+str(x))
 
                     self.controller.s.append_pipeline_module(self.module_list[i],
-                      mn[i],
-                      "",
-                      {},
-                      list(mn[values[:,i]]),
-                      list(mn[values[i,:]]),
-                      xml_parent,
-                      [self.canvas.startxy[i], i, self.connections[i]])
-                
-            self.controller.s.append_pipeline_module(self.module_list[1], # Out
-                      mn[1],
-                      "",
-                      {},
-                      list(mn[values[:,1]]),
-                      list(mn[values[1,:]]),
-                      None,
-                      [self.canvas.startxy[1], 1, self.connections[1]])
+                                                             mn[i],
+                                                             "",
+                                                             {},
+                                                             list(
+                                                                 mn[values[:, i]]),
+                                                             list(
+                                                                 mn[values[i, :]]),
+                                                             xml_parent,
+                                                             [self.canvas_startxy[i], i, self.connections[i]])
+
+            self.controller.s.append_pipeline_module(self.module_list[1],  # Out
+                                                     mn[1],
+                                                     "",
+                                                     {},
+                                                     list(mn[values[:, 1]]),
+                                                     list(mn[values[1, :]]),
+                                                     None,
+                                                     [self.canvas_startxy[1], 1, self.connections[1]])
             self.controller.s.write_to_XML()
             self.saved = True
-            self.controller._append_to_output("xml_filename",self.save_path.name)
+            self.controller._append_to_output(
+                "xml_filename", self.save_path.name)
 
     def upload(self):
-        
-        filename = askopenfilename(initialdir = os.getcwd(), 
-                                   title = 'Select a file', 
-                                   defaultextension = '.xml', 
-                                   filetypes = [('XML file', '.xml'), 
-                                                ('All Files', '*.*')])
+
+        filename = askopenfilename(initialdir=os.getcwd(),
+                                   title='Select a file',
+                                   defaultextension='.xml',
+                                   filetypes=[('XML file', '.xml'),
+                                              ('All Files', '*.*')])
         if filename is not None:
             self.reset()
 
@@ -738,65 +757,71 @@ def upload(self):
             s._print_pretty(s.loaded_modules)
             modules = s.loaded_modules
             modout = modules['output']
-            del modules['Initialiser'], modules['output'] # They are generated when resetting
+            # They are generated when resetting
+            del modules['Initialiser'], modules['output']
             disp_mod = ['Initialiser', 'output']
             id_mod = [0, 1]
-            
+
             # Place the modules
             disp_mod, id_mod = self.place_modules(modules, id_mod, disp_mod)
             connect = list(modout['coordinates'][2].keys())
             for p, parent in enumerate(modout['parents']):
-                    parent_id = id_mod[np.where(np.array(disp_mod) == parent)[0][0]]
-                    out, ins = modout['coordinates'][2][connect[p]].split('-')
-                    xout, yout, _ , _ = self.canvas.coords(out[0]+str(parent_id))
-                    xins, yins, _, _ =self.canvas.coords(ins[0]+str(1))
-                    self.canvas.create_line(
-                                xout + self.cr, 
-                                yout + self.cr, 
-                                xins + self.cr, 
-                                yins + self.cr,
-                                fill = "red", 
-                                arrow = tk.LAST, 
-                                tags = ('o'+str(parent_id), 
-                                      'o'+str(1), modout['coordinates'][2][connect[p]]))
-                    self.out_data.iloc[int(parent_id)][1] = 1
-                    self.connections[1][
-                        int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(1)
-            self.controller._append_to_output("xml_filename",filename)
+                parent_id = id_mod[np.where(
+                    np.array(disp_mod) == parent)[0][0]]
+                out, ins = modout['coordinates'][2][connect[p]].split('-')
+                xout, yout, _, _ = self.canvas.coords(out[0]+str(parent_id))
+                xins, yins, _, _ = self.canvas.coords(ins[0]+str(1))
+                self.canvas.create_line(
+                    xout + self.cr,
+                    yout + self.cr,
+                    xins + self.cr,
+                    yins + self.cr,
+                    fill="red",
+                    arrow=tk.LAST,
+                    tags=('o'+str(parent_id),
+                          'o'+str(1), modout['coordinates'][2][connect[p]]))
+                self.out_data.iloc[int(parent_id)][1] = 1
+                self.connections[1][
+                    int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(1)
+            self.controller._append_to_output("xml_filename", filename)
 
     def place_modules(self, modules, id_mod, disp_mod):
         # Place the modules
         for key in [key for key, val in modules.items() if type(val) == dict]:
             if modules[key]['class'] == 'loop':
                 # Extracts numbers from string
-                l = int(''.join(map(str, list(filter(str.isdigit, modules[key]['name'])))))
+                l = int(
+                    ''.join(map(str, list(filter(str.isdigit, modules[key]['name'])))))
                 x0, y0, x1, y1 = modules[key]['coordinates']
-                self.canvas.create_rectangle(x0, y0, 
-                                             x1, y1, 
-                                             outline = '#4ff07a',
-                                             tag = 'loop-'+str(l))
-                text_w = self.controller.pages_font.measure(modules[key]['type']) + 20
+                self.canvas.create_rectangle(x0, y0,
+                                             x1, y1,
+                                             outline='#4ff07a',
+                                             tag='loop-'+str(l))
+                text_w = self.controller.pages_font.measure(
+                    modules[key]['type']) + 20
                 self.canvas.create_text(
-                    x0 + text_w/2, 
-                    y0 + 20, 
-                    font = self.controller.pages_font, 
-                    text = modules[key]['type'], 
-                    tags = ('loop-'+str(l), 'type'+'-'+str(l)), 
-                    justify = tk.CENTER)
-                text_w = self.controller.pages_font.measure(modules[key]['condition']) + 20
+                    x0 + text_w/2,
+                    y0 + 20,
+                    font=self.controller.pages_font,
+                    text=modules[key]['type'],
+                    tags=('loop-'+str(l), 'type'+'-'+str(l)),
+                    justify=tk.CENTER)
+                text_w = self.controller.pages_font.measure(
+                    modules[key]['condition']) + 20
                 self.canvas.create_text(
-                    x1 - text_w/2, 
-                    y0 + 20, 
-                    font = self.controller.pages_font, 
-                    text = modules[key]['condition'], 
-                    tags = ('loop-'+str(l), 'condition'+'-'+str(l)), 
-                    justify = tk.CENTER)
+                    x1 - text_w/2,
+                    y0 + 20,
+                    font=self.controller.pages_font,
+                    text=modules[key]['condition'],
+                    tags=('loop-'+str(l), 'condition'+'-'+str(l)),
+                    justify=tk.CENTER)
                 self.loops.append({'type': modules[key]['type'],
                                    'condition': modules[key]['condition'],
-                                   'mod': [], 
-                                   'coord': (x0, y0, 
+                                   'mod': [],
+                                   'coord': (x0, y0,
                                              x1, y1)})
-                disp_mod, id_mod = self.place_modules(modules[key], id_mod, disp_mod)
+                disp_mod, id_mod = self.place_modules(
+                    modules[key], id_mod, disp_mod)
             else:
                 # Display module
                 self.add_module(key,
@@ -805,23 +830,27 @@ def place_modules(self, modules, id_mod, disp_mod):
                 self.module_list[-1] = modules[key]['module_type']
                 id_mod.append(modules[key]['coordinates'][1])
                 connect = list(modules[key]['coordinates'][2].keys())
-                
+
                 # Connect modules
                 for p, parent in enumerate(modules[key]['parents']):
                     if not (parent[:4] == 'loop'):
-                        parent_id = id_mod[np.where(np.array(disp_mod) == parent)[0][0]]
-                        out, ins = modules[key]['coordinates'][2][connect[p]].split('-')
-                        xout, yout, _ , _ = self.canvas.coords(out[0]+str(parent_id))
-                        xins, yins, _, _ =self.canvas.coords(ins[0]+str(id_mod[-1]))
+                        parent_id = id_mod[np.where(
+                            np.array(disp_mod) == parent)[0][0]]
+                        out, ins = modules[key]['coordinates'][2][connect[p]].split(
+                            '-')
+                        xout, yout, _, _ = self.canvas.coords(
+                            out[0]+str(parent_id))
+                        xins, yins, _, _ = self.canvas.coords(
+                            ins[0]+str(id_mod[-1]))
                         self.canvas.create_line(
-                                    xout + self.cr, 
-                                    yout + self.cr, 
-                                    xins + self.cr, 
-                                    yins + self.cr,
-                                    fill = "red", 
-                                    arrow = tk.LAST, 
-                                    tags = ('o'+str(parent_id), 
-                                          'o'+str(id_mod[-1]), modules[key]['coordinates'][2][connect[p]]))
+                            xout + self.cr,
+                            yout + self.cr,
+                            xins + self.cr,
+                            yins + self.cr,
+                            fill="red",
+                            arrow=tk.LAST,
+                            tags=('o'+str(parent_id),
+                                  'o'+str(id_mod[-1]), modules[key]['coordinates'][2][connect[p]]))
                         self.out_data.iloc[int(parent_id)][int(id_mod[-1])] = 1
                         self.connections[int(id_mod[-1])][
                             int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(id_mod[-1])
@@ -829,44 +858,45 @@ def place_modules(self, modules, id_mod, disp_mod):
                         self.loops[-1]['mod'].append(key)
                 disp_mod.append(key)
         return disp_mod, id_mod
-    
+
     def reset(self):
-        
+
         if not self.saved:
             msg = messagebox.askyesnocancel(
                 'Info', 'Are you sure you want to reset the canvas?')
         else:
             msg = True
         if msg:
-            self.canvas.delete(tk.ALL) # Reset canvas
-            
+            self.canvas.delete(tk.ALL)  # Reset canvas
+
             if hasattr(self, 'entry'):
                 self.entry.destroy()
             if hasattr(self, 'entry1'):
                 self.entry1.destroy()
             if hasattr(self, 'entry2'):
                 self.entry2.destroy()
-                
-            self.canvas.startxy = []
+
+            self.canvas_startxy = []
             self.out_data = pd.DataFrame()
             self.connections = {}
             self.modules = 0
             self.module_list = []
             self.module_names = []
-            
-            self.add_module('Initialiser', self.width/2, self.h, ini = True)
-            self.add_module('Output', self.width/2, self.height - self.h, out = True)
-        
+
+            self.add_module('Initialiser', self.width/2, self.h, ini=True)
+            self.add_module('Output', self.width/2,
+                            self.height - self.h, out=True)
+
             self.draw = False
             self.loops = []
             self.drawLoop = False
             self.l = 0
 
     def check_quit(self):
-        
+
         if not self.saved:
             response = messagebox.askokcancel(
-                "Are you sure you want to leave?", 
+                "Are you sure you want to leave?",
                 "Do you want to leave the program without saving?")
             if response:
                 if self.save_path not in [None, '']:
@@ -874,7 +904,7 @@ def check_quit(self):
                 self.controller._show_frame("MainPage")
         else:
             if self.save_path not in [None, '']:
-                    self.controller.XML.set(True)
+                self.controller.XML.set(True)
             self.controller._show_frame("MainPage")
 
 # class ResizingCanvas(tk.Canvas):
@@ -890,11 +920,7 @@ def check_quit(self):
 #         hscale = float(event.height)/self.height
 #         self.width = event.width
 #         self.height = event.height
-#         # resize the canvas 
+#         # resize the canvas
 #         self.config(width=self.width, height=self.height)
 #         # rescale all the objects tagged with the "all" tag
 #         self.scale("all",0,0,wscale,hscale)
-        
-if __name__ == "__main__":
-    app = aidCanvas()
-    app.mainloop()
\ No newline at end of file
diff --git a/aidesign/utils/plugins/dataLoader.py b/aidesign/utils/plugins/dataLoader.py
index 832a8baa..c91e3e6e 100644
--- a/aidesign/utils/plugins/dataLoader.py
+++ b/aidesign/utils/plugins/dataLoader.py
@@ -3,8 +3,9 @@
 from PIL import ImageTk
 from tkinter import ttk
 import numpy as np
+from aidesign._import_helper import get_lib_parent_dir
 # from ttkwidgets import CheckboxTreeview
-from sys import platform
+# from sys import platform
 
 class dataLoader():
     """ Creates a window 
@@ -16,11 +17,10 @@ def __init__(self,controller,data):
         
         # Window options
         self.newWindow.title('Data Importer')
-        script_dir = os.path.dirname(__file__)
-
         self.newWindow.tk.call('wm','iconphoto', self.newWindow._w, ImageTk.PhotoImage(
             file = os.path.join(os.path.join(
-                script_dir, 
+                get_lib_parent_dir(), 
+                'utils', 
                 'resources', 
                 'Assets', 
                 'ADDIcon.ico'))))
diff --git a/aidesign/utils/plugins/pipelineCanvas.py b/aidesign/utils/plugins/pipelineCanvas.py
index 7c3f89dd..bd5a4697 100644
--- a/aidesign/utils/plugins/pipelineCanvas.py
+++ b/aidesign/utils/plugins/pipelineCanvas.py
@@ -1,341 +1,300 @@
-# Import the required libraries
-import tkinter as tk
-from PIL import Image, ImageTk
+from aidesign.Data.xml_handler import XML_handler
+from aidesign._plugin_helpers import PluginSpecs
+
 import os
 import numpy as np
 import pandas as pd
-from tkinter.filedialog import asksaveasfile, askopenfile, askopenfilename
+from PIL import Image, ImageTk
+
+import tkinter as tk
 from tkinter import messagebox
+from tkinter.filedialog import asksaveasfile, askopenfile, askopenfilename
 
-from aidesign.Data.xml_handler import XML_handler
-from aidesign.utils.plugin_helpers import PluginSpecs
 
-_PLUGIN_READABLE_NAMES = {"pipeline_canvas":"default","pipelineCanvas":"alias","pipeline canvas":"alias"}
-_PLUGIN_MODULE_OPTIONS = {"layer_priority": 2,
-                            "required_children": None}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {}
+_PLUGIN_READABLE_NAMES = {"pipeline_canvas": "default",
+                          "pipelineCanvas": "alias",
+                          "pipeline canvas": "alias"}       # type:ignore
+_PLUGIN_MODULE_OPTIONS = {"layer_priority": 2,              # type:ignore
+                          "required_children": None}        # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                              # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                              # type:ignore
+
 
 class pipelineCanvas(tk.Frame):
     """Canvas for the visualisation of the pipeline state."""
-    
-    def __init__(self, parent, controller, config:dict):
-        
+
+    def __init__(self, parent, controller, config: dict):
         """ Here we define the frame displayed for the plugin specification."""
-        
-        super().__init__(parent, bg = parent['bg'])
+
+        super().__init__(parent, bg=parent['bg'])
         self.bg = parent['bg']
         self.controller = controller
         self.s = XML_handler()
-        
+
         script_dir = os.path.dirname(__file__)
-        self.tk.call('wm','iconphoto', self.controller._w, ImageTk.PhotoImage(
-            file = os.path.join(os.path.join(
-                script_dir, 
-                'resources', 
-                'Assets', 
+        self.tk.call('wm', 'iconphoto', self.controller._w, ImageTk.PhotoImage(
+            file=os.path.join(os.path.join(
+                script_dir,
+                'resources',
+                'Assets',
                 'AIDIcon.ico'))))
-        self.grid_rowconfigure(tuple(range(2)), weight=1)
         self.grid_columnconfigure(0, weight=1)
-        
-        frame1 = tk.Frame(self, bg = self.bg)
-        self.frame2 = tk.Frame(self, bg = self.bg)
-        frame3 = tk.Frame(self, bg = self.bg)
-        self.frame4 = tk.Frame(self, bg = self.bg)
-        
+
+        frame1 = tk.Frame(self, bg=self.bg)
+        self.frame2 = tk.Frame(self, bg=self.bg)
+        frame3 = tk.Frame(self, bg=self.bg)
+        self.frame4 = tk.Frame(self, bg=self.bg)
+
         # Create canvas
         self.width, self.height = 600, 600
-        self.canvas = tk.Canvas(frame1, width=self.width, 
-            height=self.height, background="white")
-        self.canvas.pack(fill = tk.BOTH, expand = True, padx=(10,0), pady=10)
-        
+        self.canvas = tk.Canvas(frame1, width=self.width,
+                                height=self.height, background="white")
+        self.canvas.pack(fill=tk.BOTH, expand=True, padx=(10, 0), pady=10)
+
+        self.m: int
         self.w, self.h = 100, 50
         self.cr = 4
         """
         TODO: Implement clicking on canvas actions
+        Commented out WIP code as it was complicating searching for bugs in plugincanvas
         """
         # self.canvas.bind('<Button-1>', self.on_click)
-        self.plugin = {}
-        self.allWeHearIs = []
-        
-        
+        # self.plugin = {}
+        # self.allWeHearIs = []
+
         self.upload()
-        
+
         self.save_path = ''
         self.saved = True
         frame1.grid(column=0, row=0, sticky="nsew")
         self.frame2.grid(column=1, row=0, sticky="ne")
         frame3.grid(column=0, row=1, sticky="swe")
         self.frame4.grid(column=1, row=1, sticky="sew")
-        
-        frame3.grid_columnconfigure(tuple(range(2)), weight=1)
-        self.frame4.grid_columnconfigure(tuple(range(2)), weight=1)
-        
-    def class_list(self,value):
+
+        frame3.grid_columnconfigure(1, weight=1)
+        self.frame4.grid_columnconfigure(1, weight=1)
+
+    def class_list(self, value):
         """ Temporary fix """
         return value
 
-    def on_click(self, event):
-        """ Passes the mouse click coordinates to the select function."""
-        self.select(event.x, event.y)
-        
-    def select(self, x: float, y:float):
-        """ 
-        Selects the module at the mouse location and updates the associated 
-        plugins as well as the colours. 
-        Blue means no plugin has been specified,
-        Orange means the module is selected.
-        Green means the plugin for this module is already set. 
-        :param x: float type of module x coordinate
-        :param y: float type of module y coordinate
-        """
-        self.selected = self.canvas.find_overlapping(
-            x-5, y-5, x+5, y+5)
-        if self.selected:
-            if len(self.selected) > 2:
-                self.canvas.selected = self.selected[-2]
-            else:
-                self.canvas.selected = self.selected[-1]
-        
-        if (self.m in self.plugin.keys()) and\
-                (self.plugin[self.m].get() != 'None') and \
-                (self.m not in self.id_done): # add
-                self.id_done.append(self.m)
-                print(np.array(self.module_names)[self.m == np.array(self.id_mod)][0])
-                self.s.append_plugin_to_module(self.plugin[self.m].get(),
-                                                 {**self.req_settings, **self.opt_settings},
-                                                 np.array(self.module_names)[self.m == np.array(self.id_mod)][0],
-                                                 True)
-        if self.m in self.id_done and self.m > 1:
-            self.canvas.itemconfig('p'+str(self.m), fill = '#46da63')
-        else:
-            self.canvas.itemconfig('p'+str(self.m), fill = self.bg)
-        
-        if len(self.canvas.gettags(self.canvas.selected)) > 0:
-            if not (len(self.canvas.gettags(self.canvas.selected)[0].split('-')) > 1) and\
-                not (self.canvas.gettags(self.canvas.selected)[0].split('-')[0] == 'loop'):
-                self.m = int(self.canvas.gettags(self.canvas.selected)[0][1:])
-            if self.m > 1:
-                if self.m not in self.id_done and self.m > 1:
-                    self.canvas.itemconfig('p'+str(self.m), fill = '#dbaa21')
-                for widget in self.allWeHearIs:
-                    widget.grid_remove()
-                
-            #     self.display_buttons()
-            #     module_number = self.id_mod.index(self.m)
-            #     if hasattr(self, 'button_forw'):
-            #         if module_number == len(self.id_mod)-1:
-            #             self.button_forw.config(text = 'Finish', bg = self.bg, 
-            #                 font = self.controller.pages_font,
-            #                 fg = 'white', height = 3, width = 15,
-            #                 command = self.finnish, state = tk.NORMAL, image = '')
-            #         else:
-            #             pCoord = self.canvas.coords('p'+str(self.id_mod[module_number+1]))
-            #             self.button_forw.config(image = self.forw_img, bg = self.bg, 
-            #                 command = lambda: self.select(
-            #                     pCoord[0], pCoord[1]), text = '', state = tk.NORMAL)
-            #         if module_number < 3:
-            #             self.button_back.config(image = self.back_img, bg = self.bg, 
-            #                 state = tk.DISABLED, text = '')
-            #         else:
-            #             mCoord = self.canvas.coords('p'+str(self.id_mod[module_number-1]))
-            #             self.button_back.config(image = self.back_img, bg = self.bg, 
-            #                 command = lambda: self.select(
-            #                     mCoord[0], mCoord[1]), text = '', state = tk.NORMAL)
-            #     else:
-            #         if module_number == len(self.id_mod)-1:
-            #             self.button_forw = tk.Button(
-            #                 self.frame4, text = 'Finish', bg = self.bg, 
-            #                 font = self.controller.pages_font,
-            #                 fg = 'white', height = 3, width = 15,
-            #                 command = self.finnish, state = tk.NORMAL, image = '')
-            #         else:
-            #             pCoord = self.canvas.coords('p'+str(self.id_mod[module_number+1]))
-            #             self.button_forw = tk.Button(
-            #                 self.frame4, image = self.forw_img, bg = self.bg, 
-            #                 command = lambda: self.select(
-            #                     pCoord[0], pCoord[1]), state = tk.NORMAL)
-            #         self.button_forw.grid(column = 1,row = 0, sticky="news", pady=(0,10), padx=(0,10))
-            #         if module_number < 3:
-            #             self.button_back = tk.Button(
-            #                 self.frame4, image = self.back_img, bg = self.bg, 
-            #                 state = tk.DISABLED)
-            #         else:
-            #             mCoord = self.canvas.coords('p'+str(self.id_mod[module_number-1]))
-            #             self.button_back = tk.Button(
-            #                 self.frame4, image = self.back_img, bg = self.bg, 
-            #                 command = lambda: self.select(
-            #                     mCoord[0], mCoord[1]), state = tk.NORMAL)
-            #         self.button_back.grid(column = 0,row = 0, sticky="news", pady=(0,10))
-            # else: # If user clicks on Initialiser or Output
-            #     # self.my_label.config(text = '')
-            #     for widget in self.allWeHearIs:
-            #         widget.grid_remove()
-            #     if hasattr(self, 'button_forw'):
-            #         self.button_back.config(image = self.back_img, bg = self.bg, 
-            #                 state = tk.DISABLED, text = '')
-            #         pCoord = self.canvas.coords('p'+str(self.id_mod[2]))
-            #         self.button_forw.config(image = self.forw_img, bg = self.bg, 
-            #                 command = lambda: self.select(
-            #                     pCoord[0], pCoord[1]), text = '', state = tk.NORMAL)
-
-    def finnish(self):
-        """ Calls function check_quit.
-        Before that, it checks if the current module plugins have been changed 
-        and, if so, updates their information in the XML_handler class.
-        """
-        if (self.m in self.plugin.keys()) and\
-                (self.plugin[self.m].get() != 'None') and \
-                (self.m not in self.id_done): # add
-                self.id_done.append(self.m)
-                self.s.append_plugin_to_module(self.plugin[self.m].get(),
-                                               {**self.req_settings, **self.opt_settings},
-                                                 np.array(self.module_names)[self.m == np.array(self.id_mod)][0],
-                                                 True)
-        self.check_quit()
-        
-    def display_buttons(self):
-        """ Updates the displayed radiobuttons and the description windows.
-        It loads the information corresponding to the selected module (self.m)
-        and shows the available plugins and their corresponding descriptions.
-        """
-        module = np.array(self.module_list)[self.m == np.array(self.id_mod)][0]
-        name = self.canvas.itemcget('t'+str(self.m), 'text')
-        # self.my_label.config(text = 'Choose a plugin for the '+name+' module')
-        ps = PluginSpecs()
-        plugin_list = list(ps.class_names[module].values())
-        plugin_list.append('Custom')
-        descriptions = list(ps.class_descriptions[module].values())
-        descriptions.append('User specified plugin')
-        if self.m not in self.plugin:
-            self.plugin[self.m] = tk.StringVar()
-            self.plugin[self.m].set(None)
-        self.allWeHearIs = []
-        for p, plug in enumerate(plugin_list):
-            rb = tk.Radiobutton(self.frame2, text = plug, fg = 'white', bg = self.bg,
-                height = 3, width = 20, var = self.plugin[self.m], 
-                selectcolor = 'black', value = plug,
-                font = self.controller.pages_font, command = self.optionsWindow)
-            rb.grid(column = 5+ (p%2 != 0), row = int(p/2)+1)
-            self.CreateToolTip(rb, text = descriptions[p])
-            self.allWeHearIs.append(rb)
-
-    def optionsWindow(self):
-        """ Function to create a new window displaying the available options 
-        of the selected plugin."""
-        
-        module = np.array(self.module_list)[self.m == np.array(self.id_mod)][0]
-        ps = PluginSpecs()
-        self.opt_settings = ps.optional_settings[module][self.plugin[self.m].get().lower()+'.py']
-        self.req_settings = ps.required_settings[module][self.plugin[self.m].get().lower()+'.py'] 
-        # req_settings = {'arg1': 'int', 'arg2': ['C', 'F']}
-        # opt_settings = {'arg3': 'int', 'arg4': ['C', 'F']}
-        if (len(self.opt_settings) != 0) or (len(self.req_settings) != 0):
-            if hasattr(self, 'newWindow') and (self.newWindow!= None):
-                self.newWindow.destroy()
-            self.newWindow = tk.Toplevel(self.controller)
-            # Window options
-            self.newWindow.title(self.plugin[self.m].get()+' plugin options')
-            script_dir = os.path.dirname(__file__)
-            self.tk.call('wm','iconphoto', self.newWindow, ImageTk.PhotoImage(
-                file = os.path.join(os.path.join(
-                    script_dir, 
-                    'resources', 
-                    'Assets', 
-                    'AIDIcon.ico'))))
-            self.newWindow.geometry("350x400")
-
-            frame1 = tk.Frame(self.newWindow)
-            frame4 = tk.Frame(self.newWindow)
-            
-            # Print settings
-            tk.Label(frame1,
-                  text ="Please indicate your desired options for the "+self.plugin[self.m].get()+" plugin.", anchor = tk.N, justify=tk.LEFT).pack(expand = True)
-            self.entry = []
-            # Required
-            r = 1
-            if len(self.req_settings) > 0:
-                frame2 = tk.Frame(self.newWindow)
-                tk.Label(frame2,
-                      text ="Required settings:", anchor = tk.W, justify=tk.LEFT).grid(row=0,column=0 , columnspan=2)
-                self.displaySettings(frame2, self.req_settings)
-                frame2.grid(column=0, row=r, sticky="nswe")
-                r += 1
-            # Optional
-            if len(self.opt_settings) > 0:
-                frame3 = tk.Frame(self.newWindow)
-                tk.Label(frame3,
-                      text ="Optional settings:", anchor = tk.W, justify=tk.LEFT).grid(row=0,column=0, columnspan=2)
-                self.displaySettings(frame3, self.opt_settings)
-                frame3.grid(column=0, row=r, sticky="nswe")
-                r += 1
-
-            self.entry[0].focus()
-            self.finishButton = tk.Button(
-                frame4, text = 'Finish', command = self.removewindow)
-            self.finishButton.grid(column = 1, row = r+1, sticky="es", pady=(0,10), padx=(0,10))
-            self.finishButton.bind("<Return>", lambda event: self.removewindow())
-            self.newWindow.protocol('WM_DELETE_WINDOW', self.removewindow)
-
-            frame1.grid(column=0, row=0, sticky="new")
-            frame4.grid(column=0, row=r, sticky="se")
-            self.newWindow.grid_rowconfigure(0, weight=1)
-            self.newWindow.grid_rowconfigure(tuple(range(r+1)), weight=2)
-            self.newWindow.grid_columnconfigure(0, weight=1)
-
-    def displaySettings(self, frame, settings):
-        """ Adds an entry for each input setting. Displays it in the specified row.
-        :param frame: tkinter frame type of frame
-        :param settings: dict type of plugin setting options
-        """
-        r = 1
-        for arg, val in settings.items():
-                tk.Label(frame,
-                  text = arg).grid(row=r,column=0)
-                self.entry.append(EntryWithPlaceholder(frame, val))
-                self.entry[-1].grid(row=r, column=1)
-                self.entry[-1].bind("<Return>", lambda event, a = len(self.entry): self.on_return_entry(a))
-                r += 1
-        frame.grid_rowconfigure(tuple(range(r)), weight=1)
-        frame.grid_columnconfigure(tuple(range(2)), weight=1)
-
-    def removewindow(self):
-        """ Stores settings options and closes window """
-        for e,ent in enumerate(self.entry):
-            if e < len(self.req_settings):
-                self.req_settings[list(self.req_settings.keys())[e]] = ent.get()
-            else:
-                print(self.opt_settings)
-                print(list(self.opt_settings.keys()))
-                print(e-len(self.req_settings))
-                self.opt_settings[list(self.opt_settings.keys())[e-len(self.req_settings)]] = ent.get()
-        self.newWindow.destroy()
-        self.newWindow = None
-        self.focus()
-
-    def on_return_entry(self, r):
-        """ Changes focus to the next available entry. When no more, focuses 
-        on the finish button.
-        : param r: int type of entry id.
-        """
-        if r < len(self.entry):
-            self.entry[r].focus()
-        else:
-            self.finishButton.focus()
-
-    def CreateToolTip(self, widget, text):
-        """ Calls ToolTip to create a window with a widget description. """
-        toolTip = ToolTip(widget)
-        def enter(event):
-            toolTip.showtip(text)
-        def leave(event):
-            toolTip.hidetip()
-        widget.bind('<Enter>', enter)
-        widget.bind('<Leave>', leave)
-        
+    # def on_click(self, event):
+    #     """ Passes the mouse click coordinates to the select function."""
+    #     self.select(event.x, event.y)
+
+    # def select(self, x: float, y: float):
+    #     """ 
+    #     Selects the module at the mouse location and updates the associated 
+    #     plugins as well as the colours. 
+    #     Blue means no plugin has been specified,
+    #     Orange means the module is selected.
+    #     Green means the plugin for this module is already set. 
+    #     :param x: float type of module x coordinate
+    #     :param y: float type of module y coordinate
+    #     """
+    #     self.selected = self.canvas.find_overlapping(
+    #         x-5, y-5, x+5, y+5)
+    #     if self.selected:
+    #         if len(self.selected) > 2:
+    #             self.canvas_selected = self.selected[-2]
+    #         else:
+    #             self.canvas_selected = self.selected[-1]
+
+    #     if (self.m in self.plugin.keys()) and\
+    #             (self.plugin[self.m].get() != 'None') and \
+    #             (self.m not in self.id_done):  # add
+    #         self.id_done.append(self.m)
+    #         print(np.array(self.module_names)[
+    #               self.m == np.array(self.id_mod)][0])
+    #         self.s.append_plugin_to_module(self.plugin[self.m].get(),
+    #                                        {**self.req_settings, **
+    #                                            self.opt_settings},
+    #                                        np.array(self.module_names)[
+    #             self.m == np.array(self.id_mod)][0],
+    #             True)
+    #     if self.m in self.id_done and self.m > 1:
+    #         self.canvas.itemconfig('p'+str(self.m), fill='#46da63')
+    #     else:
+    #         self.canvas.itemconfig('p'+str(self.m), fill=self.bg)
+
+    #     if len(self.canvas.gettags(self.canvas_selected)) > 0:
+    #         if not (len(self.canvas.gettags(self.canvas_selected)[0].split('-')) > 1) and\
+    #                 not (self.canvas.gettags(self.canvas_selected)[0].split('-')[0] == 'loop'):
+    #             self.m = int(self.canvas.gettags(self.canvas_selected)[0][1:])
+    #         if self.m > 1:
+    #             if self.m not in self.id_done and self.m > 1:
+    #                 self.canvas.itemconfig('p'+str(self.m), fill='#dbaa21')
+    #             for widget in self.allWeHearIs:
+    #                 widget.grid_remove()
+
+    # def finnish(self):
+    #     """ Calls function check_quit.
+    #     Before that, it checks if the current module plugins have been changed 
+    #     and, if so, updates their information in the XML_handler class.
+    #     """
+    #     if (self.m in self.plugin.keys()) and\
+    #             (self.plugin[self.m].get() != 'None') and \
+    #             (self.m not in self.id_done):  # add
+    #         self.id_done.append(self.m)
+    #         self.s.append_plugin_to_module(self.plugin[self.m].get(),
+    #                                        {**self.req_settings, **
+    #                                            self.opt_settings},
+    #                                        np.array(self.module_names)[
+    #             self.m == np.array(self.id_mod)][0],
+    #             True)
+    #     self.check_quit()
+
+    # def display_buttons(self):
+    #     """ Updates the displayed radiobuttons and the description windows.
+    #     It loads the information corresponding to the selected module (self.m)
+    #     and shows the available plugins and their corresponding descriptions.
+    #     """
+    #     module = np.array(self.module_list)[self.m == np.array(self.id_mod)][0]
+    #     name = self.canvas.itemcget('t'+str(self.m), 'text')
+    #     # self.my_label.config(text = 'Choose a plugin for the '+name+' module')
+    #     ps = PluginSpecs()
+    #     plugin_list = list(ps.class_names[module].values())
+    #     plugin_list.append('Custom')
+    #     descriptions = list(ps.class_descriptions[module].values())
+    #     descriptions.append('User specified plugin')
+    #     if self.m not in self.plugin:
+    #         self.plugin[self.m] = tk.StringVar()
+    #         self.plugin[self.m].set(None)
+    #     self.allWeHearIs = []
+    #     for p, plug in enumerate(plugin_list):
+    #         rb = tk.Radiobutton(self.frame2, text=plug, fg='white', bg=self.bg,
+    #                             height=3, width=20, var=self.plugin[self.m],
+    #                             selectcolor='black', value=plug,
+    #                             font=self.controller.pages_font, command=self.optionsWindow)
+    #         rb.grid(column=5 + (p % 2 != 0), row=int(p/2)+1)
+    #         self.CreateToolTip(rb, text=descriptions[p])
+    #         self.allWeHearIs.append(rb)
+
+    # def optionsWindow(self):
+    #     """ Function to create a new window displaying the available options 
+    #     of the selected plugin."""
+
+    #     module = np.array(self.module_list)[self.m == np.array(self.id_mod)][0]
+    #     ps = PluginSpecs()
+    #     self.opt_settings = ps.optional_settings[module][self.plugin[self.m].get(
+    #     ).lower()+'.py']
+    #     self.req_settings = ps.required_settings[module][self.plugin[self.m].get(
+    #     ).lower()+'.py']
+    #     # req_settings = {'arg1': 'int', 'arg2': ['C', 'F']}
+    #     # opt_settings = {'arg3': 'int', 'arg4': ['C', 'F']}
+    #     if (len(self.opt_settings) != 0) or (len(self.req_settings) != 0):
+    #         if hasattr(self, 'newWindow') and (self.newWindow != None):
+    #             self.newWindow.destroy()
+    #         self.newWindow = tk.Toplevel(self.controller)
+    #         # Window options
+    #         self.newWindow.title(self.plugin[self.m].get()+' plugin options')
+    #         script_dir = os.path.dirname(__file__)
+    #         self.tk.call('wm', 'iconphoto', self.newWindow, ImageTk.PhotoImage(
+    #             file=os.path.join(os.path.join(
+    #                 script_dir,
+    #                 'resources',
+    #                 'Assets',
+    #                 'AIDIcon.ico'))))
+    #         self.newWindow.geometry("350x400")
+
+    #         frame1 = tk.Frame(self.newWindow)
+    #         frame4 = tk.Frame(self.newWindow)
+
+    #         # Print settings
+    #         tk.Label(frame1,
+    #                  text="Please indicate your desired options for the "+self.plugin[self.m].get()+" plugin.", anchor=tk.N, justify=tk.LEFT).pack(expand=True)
+    #         self.entry = []
+    #         # Required
+    #         r = 1
+    #         if len(self.req_settings) > 0:
+    #             frame2 = tk.Frame(self.newWindow)
+    #             tk.Label(frame2,
+    #                      text="Required settings:", anchor=tk.W, justify=tk.LEFT).grid(row=0, column=0, columnspan=2)
+    #             self.displaySettings(frame2, self.req_settings)
+    #             frame2.grid(column=0, row=r, sticky="nswe")
+    #             r += 1
+    #         # Optional
+    #         if len(self.opt_settings) > 0:
+    #             frame3 = tk.Frame(self.newWindow)
+    #             tk.Label(frame3,
+    #                      text="Optional settings:", anchor=tk.W, justify=tk.LEFT).grid(row=0, column=0, columnspan=2)
+    #             self.displaySettings(frame3, self.opt_settings)
+    #             frame3.grid(column=0, row=r, sticky="nswe")
+    #             r += 1
+
+    #         self.entry[0].focus()
+    #         self.finishButton = tk.Button(
+    #             frame4, text='Finish', command=self.removewindow)
+    #         self.finishButton.grid(
+    #             column=1, row=r+1, sticky="es", pady=(0, 10), padx=(0, 10))
+    #         self.finishButton.bind(
+    #             "<Return>", lambda event: self.removewindow())
+    #         self.newWindow.protocol('WM_DELETE_WINDOW', self.removewindow)
+
+    #         frame1.grid(column=0, row=0, sticky="new")
+    #         frame4.grid(column=0, row=r, sticky="se")
+    #         self.newWindow.grid_rowconfigure(0, weight=1)
+    #         self.newWindow.grid_rowconfigure(tuple(range(r+1)), weight=2)
+    #         self.newWindow.grid_columnconfigure(0, weight=1)
+
+    # def displaySettings(self, frame, settings):
+    #     """ Adds an entry for each input setting. Displays it in the specified row.
+    #     :param frame: tkinter frame type of frame
+    #     :param settings: dict type of plugin setting options
+    #     """
+    #     r = 1
+    #     for arg, val in settings.items():
+    #         tk.Label(frame,
+    #                  text=arg).grid(row=r, column=0)
+    #         self.entry.append(EntryWithPlaceholder(frame, val))
+    #         self.entry[-1].grid(row=r, column=1)
+    #         self.entry[-1].bind("<Return>", lambda event,
+    #                             a=len(self.entry): self.on_return_entry(a))
+    #         r += 1
+    #     frame.grid_rowconfigure(tuple(range(r)), weight=1)
+    #     frame.grid_columnconfigure(tuple(range(2)), weight=1)
+
+    # def removewindow(self):
+    #     """ Stores settings options and closes window """
+    #     for e, ent in enumerate(self.entry):
+    #         if e < len(self.req_settings):
+    #             self.req_settings[list(self.req_settings.keys())[
+    #                 e]] = ent.get()
+    #         else:
+    #             print(self.opt_settings)
+    #             print(list(self.opt_settings.keys()))
+    #             print(e-len(self.req_settings))
+    #             self.opt_settings[list(self.opt_settings.keys())[
+    #                 e-len(self.req_settings)]] = ent.get()
+    #     self.newWindow.destroy()
+    #     self.newWindow = None
+    #     self.focus()
+
+    # def on_return_entry(self, r):
+    #     """ Changes focus to the next available entry. When no more, focuses 
+    #     on the finish button.
+    #     : param r: int type of entry id.
+    #     """
+    #     if r < len(self.entry):
+    #         self.entry[r].focus()
+    #     else:
+    #         self.finishButton.focus()
+
+    # def CreateToolTip(self, widget, text):
+    #     """ Calls ToolTip to create a window with a widget description. """
+    #     toolTip = ToolTip(widget)
+
+    #     def enter(event):
+    #         toolTip.showtip(text)
+
+    #     def leave(event):
+    #         toolTip.hidetip()
+    #     widget.bind('<Enter>', enter)
+    #     widget.bind('<Leave>', leave)
+
     def module_out(self, name):
         """ Updates the output DataFrame.
-        
+
         :param name: name of the model
         :type name: str
         """
@@ -345,19 +304,19 @@ def module_out(self, name):
             name_list.append(name + '-' + str(len(m_num)))
         else:
             name_list.append(name)
-        self.canvas.itemconfig('t'+str(self.modules), text = name_list[-1])
+        self.canvas.itemconfig('t'+str(self.modules), text=name_list[-1])
         values = self.out_data.values
         values = np.vstack((
-                    np.hstack((values, np.zeros((values.shape[0],1)))),
-                    np.zeros((1, values.shape[0]+1))))
-        self.out_data = pd.DataFrame(values, 
-                                     columns = name_list, 
-                                     index = name_list)
+            np.hstack((values, np.zeros((values.shape[0], 1)))),
+            np.zeros((1, values.shape[0]+1))))
+        self.out_data = pd.DataFrame(values,
+                                     columns=name_list,
+                                     index=name_list)
         self.module_names.append(name_list[-1])
 
-    def add_module(self,boxName: str,x: float,y:float,ini = False,out = False):
+    def add_module(self, boxName: str, x: float, y: float, ini=False, out=False):
         """ Creates a rectangular module with the corresponding text inside.
-        
+
         :param boxName: name of the model
         :type boxName: str
         :param x: float type of module x coordinate
@@ -367,68 +326,68 @@ def add_module(self,boxName: str,x: float,y:float,ini = False,out = False):
         """
         if not ini and not out:
             tag = ('o'+str(self.modules),)
-        else: #Make initialisation and output unmoveable
+        else:  # Make initialisation and output unmoveable
             tag = ('n0',)
         text_w = self.controller.pages_font.measure(boxName+'-00') + 10
         self.canvas.create_rectangle(
-            x - text_w/2 , 
-            y - self.h/2, 
-            x + text_w/2, 
-            y + self.h/2, 
-            tags = tag + ('p'+str(self.modules),), 
-            fill = self.bg, 
-            width = 3,
+            x - text_w/2,
+            y - self.h/2,
+            x + text_w/2,
+            y + self.h/2,
+            tags=tag + ('p'+str(self.modules),),
+            fill=self.bg,
+            width=3,
             # activefill = '#dbaa21'
-            )
+        )
         self.canvas.create_text(
-            x, 
-            y, 
-            font = self.controller.pages_font, 
-            text = boxName, 
-            tags = tag + ('t'+str(self.modules),), 
-            fill = '#d0d4d9', 
-            justify = tk.CENTER)
-        
+            x,
+            y,
+            font=self.controller.pages_font,
+            text=boxName,
+            tags=tag + ('t'+str(self.modules),),
+            fill='#d0d4d9',
+            justify=tk.CENTER)
+
         if not out:
             self.canvas.create_oval(
-                x - self.cr, 
-                y + self.h/2 - self.cr, 
-                x + self.cr, 
-                y + self.h/2 + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('d'+str(self.modules),))
-            
+                x - self.cr,
+                y + self.h/2 - self.cr,
+                x + self.cr,
+                y + self.h/2 + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('d'+str(self.modules),))
+
         if not ini:
             self.canvas.create_oval(
-                x - self.cr, 
-                y - self.h/2 - self.cr, 
-                x + self.cr, 
-                y - self.h/2 + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('u'+str(self.modules),))
-        
+                x - self.cr,
+                y - self.h/2 - self.cr,
+                x + self.cr,
+                y - self.h/2 + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('u'+str(self.modules),))
+
         if not out and not ini:
             self.canvas.create_oval(
-                x - text_w/2 - self.cr, 
-                y - self.cr, 
-                x - text_w/2 + self.cr, 
-                y + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('l'+str(self.modules),))
-        
+                x - text_w/2 - self.cr,
+                y - self.cr,
+                x - text_w/2 + self.cr,
+                y + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('l'+str(self.modules),))
+
             self.canvas.create_oval(
-                x + text_w/2 - self.cr, 
-                y - self.cr, 
-                x + text_w/2 + self.cr, 
-                y + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('r'+str(self.modules),))
-            
-        self.canvas.startxy.append((x, y))
+                x + text_w/2 - self.cr,
+                y - self.cr,
+                x + text_w/2 + self.cr,
+                y + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('r'+str(self.modules),))
+
+        self.canvas_startxy.append((x, y))
         self.connections[self.modules] = {}
         self.module_out(boxName)
         self.module_list.append(boxName)
@@ -437,9 +396,9 @@ def add_module(self,boxName: str,x: float,y:float,ini = False,out = False):
     def upload(self):
         """ Opens the XML file that was previously uploaded and places the 
         modules, loops and connections in the canvas."""
-        
+
         filename = self.controller.output["xml_filename"]
-        
+
         self.reset()
 
         self.s = XML_handler()
@@ -447,68 +406,73 @@ def upload(self):
         self.s._print_pretty(self.s.loaded_modules)
         modules = self.s.loaded_modules
         modout = modules['output']
-        del modules['Initialiser'], modules['output'] # They are generated when resetting
+        # They are generated when resetting
+        del modules['Initialiser'], modules['output']
         self.disp_mod = ['Initialiser', 'output']
         self.id_mod = [0, 1]
-        
+
         # Place the modules
         self.place_modules(modules)
         connect = list(modout['coordinates'][2].keys())
         for p, parent in enumerate(modout['parents']):
-                parent_id = self.id_mod[np.where(np.array(self.disp_mod) == parent)[0][0]]
-                out, ins = modout['coordinates'][2][connect[p]].split('-')
-                xout, yout, _ , _ = self.canvas.coords(out[0]+str(parent_id))
-                xins, yins, _, _ =self.canvas.coords(ins[0]+str(1))
-                self.canvas.create_line(
-                            xout + self.cr, 
-                            yout + self.cr, 
-                            xins + self.cr, 
-                            yins + self.cr,
-                            fill = "red", 
-                            arrow = tk.LAST, 
-                            tags = ('o'+str(parent_id), 
-                                  'o'+str(1), modout['coordinates'][2][connect[p]]))
-                self.out_data.iloc[int(parent_id)][1] = 1
-                self.connections[1][
-                    int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(1)
+            parent_id = self.id_mod[np.where(
+                np.array(self.disp_mod) == parent)[0][0]]
+            out, ins = modout['coordinates'][2][connect[p]].split('-')
+            xout, yout, _, _ = self.canvas.coords(out[0]+str(parent_id))
+            xins, yins, _, _ = self.canvas.coords(ins[0]+str(1))
+            self.canvas.create_line(
+                xout + self.cr,
+                yout + self.cr,
+                xins + self.cr,
+                yins + self.cr,
+                fill="red",
+                arrow=tk.LAST,
+                tags=('o'+str(parent_id),
+                      'o'+str(1), modout['coordinates'][2][connect[p]]))
+            self.out_data.iloc[int(parent_id)][1] = 1
+            self.connections[1][
+                int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(1)
         self.m = self.id_mod[2]
         x0, y0, x1, y1 = self.canvas.coords('p'+str(self.m))
         self.select(x0, y0)
 
-    def place_modules(self,modules: dict):
+    def place_modules(self, modules: dict):
         """Places the modules in the dictionary in the canvas.
         :param modules: dict type of modules in the pipeline.
         """
-        
+
         for key in [key for key, val in modules.items() if type(val) == dict]:
             if modules[key]['class'] == 'loop':
                 # Extracts numbers from string
-                l = int(''.join(map(str, list(filter(str.isdigit, modules[key]['name'])))))
+                l = int(
+                    ''.join(map(str, list(filter(str.isdigit, modules[key]['name'])))))
                 x0, y0, x1, y1 = modules[key]['coordinates']
-                self.canvas.create_rectangle(x0, y0, 
-                                             x1, y1, 
-                                             outline = '#4ff07a',
-                                             tag = 'loop-'+str(l))
-                text_w = self.controller.pages_font.measure(modules[key]['type']) + 20
+                self.canvas.create_rectangle(x0, y0,
+                                             x1, y1,
+                                             outline='#4ff07a',
+                                             tag='loop-'+str(l)) # type:ignore
+                text_w = self.controller.pages_font.measure(
+                    modules[key]['type']) + 20
                 self.canvas.create_text(
-                    x0 + text_w/2, 
-                    y0 + 20, 
-                    font = self.controller.pages_font, 
-                    text = modules[key]['type'], 
-                    tags = ('loop-'+str(l), 'type'+'-'+str(l)), 
-                    justify = tk.CENTER)
-                text_w = self.controller.pages_font.measure(modules[key]['condition']) + 20
+                    x0 + text_w/2,
+                    y0 + 20,
+                    font=self.controller.pages_font,
+                    text=modules[key]['type'],
+                    tags=('loop-'+str(l), 'type'+'-'+str(l)),
+                    justify=tk.CENTER)
+                text_w = self.controller.pages_font.measure(
+                    modules[key]['condition']) + 20
                 self.canvas.create_text(
-                    x1 - text_w/2, 
-                    y0 + 20, 
-                    font = self.controller.pages_font, 
-                    text = modules[key]['condition'], 
-                    tags = ('loop-'+str(l), 'condition'+'-'+str(l)), 
-                    justify = tk.CENTER)
+                    x1 - text_w/2,
+                    y0 + 20,
+                    font=self.controller.pages_font,
+                    text=modules[key]['condition'],
+                    tags=('loop-'+str(l), 'condition'+'-'+str(l)),
+                    justify=tk.CENTER)
                 self.loops.append({'type': modules[key]['type'],
                                    'condition': modules[key]['condition'],
-                                   'mod': [], 
-                                   'coord': (x0, y0, 
+                                   'mod': [],
+                                   'coord': (x0, y0,
                                              x1, y1)})
                 self.place_modules(modules[key])
             else:
@@ -519,24 +483,29 @@ def place_modules(self,modules: dict):
                 self.module_list[-1] = modules[key]['module_type']
                 self.id_mod.append(modules[key]['coordinates'][1])
                 connect = list(modules[key]['coordinates'][2].keys())
-                
+
                 # Connect modules
                 for p, parent in enumerate(modules[key]['parents']):
                     if not (parent[:4] == 'loop'):
-                        parent_id = self.id_mod[np.where(np.array(self.disp_mod) == parent)[0][0]]
-                        out, ins = modules[key]['coordinates'][2][connect[p]].split('-')
-                        xout, yout, _ , _ = self.canvas.coords(out[0]+str(parent_id))
-                        xins, yins, _, _ =self.canvas.coords(ins[0]+str(self.id_mod[-1]))
+                        parent_id = self.id_mod[np.where(
+                            np.array(self.disp_mod) == parent)[0][0]]
+                        out, ins = modules[key]['coordinates'][2][connect[p]].split(
+                            '-')
+                        xout, yout, _, _ = self.canvas.coords(
+                            out[0]+str(parent_id))
+                        xins, yins, _, _ = self.canvas.coords(
+                            ins[0]+str(self.id_mod[-1]))
                         self.canvas.create_line(
-                                    xout + self.cr, 
-                                    yout + self.cr, 
-                                    xins + self.cr, 
-                                    yins + self.cr,
-                                    fill = "red", 
-                                    arrow = tk.LAST, 
-                                    tags = ('o'+str(parent_id), 
-                                          'o'+str(self.id_mod[-1]), modules[key]['coordinates'][2][connect[p]]))
-                        self.out_data.iloc[int(parent_id)][int(self.id_mod[-1])] = 1
+                            xout + self.cr,
+                            yout + self.cr,
+                            xins + self.cr,
+                            yins + self.cr,
+                            fill="red",
+                            arrow=tk.LAST,
+                            tags=('o'+str(parent_id),
+                                  'o'+str(self.id_mod[-1]), modules[key]['coordinates'][2][connect[p]]))
+                        self.out_data.iloc[int(parent_id)][int(
+                            self.id_mod[-1])] = 1
                         self.connections[int(self.id_mod[-1])][
                             int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(self.id_mod[-1])
                     else:
@@ -545,38 +514,37 @@ def place_modules(self,modules: dict):
 
     def reset(self):
         """ Resets the canvas and the stored information."""
-        self.canvas.delete(tk.ALL) # Reset canvas
-        
-        if hasattr(self, 'newWindow') and (self.newWindow!= None):
+        self.canvas.delete(tk.ALL)  # Reset canvas
+
+        if hasattr(self, 'newWindow') and (self.newWindow != None):
             self.newWindow.destroy()
-            
-        self.canvas.startxy = []
+
+        self.canvas_startxy = []
         self.out_data = pd.DataFrame()
         self.connections = {}
         self.modules = 0
         self.module_list = []
         self.module_names = []
-        
-        self.add_module('Initialiser', self.width/2, self.h, ini = True)
-        self.add_module('Output', self.width/2, self.height - self.h, out = True)
-    
+
+        self.add_module('Initialiser', self.width/2, self.h, ini=True)
+        self.add_module('Output', self.width/2, self.height - self.h, out=True)
+
         self.draw = False
         self.loops = []
         self.drawLoop = False
         self.l = 0
-        self.id_done = [0,1]
+        self.id_done = [0, 1]
         self.plugin = {}
         for widget in self.allWeHearIs:
-                widget.grid_remove()
+            widget.grid_remove()
         self.allWeHearIs = []
         # self.my_label.config(text = '')
 
-            
     def check_quit(self):
         """Checks if all plugins are specified to reset and go back to the startpage"""
         if not len(self.id_done) == len(self.id_mod):
             response = messagebox.askokcancel(
-                "Exit", 
+                "Exit",
                 "There are some unspecified plugins. Are you sure you want to leave?")
             if response:
                 self.reset()
@@ -596,9 +564,10 @@ def check_quit(self):
             self.controller.Plugin.set(True)
             self.controller._show_frame("MainPage")
 
+
 class ToolTip(object):
     """ Defines a text window associated to a widget with a description. """
-    
+
     def __init__(self, widget):
         self.widget = widget
         self.tipwindow = None
@@ -617,8 +586,8 @@ def showtip(self, text):
         tw.wm_overrideredirect(1)
         tw.wm_geometry("+%d+%d" % (x, y))
         label = tk.Label(tw, text=self.text, justify=tk.LEFT,
-                      background="#d0d4d9", relief=tk.SOLID, borderwidth=1,
-                      font=("Helvetica", "8", "normal"))
+                         background="#d0d4d9", relief=tk.SOLID, borderwidth=1,
+                         font=("Helvetica", "8", "normal"))
         label.pack(ipadx=1)
 
     def hidetip(self):
@@ -627,8 +596,10 @@ def hidetip(self):
         if tw:
             tw.destroy()
 
+
 class EntryWithPlaceholder(tk.Entry):
     """ Defines an entry with a placeholder text displayed if not focused on."""
+
     def __init__(self, master=None, placeholder="PLACEHOLDER", color='grey'):
         super().__init__(master)
 
@@ -653,7 +624,3 @@ def foc_in(self, *args):
     def foc_out(self, *args):
         if not self.get():
             self.put_placeholder()
-
-if __name__ == "__main__":
-    app = pluginCanvas()
-    app.mainloop()
diff --git a/aidesign/utils/plugins/pluginCanvas.py b/aidesign/utils/plugins/pluginCanvas.py
index 019c748a..2bb94980 100644
--- a/aidesign/utils/plugins/pluginCanvas.py
+++ b/aidesign/utils/plugins/pluginCanvas.py
@@ -1,122 +1,137 @@
-# Import the required libraries
-import tkinter as tk
-from PIL import Image, ImageTk
+from aidesign.Data.xml_handler import XML_handler
+from aidesign._plugin_helpers import PluginSpecs
+from aidesign._import_helper import get_lib_parent_dir
+
 import os
 import numpy as np
 import pandas as pd
+
+from typing import Dict, List
+from PIL import Image, ImageTk
+import tkinter as tk
 from tkinter import messagebox
 
-from aidesign.Data.xml_handler import XML_handler
-from aidesign.utils.plugin_helpers import PluginSpecs
 
-_PLUGIN_READABLE_NAMES = {"plugin_canvas":"default","pluginCanvas":"alias","plugin canvas":"alias"}
+_PLUGIN_READABLE_NAMES = {"plugin_canvas": "default",
+                          "pluginCanvas": "alias",
+                          "plugin canvas": "alias"}         # type:ignore
 _PLUGIN_MODULE_OPTIONS = {"layer_priority": 2,
-                            "required_children": None}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {}
+                          "required_children": None}        # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                              # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                              # type:ignore
+
 
 class pluginCanvas(tk.Frame):
     """Canvas for graphical specification of plugins"""
-    
-    def __init__(self, parent, controller, config:dict):
-        
+
+    def __init__(self, parent, controller, config: dict):
         """ Here we define the frame displayed for the plugin specification."""
-        
-        super().__init__(parent, bg = parent['bg'])
+
+        super().__init__(parent, bg=parent['bg'])
         self.bg = parent['bg']
         self.parent = parent
         self.controller = controller
         self.s = XML_handler()
-        
-        script_dir = os.path.dirname(__file__)
-        self.tk.call('wm','iconphoto', self.controller._w, ImageTk.PhotoImage(
-            file = os.path.join(os.path.join(
-                script_dir, 
-                'resources', 
-                'Assets', 
+
+        script_dir = get_lib_parent_dir()
+        self.tk.call('wm', 'iconphoto', self.controller._w, ImageTk.PhotoImage(
+            file=os.path.join(os.path.join(
+                script_dir,
+                'utils',
+                'resources',
+                'Assets',
                 'AIDIcon.ico'))))
-        self.grid_rowconfigure(tuple(range(5)), weight=1)
+        # self.grid_rowconfigure(tuple(range(5)), weight=1)
+        self.grid_rowconfigure(4, weight=1)
         self.grid_columnconfigure(0, weight=1)
-        
-        frame1 = tk.Frame(self, bg = self.bg)
-        self.frame2 = tk.Frame(self, bg = self.bg)
-        self.framex = tk.Frame(self, bg = self.bg)
-        frame3 = tk.Frame(self, bg = self.bg)
-        self.frame4 = tk.Frame(self, bg = self.bg)
-        
+
+        frame1 = tk.Frame(self, bg=self.bg)
+        self.frame2 = tk.Frame(self, bg=self.bg)
+        self.framex = tk.Frame(self, bg=self.bg)
+        frame3 = tk.Frame(self, bg=self.bg)
+        self.frame4 = tk.Frame(self, bg=self.bg)
+
         # Create canvas
         self.width, self.height = 600, 600
-        self.canvas = tk.Canvas(frame1, width=self.width, 
-            height=self.height, background="white")
-        self.canvas.pack(fill = tk.BOTH, expand = True, padx=(10,0), pady=10)
-        
+        self.canvas = tk.Canvas(frame1, width=self.width,
+                                height=self.height, background="white")
+        self.canvas.pack(fill=tk.BOTH, expand=True, padx=(10, 0), pady=10)
+
+        self.m: int
         self.w, self.h = 100, 50
         self.cr = 4
         self.canvas.bind('<Button-1>', self.on_click)
-        self.id_done = [0,1]
-        self.id_mod = [0,1]
-        self.plugin = {}
-        self.dataType = {}
-        self.allWeHearIs = []
-        
-        self.my_label = tk.Label(self.frame2, 
-                    text = 
-                    '',
-                    pady= 10,
-                    font = self.controller.title_font,
-                    bg = self.bg,
-                    fg = 'white',
-                    anchor = tk.CENTER)
-        self.my_label.grid(column = 5,
-                            row = 0, columnspan = 2, padx=10)
-        
+        self.id_done = [0, 1]
+        self.id_mod = [0, 1]
+        self.out_data = pd.DataFrame()
+        self.plugin: Dict[int, tk.StringVar] = {}
+        self.dataType: Dict[int, tk.StringVar] = {}
+        self.allWeHearIs: List[tk.Radiobutton] = []
+
+        self.my_label = tk.Label(self.frame2,
+                                 text='',
+                                 pady=10,
+                                 font=self.controller.title_font,
+                                 bg=self.bg,
+                                 fg='white',
+                                 anchor=tk.CENTER)
+        self.my_label.grid(column=5,
+                           row=0, columnspan=2, padx=10)
+
         self.back_img = ImageTk.PhotoImage(Image.open(
             os.path.join(script_dir,
-                         'resources', 
-                         'Assets', 
+                         'utils',
+                         'resources',
+                         'Assets',
                          'back_arrow.png')).resize((140, 60)))
         self.forw_img = ImageTk.PhotoImage(Image.open(
             os.path.join(script_dir,
-                         'resources', 
+                         'utils',
+                         'resources',
                          'Assets',
                          'forw_arrow.png')).resize((140, 60)))
-    
+
         tk.Button(
-            frame3, text = 'Load Pipeline', fg = 'white', bg = parent['bg'], 
-            height = 3, width = 15, font = self.controller.pages_font, 
-            command = self.upload).grid(column = 0, row = 26, sticky="news", 
-                                        padx=(10,0), pady=(0,10))
+            frame3, text='Load Pipeline', fg='white', bg=parent['bg'],
+            height=3, width=15, font=self.controller.pages_font,
+            command=self.upload).grid(column=0, row=26, sticky="news",
+                                      padx=(10, 0), pady=(0, 10))
         tk.Button(
-            frame3, text = 'Back to main', fg = 'white', bg = parent['bg'], 
-            height = 3, width = 15, font = self.controller.pages_font, 
-            command = self.check_quit).grid(column = 1, row = 26, sticky="news", pady=(0,10))
-        
-        self.frame_canvas = tk.Canvas(self.framex, bg = self.bg, bd = 0, highlightthickness=0)
-        self.tree_scrolly = tk.Scrollbar(self.framex, command=self.frame_canvas.yview)
-        self.tree_scrolly.pack(side=tk.RIGHT, fill = tk.Y)
-        self.tree_scrollx = tk.Scrollbar(self.framex, command=self.frame_canvas.xview, orient = 'horizontal')
-        self.tree_scrollx.pack(side=tk.BOTTOM, fill = tk.X)
-        
+            frame3, text='Back to main', fg='white', bg=parent['bg'],
+            height=3, width=15, font=self.controller.pages_font,
+            command=self.check_quit).grid(column=1, row=26, sticky="news", pady=(0, 10))
+
+        self.frame_canvas = tk.Canvas(
+            self.framex, bg=self.bg, bd=0, highlightthickness=0)
+        self.tree_scrolly = tk.Scrollbar(
+            self.framex, command=self.frame_canvas.yview)
+        self.tree_scrolly.pack(side=tk.RIGHT, fill=tk.Y)
+        self.tree_scrollx = tk.Scrollbar(
+            self.framex, command=self.frame_canvas.xview, orient='horizontal')
+        self.tree_scrollx.pack(side=tk.BOTTOM, fill=tk.X)
+
         self.save_path = ''
         self.saved = True
         frame1.grid(column=0, row=0, rowspan=5, sticky="nsew")
         self.frame2.grid(column=1, row=0, sticky="new")
         frame3.grid(column=0, row=5, sticky="swe")
         self.frame4.grid(column=1, row=5, sticky="sew")
-        
+
         # self.frame2.grid_columnconfigure(tuple(range(2)), weight=1)
-        frame3.grid_columnconfigure(tuple(range(2)), weight=2)
-        self.frame4.grid_columnconfigure(tuple(range(2)), weight=2)
+        self.frame2.grid_columnconfigure(1, weight=1)
+        # frame3.grid_columnconfigure(tuple(range(2)), weight=2)
+        frame3.grid_columnconfigure(1, weight=2)
+        self.frame4.grid_columnconfigure(1, weight=2)
 
-    def class_list(self,value):
+    def class_list(self, value):
         """ Temporary fix """
         return value
 
     def on_click(self, event):
         """ Passes the mouse click coordinates to the select function."""
         self.select(event.x, event.y)
-        
-    def select(self, x: float, y:float):
+
+    def select(self, x: float, y: float):
         """ 
         Selects the module at the mouse location and updates the associated 
         plugins as well as the colours. 
@@ -128,93 +143,102 @@ def select(self, x: float, y:float):
         """
         self.selected = self.canvas.find_overlapping(
             x-5, y-5, x+5, y+5)
+        self.canvas_selected: int
         if self.selected:
             if len(self.selected) > 2:
-                self.canvas.selected = self.selected[-2]
+                self.canvas_selected = self.selected[-2]
             else:
-                self.canvas.selected = self.selected[-1]
-        
+                self.canvas_selected = self.selected[-1]
+
         if (self.m in self.plugin.keys()) and\
                 (self.plugin[self.m].get() != 'None') and \
-                (self.m not in self.id_done): # add
-                self.id_done.append(self.m)
-                # print(np.array(self.module_names)[self.m == np.array(self.id_mod)][0])
-                self.s.append_plugin_to_module(self.plugin[self.m].get(),
-                                                 {**self.req_settings, **self.opt_settings},
-                                                 np.array(self.module_names)[self.m == np.array(self.id_mod)][0],
-                                                 True)
+                (self.m not in self.id_done):  # add
+            self.id_done.append(self.m)
+            # print(np.array(self.module_names)[self.m == np.array(self.id_mod)][0])
+            self.s.append_plugin_to_module(self.plugin[self.m].get(),
+                                           {**self.req_settings, **
+                                               self.opt_settings},
+                                           np.array(self.module_names)[
+                self.m == np.array(self.id_mod)][0],
+                True)
         if self.m in self.id_done and self.m > 1:
-            self.canvas.itemconfig('p'+str(self.m), fill = '#46da63')
+            self.canvas.itemconfig('p'+str(self.m), fill='#46da63')
         else:
-            self.canvas.itemconfig('p'+str(self.m), fill = self.bg)
-        
-        if len(self.canvas.gettags(self.canvas.selected)) > 0:
-            if not (len(self.canvas.gettags(self.canvas.selected)[0].split('-')) > 1) and\
-                not (self.canvas.gettags(self.canvas.selected)[0].split('-')[0] == 'loop'):
-                self.m = int(self.canvas.gettags(self.canvas.selected)[0][1:])
+            self.canvas.itemconfig('p'+str(self.m), fill=self.bg)
+
+        if len(self.canvas.gettags(self.canvas_selected)) > 0:
+            if not (len(self.canvas.gettags(self.canvas_selected)[0].split('-')) > 1) and\
+                    not (self.canvas.gettags(self.canvas_selected)[0].split('-')[0] == 'loop'):
+                self.m = int(self.canvas.gettags(self.canvas_selected)[0][1:])
             if self.m > 1:
                 if self.m not in self.id_done and self.m > 1:
-                    self.canvas.itemconfig('p'+str(self.m), fill = '#dbaa21')
+                    self.canvas.itemconfig('p'+str(self.m), fill='#dbaa21')
                 for widget in self.allWeHearIs:
                     widget.grid_remove()
-                
+
                 self.display_buttons()
                 module_number = self.id_mod.index(self.m)
-                if hasattr(self, 'button_forw'):
+                if not hasattr(self, 'button_forw'):
                     if module_number == len(self.id_mod)-1:
-                        self.button_forw.config(text = 'Finish', bg = self.bg, 
-                            font = self.controller.pages_font,
-                            fg = 'white', height = 3, width = 15,
-                            command = self.finnish, state = tk.NORMAL, image = '')
+                        self.button_forw = tk.Button(
+                            self.frame4, text='Finish', bg=self.bg,
+                            font=self.controller.pages_font,
+                            fg='white', height=3, width=15,
+                            command=self.finnish, state=tk.NORMAL, image='')
                     else:
-                        pCoord = self.canvas.coords('p'+str(self.id_mod[module_number+1]))
-                        self.button_forw.config(image = self.forw_img, bg = self.bg, 
-                            command = lambda: self.select(
-                                pCoord[0], pCoord[1]), text = '', state = tk.NORMAL)
+                        pCoord = self.canvas.coords(
+                            'p'+str(self.id_mod[module_number+1]))
+                        self.button_forw = tk.Button(
+                            self.frame4, image=self.forw_img, bg=self.bg,
+                            command=lambda: self.select(
+                                pCoord[0], pCoord[1]), state=tk.NORMAL)
+                    self.button_forw.grid(
+                        column=1, row=0, sticky="news", pady=(0, 10), padx=(0, 10))
                     if module_number < 3:
-                        self.button_back.config(image = self.back_img, bg = self.bg, 
-                            state = tk.DISABLED, text = '')
+                        self.button_back = tk.Button(
+                            self.frame4, image=self.back_img, bg=self.bg,
+                            state=tk.DISABLED)
                     else:
-                        mCoord = self.canvas.coords('p'+str(self.id_mod[module_number-1]))
-                        self.button_back.config(image = self.back_img, bg = self.bg, 
-                            command = lambda: self.select(
-                                mCoord[0], mCoord[1]), text = '', state = tk.NORMAL)
+                        mCoord = self.canvas.coords(
+                            'p'+str(self.id_mod[module_number-1]))
+                        self.button_back = tk.Button(
+                            self.frame4, image=self.back_img, bg=self.bg,
+                            command=lambda: self.select(
+                                mCoord[0], mCoord[1]), state=tk.NORMAL)
+                    self.button_back.grid(
+                        column=0, row=0, sticky="news", pady=(0, 10))
                 else:
                     if module_number == len(self.id_mod)-1:
-                        self.button_forw = tk.Button(
-                            self.frame4, text = 'Finish', bg = self.bg, 
-                            font = self.controller.pages_font,
-                            fg = 'white', height = 3, width = 15,
-                            command = self.finnish, state = tk.NORMAL, image = '')
+                        self.button_forw.config(text='Finish', bg=self.bg,
+                                                font=self.controller.pages_font,
+                                                fg='white', height=3, width=15,
+                                                command=self.finnish, state=tk.NORMAL, image='')
                     else:
-                        pCoord = self.canvas.coords('p'+str(self.id_mod[module_number+1]))
-                        self.button_forw = tk.Button(
-                            self.frame4, image = self.forw_img, bg = self.bg, 
-                            command = lambda: self.select(
-                                pCoord[0], pCoord[1]), state = tk.NORMAL)
-                    self.button_forw.grid(column = 1,row = 0, sticky="news", pady=(0,10), padx=(0,10))
+                        pCoord = self.canvas.coords(
+                            'p'+str(self.id_mod[module_number+1]))
+                        self.button_forw.config(image=self.forw_img, bg=self.bg,
+                                                command=lambda: self.select(
+                                                    pCoord[0], pCoord[1]), text='', state=tk.NORMAL)
                     if module_number < 3:
-                        self.button_back = tk.Button(
-                            self.frame4, image = self.back_img, bg = self.bg, 
-                            state = tk.DISABLED)
+                        self.button_back.config(image=self.back_img, bg=self.bg,
+                                                state=tk.DISABLED, text='')
                     else:
-                        mCoord = self.canvas.coords('p'+str(self.id_mod[module_number-1]))
-                        self.button_back = tk.Button(
-                            self.frame4, image = self.back_img, bg = self.bg, 
-                            command = lambda: self.select(
-                                mCoord[0], mCoord[1]), state = tk.NORMAL)
-                    self.button_back.grid(column = 0,row = 0, sticky="news", pady=(0,10))
-            else: # If user clicks on Initialiser or Output
-                self.my_label.config(text = '')
+                        mCoord = self.canvas.coords(
+                            'p'+str(self.id_mod[module_number-1]))
+                        self.button_back.config(image=self.back_img, bg=self.bg,
+                                                command=lambda: self.select(
+                                                    mCoord[0], mCoord[1]), text='', state=tk.NORMAL)
+            else:  # If user clicks on Initialiser or Output
+                self.my_label.config(text='')
                 for widget in self.allWeHearIs:
                     widget.grid_remove()
                 if hasattr(self, 'button_forw'):
-                    self.button_back.config(image = self.back_img, bg = self.bg, 
-                            state = tk.DISABLED, text = '')
+                    self.button_back.config(image=self.back_img, bg=self.bg,
+                                            state=tk.DISABLED, text='')
                     pCoord = self.canvas.coords('p'+str(self.id_mod[2]))
-                    self.button_forw.config(image = self.forw_img, bg = self.bg, 
-                            command = lambda: self.select(
-                                pCoord[0], pCoord[1]), text = '', state = tk.NORMAL)
+                    self.button_forw.config(image=self.forw_img, bg=self.bg,
+                                            command=lambda: self.select(
+                                                pCoord[0], pCoord[1]), text='', state=tk.NORMAL)
 
     def finnish(self):
         """ Calls function check_quit.
@@ -223,14 +247,16 @@ def finnish(self):
         """
         if (self.m in self.plugin.keys()) and\
                 (self.plugin[self.m].get() != 'None') and \
-                (self.m not in self.id_done): # add
-                self.id_done.append(self.m)
-                self.s.append_plugin_to_module(self.plugin[self.m].get(),
-                                               {**self.req_settings, **self.opt_settings},
-                                                 np.array(self.module_names)[self.m == np.array(self.id_mod)][0],
-                                                 True)
+                (self.m not in self.id_done):  # add
+            self.id_done.append(self.m)
+            self.s.append_plugin_to_module(self.plugin[self.m].get(),
+                                           {**self.req_settings, **
+                                               self.opt_settings},
+                                           np.array(self.module_names)[
+                self.m == np.array(self.id_mod)][0],
+                True)
         self.check_quit()
-        
+
     def display_buttons(self):
         """ Updates the displayed radiobuttons and the description windows.
         It loads the information corresponding to the selected module (self.m)
@@ -243,89 +269,97 @@ def display_buttons(self):
         plugin_list.append('Custom')
         descriptions = list(ps.class_descriptions[module].values())
         descriptions.append('User specified plugin')
-        type_list = np.array([x.get('Type', None) for x in list(ps.module_options[module].values())])
+        type_list = np.array([x.get('Type', None)
+                             for x in list(ps.module_options[module].values())])
         type_list = np.append(type_list, None)
         text = ''
         if module.lower() == 'modelling':
             text = '\nPluggins in white correspond to '+self.controller.output_type
-        self.my_label.config(text = 'Choose a plugin for the '+name+' module'+text)
+        self.my_label.config(
+            text='Choose a plugin for the '+name+' module'+text)
         if self.m not in self.plugin:
             self.plugin[self.m] = tk.StringVar()
             self.plugin[self.m].set(None)
         self.allWeHearIs = []
-        
+
         framexx = []
         framexx_name = np.array([])
         framexx_i = []
-        values = np.append(np.unique(type_list[type_list != None]), 'other') if not any(type_list == 'other') else np.unique(type_list[type_list != None])
-        for i,t in enumerate(values):
-            framexx.append(tk.Frame(self.framep, bg = self.bg))
-            framexx_name = np.append(framexx_name, t if t is not None else 'other')
+        values = np.append(np.unique(type_list[type_list != None]), 'other') if not any(
+            type_list == 'other') else np.unique(type_list[type_list != None])
+        for i, t in enumerate(values):
+            framexx.append(tk.Frame(self.framep, bg=self.bg))
+            framexx_name = np.append(
+                framexx_name, t if t is not None else 'other')
             framexx_i.append(0)
-            label = tk.Label(framexx[-1], 
-                        text = framexx_name[-1].capitalize(),
-                        pady= 10,
-                        font = self.controller.title_font,
-                        bg = self.bg,
-                        fg = 'white')
-            label.grid(column = 0,
-                                row = 0, padx = (10,0), sticky = 'nw')
-            framexx[-1].grid(column = 0, row = i, sticky="nsew")
+            label = tk.Label(framexx[-1],
+                             text=framexx_name[-1].capitalize(),
+                             pady=10,
+                             font=self.controller.title_font,
+                             bg=self.bg,
+                             fg='white')
+            label.grid(column=0,
+                       row=0, padx=(10, 0), sticky='nw')
+            framexx[-1].grid(column=0, row=i, sticky="nsew")
         unsupervised = ['clustering', 'RL']
         for p, plug in enumerate(plugin_list):
-            if module.lower() == 'modelling'and p < len(plugin_list)-1:
-                colour = 'white' if type_list[p] == self.controller.output_type or (self.controller.output_type == 'unsupervised' and type_list[p] in unsupervised) else 'grey'
+            if module.lower() == 'modelling' and p < len(plugin_list)-1:
+                colour = 'white' if type_list[p] == self.controller.output_type or (
+                    self.controller.output_type == 'unsupervised' and type_list[p] in unsupervised) else 'grey'
             else:
                 colour = 'white'
-            frame_idx = np.where(framexx_name == type_list[p])[0][0] if type_list[p] in framexx_name else np.where(framexx_name == 'other')[0][0]
-            rb = tk.Radiobutton(framexx[frame_idx], text = plug, fg = colour, bg = self.bg,
-                height = 3, var = self.plugin[self.m], 
-                selectcolor = 'black', value = plug, justify = tk.LEFT,
-                font = self.controller.pages_font, command = self.optionsWindow)
-            rb.grid(column = int(framexx_i[frame_idx]%2 != 0), 
-                    row = int(framexx_i[frame_idx]/2)+1, 
-                    sticky = 'w',
+            frame_idx = np.where(framexx_name == type_list[p])[
+                0][0] if type_list[p] in framexx_name else np.where(framexx_name == 'other')[0][0]
+            rb = tk.Radiobutton(framexx[frame_idx], text=plug, fg=colour, bg=self.bg,
+                                height=3, var=self.plugin[self.m],
+                                selectcolor='black', value=plug, justify=tk.LEFT,
+                                font=self.controller.pages_font, command=self.optionsWindow)
+            rb.grid(column=int(framexx_i[frame_idx] % 2 != 0),
+                    row=int(framexx_i[frame_idx]/2)+1,
+                    sticky='w',
                     padx=(10, 0))
             framexx_i[frame_idx] += 1
-            self.CreateToolTip(rb, text = descriptions[p])
+            self.CreateToolTip(rb, text=descriptions[p])
             self.allWeHearIs.append(rb)
 
     def optionsWindow(self):
         """ Function to create a new window displaying the available options 
         of the selected plugin."""
-        
+
         module = np.array(self.module_list)[self.m == np.array(self.id_mod)][0]
         ps = PluginSpecs()
-        self.opt_settings = ps.optional_settings[module][self.plugin[self.m].get()]
-        self.req_settings = ps.required_settings[module][self.plugin[self.m].get()] 
+        self.opt_settings = ps.optional_settings[module][self.plugin[self.m].get(
+        )]
+        self.req_settings = ps.required_settings[module][self.plugin[self.m].get(
+        )]
         if (len(self.opt_settings) != 0) or (len(self.req_settings) != 0):
-            if hasattr(self, 'newWindow') and (self.newWindow!= None):
+            if hasattr(self, 'newWindow') and (self.newWindow != None):
                 self.newWindow.destroy()
             self.newWindow = tk.Toplevel(self.controller)
             # Window options
             self.newWindow.title(self.plugin[self.m].get()+' plugin options')
             script_dir = os.path.dirname(__file__)
-            self.tk.call('wm','iconphoto', self.newWindow, ImageTk.PhotoImage(
-                file = os.path.join(os.path.join(
-                    script_dir, 
-                    'resources', 
-                    'Assets', 
+            self.tk.call('wm', 'iconphoto', self.newWindow, ImageTk.PhotoImage(
+                file=os.path.join(os.path.join(
+                    script_dir,
+                    'resources',
+                    'Assets',
                     'AIDIcon.ico'))))
             self.newWindow.geometry("350x400")
 
             frame1 = tk.Frame(self.newWindow)
             frame4 = tk.Frame(self.newWindow)
-            
+
             # Print settings
             tk.Label(frame1,
-                  text ="Please indicate your desired options for the "+self.plugin[self.m].get()+" plugin.", anchor = tk.N, justify=tk.LEFT).pack(expand = True)
+                     text="Please indicate your desired options for the "+self.plugin[self.m].get()+" plugin.", anchor=tk.N, justify=tk.LEFT).pack(expand=True)
             self.entry = []
             # Required
             r = 1
             if len(self.req_settings) > 0:
                 frame2 = tk.Frame(self.newWindow)
                 tk.Label(frame2,
-                      text ="Required settings:", anchor = tk.W, justify=tk.LEFT).grid(row=0,column=0 , columnspan=2)
+                         text="Required settings:", anchor=tk.W, justify=tk.LEFT).grid(row=0, column=0, columnspan=2)
                 self.displaySettings(frame2, self.req_settings)
                 frame2.grid(column=0, row=r, sticky="nswe")
                 r += 1
@@ -333,16 +367,18 @@ def optionsWindow(self):
             if len(self.opt_settings) > 0:
                 frame3 = tk.Frame(self.newWindow)
                 tk.Label(frame3,
-                      text ="Optional settings:", anchor = tk.W, justify=tk.LEFT).grid(row=0,column=0, columnspan=2)
+                         text="Optional settings:", anchor=tk.W, justify=tk.LEFT).grid(row=0, column=0, columnspan=2)
                 self.displaySettings(frame3, self.opt_settings)
                 frame3.grid(column=0, row=r, sticky="nswe")
                 r += 1
             if len(self.entry) > 0:
                 self.entry[0].focus()
             self.finishButton = tk.Button(
-                frame4, text = 'Finish', command = self.removewindow)
-            self.finishButton.grid(column = 1, row = r+1, sticky="es", pady=(0,10), padx=(0,10))
-            self.finishButton.bind("<Return>", lambda event: self.removewindow())
+                frame4, text='Finish', command=self.removewindow)
+            self.finishButton.grid(
+                column=1, row=r+1, sticky="es", pady=(0, 10), padx=(0, 10))
+            self.finishButton.bind(
+                "<Return>", lambda event: self.removewindow())
             self.newWindow.protocol('WM_DELETE_WINDOW', self.removewindow)
 
             frame1.grid(column=0, row=0, sticky="new")
@@ -359,23 +395,24 @@ def displaySettings(self, frame, settings):
         r = 1
         for arg, val in settings.items():
             tk.Label(frame,
-              text = arg).grid(row=r,column=0)
+                     text=arg).grid(row=r, column=0)
             if arg == 'Data':
                 if self.m not in self.dataType:
                     self.dataType[self.m] = tk.StringVar()
                     self.dataType[self.m].set('X')
-                tk.Radiobutton(frame, text = 'Input (X)', fg = 'black', 
-                               var = self.dataType[self.m], value = 'X'
+                tk.Radiobutton(frame, text='Input (X)', fg='black',
+                               var=self.dataType[self.m], value='X'
                                ).grid(row=r, column=1)
                 if len(self.s._get_all_elements_with_tag("Y")) > 0 or \
-                    len(self.s._get_all_elements_with_tag("Y_test")) > 0:
-                    tk.Radiobutton(frame, text = 'Output (Y)', fg = 'black', 
-                                   var = self.dataType[self.m], value = 'Y'
+                        len(self.s._get_all_elements_with_tag("Y_test")) > 0:
+                    tk.Radiobutton(frame, text='Output (Y)', fg='black',
+                                   var=self.dataType[self.m], value='Y'
                                    ).grid(row=r, column=2)
             else:
                 self.entry.append(EntryWithPlaceholder(frame, val))
                 self.entry[-1].grid(row=r, column=1)
-                self.entry[-1].bind("<Return>", lambda event, a = len(self.entry): self.on_return_entry(a))
+                self.entry[-1].bind("<Return>", lambda event,
+                                    a=len(self.entry): self.on_return_entry(a))
             r += 1
         frame.grid_rowconfigure(tuple(range(r)), weight=1)
         frame.grid_columnconfigure(tuple(range(2)), weight=1)
@@ -385,7 +422,7 @@ def removewindow(self):
         self.req_settings.pop("Data", None)
         req_keys = list(self.req_settings.keys())
         opt_keys = list(self.opt_settings.keys())
-        for e,ent in enumerate(self.entry):
+        for e, ent in enumerate(self.entry):
             if e < len(self.req_settings):
                 if ent.get() == self.entry[e].placeholder or len(ent.get()) == 0:
                     self.req_settings.pop(req_keys[e], None)
@@ -418,16 +455,18 @@ def on_return_entry(self, r):
     def CreateToolTip(self, widget, text):
         """ Calls ToolTip to create a window with a widget description. """
         toolTip = ToolTip(widget)
+
         def enter(event):
             toolTip.showtip(text)
+
         def leave(event):
             toolTip.hidetip()
         widget.bind('<Enter>', enter)
         widget.bind('<Leave>', leave)
-        
+
     def module_out(self, name):
         """ Updates the output DataFrame.
-        
+
         :param name: name of the model
         :type name: str
         """
@@ -437,19 +476,19 @@ def module_out(self, name):
             name_list.append(name + '-' + str(len(m_num)))
         else:
             name_list.append(name)
-        self.canvas.itemconfig('t'+str(self.modules), text = name_list[-1])
+        self.canvas.itemconfig('t'+str(self.modules), text=name_list[-1])
         values = self.out_data.values
         values = np.vstack((
-                    np.hstack((values, np.zeros((values.shape[0],1)))),
-                    np.zeros((1, values.shape[0]+1))))
-        self.out_data = pd.DataFrame(values, 
-                                     columns = name_list, 
-                                     index = name_list)
+            np.hstack((values, np.zeros((values.shape[0], 1)))),
+            np.zeros((1, values.shape[0]+1))))
+        self.out_data = pd.DataFrame(values,
+                                     columns=name_list,
+                                     index=name_list)
         self.module_names.append(name_list[-1])
 
-    def add_module(self,boxName: str,x: float,y:float,ini = False,out = False):
+    def add_module(self, boxName: str, x: float, y: float, ini=False, out=False):
         """ Creates a rectangular module with the corresponding text inside.
-        
+
         :param boxName: name of the model
         :type boxName: str
         :param x: float type of module x coordinate
@@ -459,68 +498,68 @@ def add_module(self,boxName: str,x: float,y:float,ini = False,out = False):
         """
         if not ini and not out:
             tag = ('o'+str(self.modules),)
-        else: #Make initialisation and output unmoveable
+        else:  # Make initialisation and output unmoveable
             tag = ('n0',)
         text_w = self.controller.pages_font.measure(boxName+'-00') + 10
         self.canvas.create_rectangle(
-            x - text_w/2 , 
-            y - self.h/2, 
-            x + text_w/2, 
-            y + self.h/2, 
-            tags = tag + ('p'+str(self.modules),), 
-            fill = self.bg, 
-            width = 3,
+            x - text_w/2,
+            y - self.h/2,
+            x + text_w/2,
+            y + self.h/2,
+            tags=tag + ('p'+str(self.modules),),
+            fill=self.bg,
+            width=3,
             # activefill = '#dbaa21'
-            )
+        )
         self.canvas.create_text(
-            x, 
-            y, 
-            font = self.controller.pages_font, 
-            text = boxName, 
-            tags = tag + ('t'+str(self.modules),), 
-            fill = '#d0d4d9', 
-            justify = tk.CENTER)
-        
+            x,
+            y,
+            font=self.controller.pages_font,
+            text=boxName,
+            tags=tag + ('t'+str(self.modules),),
+            fill='#d0d4d9',
+            justify=tk.CENTER)
+
         if not out:
             self.canvas.create_oval(
-                x - self.cr, 
-                y + self.h/2 - self.cr, 
-                x + self.cr, 
-                y + self.h/2 + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('d'+str(self.modules),))
-            
+                x - self.cr,
+                y + self.h/2 - self.cr,
+                x + self.cr,
+                y + self.h/2 + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('d'+str(self.modules),))
+
         if not ini:
             self.canvas.create_oval(
-                x - self.cr, 
-                y - self.h/2 - self.cr, 
-                x + self.cr, 
-                y - self.h/2 + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('u'+str(self.modules),))
-        
+                x - self.cr,
+                y - self.h/2 - self.cr,
+                x + self.cr,
+                y - self.h/2 + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('u'+str(self.modules),))
+
         if not out and not ini:
             self.canvas.create_oval(
-                x - text_w/2 - self.cr, 
-                y - self.cr, 
-                x - text_w/2 + self.cr, 
-                y + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('l'+str(self.modules),))
-        
+                x - text_w/2 - self.cr,
+                y - self.cr,
+                x - text_w/2 + self.cr,
+                y + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('l'+str(self.modules),))
+
             self.canvas.create_oval(
-                x + text_w/2 - self.cr, 
-                y - self.cr, 
-                x + text_w/2 + self.cr, 
-                y + self.cr, 
-                width = 2, 
-                fill = 'black', 
-                tags = tag + ('r'+str(self.modules),))
-            
-        self.canvas.startxy.append((x, y))
+                x + text_w/2 - self.cr,
+                y - self.cr,
+                x + text_w/2 + self.cr,
+                y + self.cr,
+                width=2,
+                fill='black',
+                tags=tag + ('r'+str(self.modules),))
+
+        self.canvas_startxy.append((x, y))
         self.connections[self.modules] = {}
         self.module_out(boxName)
         self.module_list.append(boxName)
@@ -529,9 +568,9 @@ def add_module(self,boxName: str,x: float,y:float,ini = False,out = False):
     def upload(self):
         """ Opens the XML file that was previously uploaded and places the 
         modules, loops and connections in the canvas."""
-        
+
         filename = self.controller.output["xml_filename"]
-        
+
         self.reset()
 
         self.s = XML_handler()
@@ -539,87 +578,97 @@ def upload(self):
         # self.s._print_pretty(self.s.loaded_modules)
         modules = self.s.loaded_modules
         modout = modules['output']
-        del modules['Initialiser'], modules['output'] # They are generated when resetting
+        # They are generated when resetting
+        del modules['Initialiser'], modules['output']
         self.disp_mod = ['Initialiser', 'output']
         self.id_mod = [0, 1]
-        
+
         # Place the modules
         self.place_modules(modules)
         connect = list(modout['coordinates'][2].keys())
         for p, parent in enumerate(modout['parents']):
-                parent_id = self.id_mod[np.where(np.array(self.disp_mod) == parent)[0][0]]
-                out, ins = modout['coordinates'][2][connect[p]].split('-')
-                xout, yout, _ , _ = self.canvas.coords(out[0]+str(parent_id))
-                xins, yins, _, _ =self.canvas.coords(ins[0]+str(1))
-                self.canvas.create_line(
-                            xout + self.cr, 
-                            yout + self.cr, 
-                            xins + self.cr, 
-                            yins + self.cr,
-                            fill = "red", 
-                            arrow = tk.LAST, 
-                            tags = ('o'+str(parent_id), 
-                                  'o'+str(1), modout['coordinates'][2][connect[p]]))
-                self.out_data.iloc[int(parent_id)][1] = 1
-                self.connections[1][
-                    int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(1)
+            parent_id = self.id_mod[np.where(
+                np.array(self.disp_mod) == parent)[0][0]]
+            out, ins = modout['coordinates'][2][connect[p]].split('-')
+            xout, yout, _, _ = self.canvas.coords(out[0]+str(parent_id))
+            xins, yins, _, _ = self.canvas.coords(ins[0]+str(1))
+            self.canvas.create_line(
+                xout + self.cr,
+                yout + self.cr,
+                xins + self.cr,
+                yins + self.cr,
+                fill="red",
+                arrow=tk.LAST,
+                tags=('o'+str(parent_id),
+                      'o'+str(1), modout['coordinates'][2][connect[p]]))
+            self.out_data.iloc[int(parent_id)][1] = 1
+            self.connections[1][
+                int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(1)
         self.m = self.id_mod[2]
         x0, y0, x1, y1 = self.canvas.coords('p'+str(self.m))
-        
+
         # Configure frame for scrollbar
-        self.frame_canvas.configure(yscrollcommand=self.tree_scrolly.set, xscrollcommand=self.tree_scrollx.set)
-        self.frame_canvas.bind('<Configure>', lambda e: self.frame_canvas.configure(scrollregion = self.frame_canvas.bbox("all")))
-        self.framep = tk.Frame(self.frame_canvas, bg = self.bg)
-        self.frame_canvas.create_window((0,0), window = self.framep)
-        
-        self.set_mousewheel(self.frame_canvas, lambda e: self.frame_canvas.yview_scroll(-1*(e.delta//120), "units"))
-        
+        self.frame_canvas.configure(
+            yscrollcommand=self.tree_scrolly.set, xscrollcommand=self.tree_scrollx.set)
+        self.frame_canvas.bind('<Configure>', lambda e: self.frame_canvas.configure(
+            scrollregion=self.frame_canvas.bbox("all")))
+        self.framep = tk.Frame(self.frame_canvas, bg=self.bg)
+        self.frame_canvas.create_window((0, 0), window=self.framep)
+
+        self.set_mousewheel(
+            self.frame_canvas, lambda e: self.frame_canvas.yview_scroll(-1*(e.delta//120), "units"))
+
         self.select(x0, y0)
-        
-        self.framex.grid(column=1, row=1, sticky="nswe", rowspan=4, pady=(0,10))
+
+        self.framex.grid(column=1, row=1, sticky="nswe",
+                         rowspan=4, pady=(0, 10))
         self.framex.grid_columnconfigure(tuple(range(2)), weight=1)
-        self.frame_canvas.pack(side=tk.LEFT, fill = tk.BOTH, expand = True)
+        self.frame_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
 
     def set_mousewheel(self, widget, command):
         """Activate / deactivate mousewheel scrolling when 
         cursor is over / not over the widget respectively."""
-        widget.bind("<Enter>", lambda _: widget.bind_all('<MouseWheel>', command))
+        widget.bind("<Enter>", lambda _: widget.bind_all(
+            '<MouseWheel>', command))
         widget.bind("<Leave>", lambda _: widget.unbind_all('<MouseWheel>'))
-            
-    def place_modules(self,modules: dict):
+
+    def place_modules(self, modules: dict):
         """Places the modules in the dictionary in the canvas.
         :param modules: dict type of modules in the pipeline.
         """
-        
+
         for key in [key for key, val in modules.items() if type(val) == dict]:
             if modules[key]['class'] == 'loop':
                 # Extracts numbers from string
-                l = int(''.join(map(str, list(filter(str.isdigit, modules[key]['name'])))))
+                l = int(
+                    ''.join(map(str, list(filter(str.isdigit, modules[key]['name'])))))
                 x0, y0, x1, y1 = modules[key]['coordinates']
-                self.canvas.create_rectangle(x0, y0, 
-                                             x1, y1, 
-                                             outline = '#4ff07a',
-                                             tag = 'loop-'+str(l))
-                text_w = self.controller.pages_font.measure(modules[key]['type']) + 20
+                self.canvas.create_rectangle(x0, y0,
+                                             x1, y1,
+                                             outline='#4ff07a',
+                                             tag='loop-'+str(l))  # type: ignore
+                text_w = self.controller.pages_font.measure(
+                    modules[key]['type']) + 20
                 self.canvas.create_text(
-                    x0 + text_w/2, 
-                    y0 + 20, 
-                    font = self.controller.pages_font, 
-                    text = modules[key]['type'], 
-                    tags = ('loop-'+str(l), 'type'+'-'+str(l)), 
-                    justify = tk.CENTER)
-                text_w = self.controller.pages_font.measure(modules[key]['condition']) + 20
+                    x0 + text_w/2,
+                    y0 + 20,
+                    font=self.controller.pages_font,
+                    text=modules[key]['type'],
+                    tags=('loop-'+str(l), 'type'+'-'+str(l)),
+                    justify=tk.CENTER)
+                text_w = self.controller.pages_font.measure(
+                    modules[key]['condition']) + 20
                 self.canvas.create_text(
-                    x1 - text_w/2, 
-                    y0 + 20, 
-                    font = self.controller.pages_font, 
-                    text = modules[key]['condition'], 
-                    tags = ('loop-'+str(l), 'condition'+'-'+str(l)), 
-                    justify = tk.CENTER)
+                    x1 - text_w/2,
+                    y0 + 20,
+                    font=self.controller.pages_font,
+                    text=modules[key]['condition'],
+                    tags=('loop-'+str(l), 'condition'+'-'+str(l)),
+                    justify=tk.CENTER)
                 self.loops.append({'type': modules[key]['type'],
                                    'condition': modules[key]['condition'],
-                                   'mod': [], 
-                                   'coord': (x0, y0, 
+                                   'mod': [],
+                                   'coord': (x0, y0,
                                              x1, y1)})
                 self.place_modules(modules[key])
             else:
@@ -630,24 +679,29 @@ def place_modules(self,modules: dict):
                 self.module_list[-1] = modules[key]['module_type']
                 self.id_mod.append(modules[key]['coordinates'][1])
                 connect = list(modules[key]['coordinates'][2].keys())
-                
+
                 # Connect modules
                 for p, parent in enumerate(modules[key]['parents']):
                     if not (parent[:4] == 'loop'):
-                        parent_id = self.id_mod[np.where(np.array(self.disp_mod) == parent)[0][0]]
-                        out, ins = modules[key]['coordinates'][2][connect[p]].split('-')
-                        xout, yout, _ , _ = self.canvas.coords(out[0]+str(parent_id))
-                        xins, yins, _, _ =self.canvas.coords(ins[0]+str(self.id_mod[-1]))
+                        parent_id = self.id_mod[np.where(
+                            np.array(self.disp_mod) == parent)[0][0]]
+                        out, ins = modules[key]['coordinates'][2][connect[p]].split(
+                            '-')
+                        xout, yout, _, _ = self.canvas.coords(
+                            out[0]+str(parent_id))
+                        xins, yins, _, _ = self.canvas.coords(
+                            ins[0]+str(self.id_mod[-1]))
                         self.canvas.create_line(
-                                    xout + self.cr, 
-                                    yout + self.cr, 
-                                    xins + self.cr, 
-                                    yins + self.cr,
-                                    fill = "red", 
-                                    arrow = tk.LAST, 
-                                    tags = ('o'+str(parent_id), 
-                                          'o'+str(self.id_mod[-1]), modules[key]['coordinates'][2][connect[p]]))
-                        self.out_data.iloc[int(parent_id)][int(self.id_mod[-1])] = 1
+                            xout + self.cr,
+                            yout + self.cr,
+                            xins + self.cr,
+                            yins + self.cr,
+                            fill="red",
+                            arrow=tk.LAST,
+                            tags=('o'+str(parent_id),
+                                  'o'+str(self.id_mod[-1]), modules[key]['coordinates'][2][connect[p]]))
+                        self.out_data.iloc[int(parent_id)][int(
+                            self.id_mod[-1])] = 1
                         self.connections[int(self.id_mod[-1])][
                             int(parent_id)] = out[0]+str(parent_id) + '-' + ins[0]+str(self.id_mod[-1])
                     else:
@@ -656,38 +710,37 @@ def place_modules(self,modules: dict):
 
     def reset(self):
         """ Resets the canvas and the stored information."""
-        self.canvas.delete(tk.ALL) # Reset canvas
-        
-        if hasattr(self, 'newWindow') and (self.newWindow!= None):
+        self.canvas.delete(tk.ALL)  # Reset canvas
+
+        if hasattr(self, 'newWindow') and (self.newWindow != None):
             self.newWindow.destroy()
-            
-        self.canvas.startxy = []
+
+        self.canvas_startxy = []
         self.out_data = pd.DataFrame()
         self.connections = {}
         self.modules = 0
         self.module_list = []
         self.module_names = []
-        
-        self.add_module('Initialiser', self.width/2, self.h, ini = True)
-        self.add_module('Output', self.width/2, self.height - self.h, out = True)
-    
+
+        self.add_module('Initialiser', self.width/2, self.h, ini=True)
+        self.add_module('Output', self.width/2, self.height - self.h, out=True)
+
         self.draw = False
         self.loops = []
         self.drawLoop = False
         self.l = 0
-        self.id_done = [0,1]
+        self.id_done = [0, 1]
         self.plugin = {}
         for widget in self.allWeHearIs:
-                widget.grid_remove()
+            widget.grid_remove()
         self.allWeHearIs = []
-        self.my_label.config(text = '')
+        self.my_label.config(text='')
 
-            
     def check_quit(self):
         """Checks if all plugins are specified to reset and go back to the startpage"""
         if not len(self.id_done) == len(self.id_mod):
             response = messagebox.askokcancel(
-                "Exit", 
+                "Exit",
                 "There are some unspecified plugins. Are you sure you want to leave?")
             if response:
                 self.reset()
@@ -709,9 +762,10 @@ def check_quit(self):
             self.controller.Plugin.set(True)
             self.controller._show_frame("MainPage")
 
+
 class ToolTip(object):
     """ Defines a text window associated to a widget with a description. """
-    
+
     def __init__(self, widget):
         self.widget = widget
         self.tipwindow = None
@@ -730,8 +784,8 @@ def showtip(self, text):
         tw.wm_overrideredirect(1)
         tw.wm_geometry("+%d+%d" % (x, y))
         label = tk.Label(tw, text=self.text, justify=tk.LEFT,
-                      background="#d0d4d9", relief=tk.SOLID, borderwidth=1,
-                      font=("Helvetica", "8", "normal"))
+                         background="#d0d4d9", relief=tk.SOLID, borderwidth=1,
+                         font=("Helvetica", "8", "normal"))
         label.pack(ipadx=1)
 
     def hidetip(self):
@@ -740,8 +794,10 @@ def hidetip(self):
         if tw:
             tw.destroy()
 
+
 class EntryWithPlaceholder(tk.Entry):
     """ Defines an entry with a placeholder text displayed if not focused on."""
+
     def __init__(self, master=None, placeholder="PLACEHOLDER", color='grey'):
         super().__init__(master)
 
@@ -766,7 +822,3 @@ def foc_in(self, *args):
     def foc_out(self, *args):
         if not self.get():
             self.put_placeholder()
-
-if __name__ == "__main__":
-    app = pluginCanvas()
-    app.mainloop()
diff --git a/aidesign/utils/plugins/startpage.py b/aidesign/utils/plugins/startpage.py
index d034f202..c71f65e5 100644
--- a/aidesign/utils/plugins/startpage.py
+++ b/aidesign/utils/plugins/startpage.py
@@ -1,122 +1,135 @@
+from aidesign._import_helper import get_lib_parent_dir
+
 import tkinter as tk
 import os
 from PIL import Image, ImageTk
 from tkinter import messagebox
 from tkinter.filedialog import askopenfilename
+import pandas as pd
+
+_PLUGIN_READABLE_NAMES = {"start": "default",
+                          "start page": "alias"}                    # type:ignore
+_PLUGIN_MODULE_OPTIONS = {}                                         # type:ignore
+_PLUGIN_REQUIRED_SETTINGS = {}                                      # type:ignore
+_PLUGIN_OPTIONAL_SETTINGS = {}                                      # type:ignore
+
 
-_PLUGIN_READABLE_NAMES = {"start":"default","start page":"alias"}
-_PLUGIN_MODULE_OPTIONS = {}
-_PLUGIN_REQUIRED_SETTINGS = {}
-_PLUGIN_OPTIONAL_SETTINGS = {}
 class StartPage(tk.Frame):
     """Main Page for launching GUI methods"""
+
     def __init__(self, parent, controller):
         " Here we define the main frame displayed upon opening the program."
         " This leads to the different methods to provide feedback."
-        super().__init__(parent, bg = parent['bg'])
+        super().__init__(parent, bg=parent['bg'])
         self.controller = controller
         self.controller.title('User interaction adaptation')
-        
-        script_dir = os.path.dirname(__file__)
+
+        # script_dir = os.path.dirname(__file__)
+        self.out_data = pd.DataFrame()
+        script_dir = get_lib_parent_dir()
         self.my_img1 = ImageTk.PhotoImage(
-                            Image.open(
-                                os.path.join(
-                                    script_dir,
-                                    'resources',
-                                    'Assets',
-                                    'UFAIcon_name.png')
-                                    ).resize((600, 400))
-                                )
-
-        self.my_label = tk.Label(self, 
-                                    image = self.my_img1,
-                                    bg = parent['bg'])
-
-        self.my_label.grid(column = 0,
-                            row = 0,
-                            rowspan = 10,
-                            columnspan = 4)
-        
-        my_label = tk.Label(self, 
-                                text = 'Choose feedback method ...',
-                                pady= 10,
-                                font = controller.title_font,
-                                bg = parent['bg'],
-                                fg = 'white')
-                                
-        my_label.grid(column = 0,
-                            row = 11,
-                            columnspan = 4)
+            Image.open(
+                os.path.join(
+                    script_dir,
+                    "utils",
+                    'resources',
+                    'Assets',
+                    'UFAIcon_name.png')
+            ).resize((600, 400))
+        )
+
+        self.my_label = tk.Label(self,
+                                 image=self.my_img1,
+                                 bg=parent['bg'])
+
+        self.my_label.grid(column=0,
+                           row=0,
+                           rowspan=10,
+                           columnspan=4)
+
+        my_label = tk.Label(self,
+                            text='Choose feedback method ...',
+                            pady=10,
+                            font=controller.title_font,
+                            bg=parent['bg'],
+                            fg='white')
+
+        my_label.grid(column=0,
+                      row=11,
+                      columnspan=4)
 
         button_speak = tk.Button(self,
-                                    text = 'Speak',
-                                    fg = 'white',
-                                    font = controller.title_font, 
-                                    bg = parent['bg'],
-                                    height = 3,
-                                    width = 20,
-                                    command = self.NotImpl
-                                    ).grid(column = 0, row = 59)
+                                 text='Speak',
+                                 fg='white',
+                                 font=controller.title_font,
+                                 bg=parent['bg'],
+                                 height=3,
+                                 width=20,
+                                 command=self.NotImpl
+                                 ).grid(column=0, row=59)
 
         button_write = tk.Button(self,
-                                    text = 'Write',
-                                    fg = 'white',
-                                    font = controller.title_font, 
-                                    bg = parent['bg'],
-                                    height = 3,
-                                    width = 20,
-                                    command = self.NotImpl
-                                    ).grid(column = 1, row = 59)
+                                 text='Write',
+                                 fg='white',
+                                 font=controller.title_font,
+                                 bg=parent['bg'],
+                                 height=3,
+                                 width=20,
+                                 command=self.NotImpl
+                                 ).grid(column=1, row=59)
 
         button_manual = tk.Button(self,
-                                    text = 'Manual input',
-                                    fg = 'white',
-                                    font = controller.title_font, 
-                                    bg = parent['bg'],
-                                    height = 3,
-                                    width = 20, 
-                                    command = lambda: controller.show_frame("PageManual")
-                                    ).grid(column = 2, row = 59)
+                                  text='Manual input',
+                                  fg='white',
+                                  font=controller.title_font,
+                                  bg=parent['bg'],
+                                  height=3,
+                                  width=20,
+                                  command=lambda: controller.show_frame(
+                                      "PageManual")
+                                  ).grid(column=2, row=59)
 
         button_canvas = tk.Button(self,
-                                    text = 'Interact with canvas',
-                                    fg = 'white',
-                                    font = controller.title_font,
-                                    bg = parent['bg'],
-                                    height = 3,
-                                    width = 20, 
-                                    command = lambda: controller.show_frame("PageCanvas")
-                                    ).grid(column = 3, row = 59)
+                                  text='Interact with canvas',
+                                  fg='white',
+                                  font=controller.title_font,
+                                  bg=parent['bg'],
+                                  height=3,
+                                  width=20,
+                                  command=lambda: controller.show_frame(
+                                      "PageCanvas")
+                                  ).grid(column=3, row=59)
 
         button_upload = tk.Button(self,
-                                    text = 'Upoad file',
-                                    fg = 'white',
-                                    font = controller.title_font, 
-                                    bg = parent['bg'],
-                                    height = 3,
-                                    width = 20, 
-                                    command = self.upload_data
-                                    ).grid(column = 0, row = 60)
-        
+                                  text='Upoad file',
+                                  fg='white',
+                                  font=controller.title_font,
+                                  bg=parent['bg'],
+                                  height=3,
+                                  width=20,
+                                  command=self.upload_data
+                                  ).grid(column=0, row=60)
+
     # def light_theme(self):
     #     listbox_tasks.config(bg="white", fg="black")
     #     button_add_task.config(highlightbackground='white')
     #     button_delete_task.config(highlightbackground='white')
     #     button_load_tasks.config(highlightbackground='white')
     #     button_save_tasks.config(highlightbackground='white')
-    #     entry_task.config(bg='white', fg='black')       
-        
+    #     entry_task.config(bg='white', fg='black')
+
     def NotImpl(self):
-        messagebox.showwarning("Error", "This functionality is not implemented yet.")
-        
+        messagebox.showwarning(
+            "Error", "This functionality is not implemented yet.")
+
     def upload_data(self):
-        filename = askopenfilename(initialdir = os.getcwd(),
-                                    title = 'Select a file',
-                                    defaultextension = '.csv', 
-                                    filetypes = [
-                                            ('Text file', '.txt'),
-                                            ('CSS file', '.css'),
-                                            ('All Files', '*.*')
-                                            ])
+        filename = askopenfilename(initialdir=os.getcwd(),
+                                   title='Select a file',
+                                   defaultextension='.csv',
+                                   filetypes=[
+            ('Text file', '.txt'),
+            ('CSS file', '.css'),
+            ('All Files', '*.*')
+        ])
         if filename is not None:
-            messagebox.showinfo("Success", "Feedback correctly uploaded.")
\ No newline at end of file
+            messagebox.showinfo("Success", "Feedback correctly uploaded.")
diff --git a/aidesign/utils/plugins/resources/Assets/ADDIcon.ico b/aidesign/utils/resources/Assets/ADDIcon.ico
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/ADDIcon.ico
rename to aidesign/utils/resources/Assets/ADDIcon.ico
diff --git a/aidesign/utils/plugins/resources/Assets/ADDIcon_name.png b/aidesign/utils/resources/Assets/ADDIcon_name.png
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/ADDIcon_name.png
rename to aidesign/utils/resources/Assets/ADDIcon_name.png
diff --git a/aidesign/utils/plugins/resources/Assets/AIDIcon.ico b/aidesign/utils/resources/Assets/AIDIcon.ico
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/AIDIcon.ico
rename to aidesign/utils/resources/Assets/AIDIcon.ico
diff --git a/aidesign/utils/plugins/resources/Assets/AIDIcon_name.png b/aidesign/utils/resources/Assets/AIDIcon_name.png
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/AIDIcon_name.png
rename to aidesign/utils/resources/Assets/AIDIcon_name.png
diff --git a/aidesign/utils/plugins/resources/Assets/AIFRED.png b/aidesign/utils/resources/Assets/AIFRED.png
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/AIFRED.png
rename to aidesign/utils/resources/Assets/AIFRED.png
diff --git a/aidesign/utils/plugins/resources/Assets/Hospital.xml b/aidesign/utils/resources/Assets/Hospital.xml
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/Hospital.xml
rename to aidesign/utils/resources/Assets/Hospital.xml
diff --git a/aidesign/utils/plugins/resources/Assets/UFAIcon.ico b/aidesign/utils/resources/Assets/UFAIcon.ico
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/UFAIcon.ico
rename to aidesign/utils/resources/Assets/UFAIcon.ico
diff --git a/aidesign/utils/plugins/resources/Assets/UFAIcon_name.png b/aidesign/utils/resources/Assets/UFAIcon_name.png
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/UFAIcon_name.png
rename to aidesign/utils/resources/Assets/UFAIcon_name.png
diff --git a/aidesign/utils/plugins/resources/Assets/back_arrow.png b/aidesign/utils/resources/Assets/back_arrow.png
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/back_arrow.png
rename to aidesign/utils/resources/Assets/back_arrow.png
diff --git a/aidesign/utils/plugins/resources/Assets/forw_arrow.png b/aidesign/utils/resources/Assets/forw_arrow.png
similarity index 100%
rename from aidesign/utils/plugins/resources/Assets/forw_arrow.png
rename to aidesign/utils/resources/Assets/forw_arrow.png
diff --git a/aidesign/utils/plugins/resources/example_radiography_images/00028173_005.png b/aidesign/utils/resources/example_radiography_images/00028173_005.png
similarity index 100%
rename from aidesign/utils/plugins/resources/example_radiography_images/00028173_005.png
rename to aidesign/utils/resources/example_radiography_images/00028173_005.png
diff --git a/aidesign/utils/plugins/resources/example_radiography_images/00028179_000.png b/aidesign/utils/resources/example_radiography_images/00028179_000.png
similarity index 100%
rename from aidesign/utils/plugins/resources/example_radiography_images/00028179_000.png
rename to aidesign/utils/resources/example_radiography_images/00028179_000.png
diff --git a/aidesign/utils/plugins/resources/example_radiography_images/00028198_002.png b/aidesign/utils/resources/example_radiography_images/00028198_002.png
similarity index 100%
rename from aidesign/utils/plugins/resources/example_radiography_images/00028198_002.png
rename to aidesign/utils/resources/example_radiography_images/00028198_002.png
diff --git a/aidesign/utils/plugins/resources/example_radiography_images/00028208_035.png b/aidesign/utils/resources/example_radiography_images/00028208_035.png
similarity index 100%
rename from aidesign/utils/plugins/resources/example_radiography_images/00028208_035.png
rename to aidesign/utils/resources/example_radiography_images/00028208_035.png
diff --git a/aidesign/utils/plugins/resources/example_radiography_images/00028211_021.png b/aidesign/utils/resources/example_radiography_images/00028211_021.png
similarity index 100%
rename from aidesign/utils/plugins/resources/example_radiography_images/00028211_021.png
rename to aidesign/utils/resources/example_radiography_images/00028211_021.png
diff --git a/aidesign/utils/plugins/resources/example_radiography_images/00028253_000.png b/aidesign/utils/resources/example_radiography_images/00028253_000.png
similarity index 100%
rename from aidesign/utils/plugins/resources/example_radiography_images/00028253_000.png
rename to aidesign/utils/resources/example_radiography_images/00028253_000.png
diff --git a/aidesign/utils/plugins/resources/example_radiography_images/00028279_000.png b/aidesign/utils/resources/example_radiography_images/00028279_000.png
similarity index 100%
rename from aidesign/utils/plugins/resources/example_radiography_images/00028279_000.png
rename to aidesign/utils/resources/example_radiography_images/00028279_000.png
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 00000000..8f4bd118
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,16 @@
+[mypy]
+python_version = 3.8
+warn_return_any = False
+warn_unused_configs = True
+
+[mypy-pandas.*]
+ignore_missing_imports = True
+
+[mypy-sklearn.*]
+ignore_missing_imports = True
+
+[mypy-PIL.*]
+ignore_missing_imports = True
+
+[mypy-pkg_resources.*]
+ignore_missing_imports = True
\ No newline at end of file


Test patch:
diff --git a/aidesign/Data/resources/xml_files/data_passing_userfeedback_test.xml b/aidesign/Data/resources/xml_files/data_passing_userfeedback_test.xml
index 4c564344..a85b7640 100644
--- a/aidesign/Data/resources/xml_files/data_passing_userfeedback_test.xml
+++ b/aidesign/Data/resources/xml_files/data_passing_userfeedback_test.xml
@@ -5,10 +5,10 @@
             <Y file="./Data/resources/image_classification/Y_data.csv" />
         </inputdata>
         <relationships>
-            <child name="My First UserFeedback Module" />
+            <child name="My First UserInteraction Module" />
         </relationships>
     </Initialiser>
-    <UserFeedback name="My First UserFeedback Module">
+    <UserInteraction name="My First UserInteraction Module">
         <plugin type="manual">
             <some_param val = "2" />
         </plugin>
@@ -16,7 +16,7 @@
             <parent name="Init" />
             <child name="Output" />
         </relationships>
-    </UserFeedback>
+    </UserInteraction>
     <Output>
         <plugin type="">
             <out_data>
@@ -25,7 +25,7 @@
             <save_to file="../resources/output_test.csv" />
         </plugin>
         <relationships>
-            <parent name="My Second UserFeedback Module" />
+            <parent name="My Second UserInteraction Module" />
         </relationships>
     </Output>
 </pipeline>
diff --git a/aidesign/Data/resources/xml_files/regerssion_test.xml b/aidesign/Data/resources/xml_files/regerssion_test.xml
index 9accbcc4..a25962cb 100644
--- a/aidesign/Data/resources/xml_files/regerssion_test.xml
+++ b/aidesign/Data/resources/xml_files/regerssion_test.xml
@@ -19,16 +19,7 @@
         <coordinates>
             [(300.0,300.0),2,{0:'d0-u2'}]
         </coordinates>
-        <plugin type="Regression">
-            <power>
-                 2
-            </power>
-            <train_name>
-                 x
-            </train_name>
-            <target_name>
-                 c
-            </target_name>
+        <plugin type="LinearRegression">
         </plugin>
     </Modelling>
     <Output name="Output">
diff --git a/aidesign/Data/resources/xml_files/test-delete.xml b/aidesign/Data/resources/xml_files/test-delete.xml
new file mode 100644
index 00000000..23e61fc2
--- /dev/null
+++ b/aidesign/Data/resources/xml_files/test-delete.xml
@@ -0,0 +1,36 @@
+<pipeline>
+    <Initialiser name="Initialiser">
+        <relationships>
+            <child name="User Interaction" />
+        </relationships>
+        <coordinates>
+            [(300.0,50),0,{}]
+        </coordinates>
+        <inputdata>
+            <X file="./Data/resources/supervised_classification/X_tr.csv" />
+        </inputdata>
+    </Initialiser>
+    <UserInteraction name="User Interaction">
+        <relationships>
+            <parent name="Initialiser" />
+            <child name="Output" />
+        </relationships>
+        <coordinates>
+            [(300.0,300.0),2,{0:'d0-u2'}]
+        </coordinates>
+        <plugin type="ManualInput">
+            <image_dir>
+                 ./aidesign/Data/resources/image_classification/training_images
+            </image_dir>
+        </plugin>
+    </UserInteraction>
+    <Output name="Output">
+        <plugin type="" />
+        <relationships>
+            <parent name="User Interaction" />
+        </relationships>
+        <coordinates>
+            [(300.0,550),1,{2:'d2-u1'}]
+        </coordinates>
+    </Output>
+</pipeline>