Repository: jeroenvdhoven/mlpype (language: python)

Issue:
Reduce python level



Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index 2c77080..2b3c0bf 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,6 @@ clean:
 	rm -rf dist/ packages/
 
 dev-install:
-	pip install -e .
 	./scripts/dev_install.sh -e 1
 
 host-pypi-local:
diff --git a/pype/base/data/dataset.py b/pype/base/data/dataset.py
index 6c61c0a..cbdecba 100644
--- a/pype/base/data/dataset.py
+++ b/pype/base/data/dataset.py
@@ -1,9 +1,9 @@
-from typing import Iterable, List, TypeVar
+from typing import Dict, Iterable, List, TypeVar
 
 Data = TypeVar("Data")
 
 
-class DataSet(dict[str, Data]):
+class DataSet(Dict[str, Data]):
     """A DataSet consisting of multiple name-Data pairs.
 
     The Data can be multiple types of objects:
@@ -24,11 +24,11 @@ def get_all(self, keys: List[str]) -> List[Data]:
         return [self[key] for key in keys]
 
     @classmethod
-    def from_dict(cls, dct: dict[str, Data]) -> "DataSet[Data]":
+    def from_dict(cls, dct: Dict[str, Data]) -> "DataSet[Data]":
         """Creates a DataSet from a dictionary.
 
         Args:
-            dct (dict[str, Data]): The dictionary containing data.
+            dct (Dict[str, Data]): The dictionary containing data.
 
         Returns:
             DataSet[Data]: A DataSet based on the given dict.
diff --git a/pype/base/data/dataset_source.py b/pype/base/data/dataset_source.py
index dfbfb3d..6889a1a 100644
--- a/pype/base/data/dataset_source.py
+++ b/pype/base/data/dataset_source.py
@@ -1,4 +1,4 @@
-from typing import TypeVar
+from typing import Dict, TypeVar
 
 from pype.base.data.data_source import DataSource
 from pype.base.data.dataset import DataSet
@@ -6,7 +6,7 @@
 Data = TypeVar("Data")
 
 
-class DataSetSource(dict[str, DataSource[Data]]):
+class DataSetSource(Dict[str, DataSource[Data]]):
     """A collection of DataSources that together form a DataSet when loaded."""
 
     def read(self) -> DataSet[Data]:
diff --git a/pype/base/deploy/inference.py b/pype/base/deploy/inference.py
index 2eed853..d1f59e7 100644
--- a/pype/base/deploy/inference.py
+++ b/pype/base/deploy/inference.py
@@ -1,6 +1,6 @@
 import json
 from pathlib import Path
-from typing import Type
+from typing import Optional, Type, Union
 
 from pype.base.constants import Constants
 from pype.base.data import DataSet, DataSetSource
@@ -34,13 +34,13 @@ def __init__(
         self.input_type_checker = input_type_checker
         self.output_type_checker = output_type_checker
 
-    def predict(self, data: DataSet | DataSetSource) -> DataSet:
+    def predict(self, data: Union[DataSet, DataSetSource]) -> DataSet:
         """Predicts using the given data using the Pipeline and Model.
 
         # TODO: inverse transformation.
 
         Args:
-            data (DataSet | DataSetSource): The data to predict for.
+            data (Union[DataSet, DataSetSource]): The data to predict for.
 
         Returns:
             DataSet: The predictions from Model.
@@ -54,7 +54,7 @@ def predict(self, data: DataSet | DataSetSource) -> DataSet:
         return predicted
 
     @classmethod
-    def from_folder(cls: Type["Inferencer"], folder: Path, serialiser: Serialiser | None = None) -> "Inferencer":
+    def from_folder(cls: Type["Inferencer"], folder: Path, serialiser: Optional[Serialiser] = None) -> "Inferencer":
         """Loads a Inferencer from the results of an Experiment.
 
         We use the absolute version of the path to try and prevent loading issues.
@@ -62,7 +62,7 @@ def from_folder(cls: Type["Inferencer"], folder: Path, serialiser: Serialiser |
         Args:
             folder (Path): The output folder from an Experiment, from which we load
                 all required elements to make a inference pipeline.
-            serialiser (Serialiser | None): The Serialiser used to deserialise the pipeline and input/output
+            serialiser (Optional[Serialiser]): The Serialiser used to deserialise the pipeline and input/output
                 type checkers. Defaults to a JoblibSerialiser.
 
         Returns:
diff --git a/pype/base/deploy/wheel/builder.py b/pype/base/deploy/wheel/builder.py
index df32d09..bed7cf1 100644
--- a/pype/base/deploy/wheel/builder.py
+++ b/pype/base/deploy/wheel/builder.py
@@ -4,6 +4,7 @@
 from pathlib import Path
 from shutil import copytree
 from tempfile import TemporaryDirectory
+from typing import List, Optional, Union
 
 from pype.base.constants import Constants
 from pype.base.utils.workspace import switch_directory
@@ -34,11 +35,11 @@ class WheelBuilder:
         model_name (str): The name of the model. Will be used as package name. Should follow
             proper package naming conventions.
         version (str): Model/package version.
-        output_wheel_file (Path | str | None, optional): The output directory. Defaults to "wheel_output"
+        output_wheel_file (Optional[Union[Path, str]]): The output directory. Defaults to "wheel_output"
             in the current directory.
-        libraries (list[str] | None, optional): A list of (versioned) libraries that are used as
+        libraries (Optional[List[str]]): A list of (versioned) libraries that are used as
             dependencies. By default we use the requirements.txt file from the experiment run.
-        extensions (list[WheelExtension] | None, optional): A list of extension to add to this
+        extensions (Optional[List[WheelExtension]], optional): A list of extension to add to this
             wheel file. Extensions add extra functionality besides the base "import pipeline" functionality.
             This can include hosting a FastAPI app for instance.
     """
@@ -46,9 +47,9 @@ class WheelBuilder:
     model_folder: Path
     model_name: str
     version: str
-    output_wheel_file: Path | str | None = None
-    libraries: list[str] | None = None
-    extensions: list[WheelExtension] | None = None
+    output_wheel_file: Optional[Union[Path, str]] = None
+    libraries: Optional[List[str]] = None
+    extensions: Optional[List[WheelExtension]] = None
 
     def __post_init__(self) -> None:
         """Performs checks and default initialisations."""
diff --git a/pype/base/deploy/wheel/extensions.py b/pype/base/deploy/wheel/extensions.py
index 4fdb330..0a0ad16 100644
--- a/pype/base/deploy/wheel/extensions.py
+++ b/pype/base/deploy/wheel/extensions.py
@@ -2,6 +2,7 @@
 from dataclasses import dataclass
 from pathlib import Path
 from shutil import copyfile
+from typing import List, Tuple
 
 
 @dataclass
@@ -29,8 +30,8 @@ class WheelExtension:
     """
 
     name: str
-    functionality: list[tuple[Path, list[str]]]
-    libraries: list[str]
+    functionality: List[Tuple[Path, List[str]]]
+    libraries: List[str]
 
     def __post_init__(self) -> None:
         """Perform checks on inputs."""
@@ -71,14 +72,14 @@ def _check_file_name(name: str) -> None:
     def extend(
         self,
         package_name: str,
-        libraries: list[str],
+        libraries: List[str],
         wheel_package_dir: Path,
     ) -> None:
         """Extends the given wheel file with this extension.
 
         Args:
             package_name (str): The name of the main package.
-            libraries (list[str]): The current set of libraries to be installed.
+            libraries (List[str]): The current set of libraries to be installed.
             wheel_package_dir (Path): The Path to the main package.
         """
         libraries.extend(self.libraries)
diff --git a/pype/base/deploy/wheel/helpers/setup_template.py b/pype/base/deploy/wheel/helpers/setup_template.py
index 4d0a7a5..c099b83 100644
--- a/pype/base/deploy/wheel/helpers/setup_template.py
+++ b/pype/base/deploy/wheel/helpers/setup_template.py
@@ -1,17 +1,18 @@
 import os
 from pathlib import Path
+from typing import List, Union
 
 from setuptools import setup
 
 
-def package_files(directory: Path | str) -> list[str]:
+def package_files(directory: Union[Path, str]) -> List[str]:
     """Find all files from the output directory of an Experiment run to include.
 
     Args:
-        directory (Path | str): The main output directory to search.
+        directory (Union[Path, str]): The main output directory to search.
 
     Returns:
-        list[str]: The files to include.
+        List[str]: The files to include.
     """
     paths = []
     for (path, _, filenames) in os.walk(directory):
diff --git a/pype/base/deploy/wheel/wheel.py b/pype/base/deploy/wheel/wheel.py
index 344bd4b..bfacece 100644
--- a/pype/base/deploy/wheel/wheel.py
+++ b/pype/base/deploy/wheel/wheel.py
@@ -3,6 +3,7 @@
 from pathlib import Path
 from shutil import copyfile, copytree
 from tempfile import TemporaryDirectory
+from typing import List, Optional, Union
 
 from pype.base.constants import Constants
 from pype.base.utils.workspace import switch_directory
@@ -12,8 +13,8 @@ def make_wheel(
     model_folder: Path,
     model_name: str,
     version: str,
-    output_wheel_file: Path | str | None = None,
-    libraries: list[str] | None = None,
+    output_wheel_file: Optional[Union[Path, str]] = None,
+    libraries: Optional[List[str]] = None,
 ) -> None:
     """Turns an output folder from an Experiment into a wheel file.
 
@@ -26,9 +27,9 @@ def make_wheel(
         model_name (str): The name of the model. Will be used as package name. Should follow
             proper package naming conventions.
         version (str): Model/package version.
-        output_wheel_file (Path | str | None, optional): The output directory. Defaults to "wheel_output"
+        output_wheel_file (Optional[Union[Path, str]]): The output directory. Defaults to "wheel_output"
             in the current directory.
-        libraries (list[str] | None, optional): A list of (versioned) libraries that are used as
+        libraries (Optional[List[str]]): A list of (versioned) libraries that are used as
             dependencies. By default we use the requirements.txt file from the experiment run.
     """
     if output_wheel_file is None:
diff --git a/pype/base/evaluate/evaluator.py b/pype/base/evaluate/evaluator.py
index bc3f173..ee144ad 100644
--- a/pype/base/evaluate/evaluator.py
+++ b/pype/base/evaluate/evaluator.py
@@ -1,4 +1,4 @@
-from typing import Callable, Dict, Generic, TypeVar
+from typing import Callable, Dict, Generic, Optional, TypeVar, Union
 
 from pype.base.data import DataSet
 from pype.base.model import Model
@@ -10,12 +10,12 @@
 class Evaluator(Generic[Data]):
     def __init__(
         self,
-        functions: Dict[str, Callable[[Data, Data], float | int | str | bool]],
+        functions: Dict[str, Callable[[Union[Data, Data]], Union[float, int, str, bool]]],
     ) -> None:
         """Evaluates a Model on the given Functions.
 
         Args:
-            functions (Dict[str, Callable[[Tuple[Data, ...]], float | int | str | bool]]):
+            functions (Dict[str, Callable[[Union[Data, Data]], Union[float, int, str, bool]]]):
                 A dict of metric names and metric functions. We expect each to return
                 a float, int, string, or boolean value. We provide the arguments as follows:
                     - first, all labeled data, e.g. y_true.
@@ -24,8 +24,8 @@ def __init__(
         self.functions = functions
 
     def evaluate(
-        self, model: Model, data: DataSet, pipeline: Pipeline | None = None
-    ) -> Dict[str, float | int | str | bool]:
+        self, model: Model, data: DataSet, pipeline: Optional[Pipeline] = None
+    ) -> Dict[str, Union[str, float, int, str, bool]]:
         """Evaluate the given model on the given dataset.
 
         We assume the model does not need to be transformed anymore if pipeline is None.
@@ -33,10 +33,10 @@ def evaluate(
         Args:
             model (Model): The Model to evaluate.
             data (DataSet): The Dataset to use to evaluate the model.
-            pipeline (Pipeline, optional): If not None, this will be used to transform `data` first.
+            pipeline (Optional[Pipeline]): If not None, this will be used to transform `data` first.
 
         Returns:
-            Dict[str, float | int | str | bool]: A dictionary of metric_name-value pairs. The result
+            Dict[str, Union[str, float, int, str, bool]]: A dictionary of metric_name-value pairs. The result
                 of the evaluation.
         """
         if pipeline is not None:
diff --git a/pype/base/experiment/argument_parsing.py b/pype/base/experiment/argument_parsing.py
index 3405664..e4c01bd 100644
--- a/pype/base/experiment/argument_parsing.py
+++ b/pype/base/experiment/argument_parsing.py
@@ -3,7 +3,7 @@
 import typing
 import warnings
 from argparse import ArgumentParser
-from typing import Any, Callable, Iterable, List, Tuple, Type
+from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, Union
 
 from docstring_parser import parse
 
@@ -37,8 +37,8 @@ def add_args_to_parser_for_class(
     parser: ArgumentParser,
     class_: Type,
     prefix: str,
-    excluded_superclasses: list[Type],
-    excluded_args: list[str] | None = None,
+    excluded_superclasses: List[Type],
+    excluded_args: Optional[List[str]] = None,
 ) -> None:
     """Adds arguments for the constructor of a class to the given parser.
 
@@ -56,9 +56,9 @@ def add_args_to_parser_for_class(
         class_ (Type): The class who's constructor's arguments we want to add.
         prefix (str): A prefix that should be set on each argument name before adding it to
             the ArgumentParser.
-        excluded_superclasses (list[Type]): Superclasses of the class_ that should be ignored in case
+        excluded_superclasses (List[Type]): Superclasses of the class_ that should be ignored in case
             there are kw-args style arguments to the constructor.
-        excluded_args (list[str] | None, optional): argument names to never include.
+        excluded_args (Optional[List[str]], optional): argument names to never include.
             By default 'self' and 'cls' are ignored.
     """
     init_func = class_.__init__
@@ -82,8 +82,8 @@ def add_args_to_parser_for_function(
     parser: ArgumentParser,
     function: Callable,
     prefix: str,
-    excluded: list[str] | None = None,
-    class_docstring_args: dict[str, type | None] | None = None,
+    excluded: Optional[List[str]] = None,
+    class_docstring_args: Optional[Dict[str, Union[type, None]]] = None,
 ) -> None:
     """Add arguments for a given function to the given parser.
 
@@ -92,8 +92,8 @@ def add_args_to_parser_for_function(
         function (Callable): The function who's arguments we want to add.
         prefix (str): A prefix that should be set on each argument name before adding it to
             the ArgumentParser.
-        excluded (list[str] | None, optional): argument names to never include. By default 'self' and 'cls' are ignored.
-        class_docstring_args: (dict[str, type | None], optional): docstring arguments obtained from the class.
+        excluded (Optional[List[str]]): argument names to never include. By default 'self' and 'cls' are ignored.
+        class_docstring_args: (Optional[Dict[str, Union[type, None]]): docstring arguments obtained from the class.
             Useful for init functions, since those can be documented in multiple places.
     """
     args = inspect.signature(function)
@@ -111,8 +111,8 @@ def add_args_to_parser_for_function(
 
 
 def _parse_docs_to_type_args(
-    func: Callable, extra_mappings: dict[str, type] | None = None, include_none_args: bool = False
-) -> dict[str, type | None]:
+    func: Callable, extra_mappings: Optional[Dict[str, type]] = None, include_none_args: bool = False
+) -> Dict[str, Union[type, None]]:
     """Parses the docstring of a function into an arg-type dictionary.
 
     We only parse the following types, whose first letter can be capitalized.
@@ -127,19 +127,19 @@ def _parse_docs_to_type_args(
 
     Args:
         func (Callable): The function whose docstring needs to be read.
-        extra_mappings (dict[str, type], optional): Optional extra regex -> type mappings.
+        extra_mappings (Dict[str, type], optional): Optional extra regex -> type mappings.
             Defaults to no extra mappings.
         include_none_args (bool, optional): Whether to include arguments in the output
             with no good type mappings. Defaults to False.
 
     Returns:
-        dict[str, type]: A dictionary mapping argument names to types.
+        Dict[str, Union[type, None]]: A dictionary mapping argument names to types.
     """
     signature = inspect.signature(func)
     if extra_mappings is None:
         extra_mappings = {}
 
-    result: dict[str, type | None] = {}
+    result: Dict[str, Union[type, None]] = {}
     doc = inspect.getdoc(func)
     if doc is None:
         return result
@@ -154,7 +154,7 @@ def _parse_docs_to_type_args(
     return result
 
 
-def _parse_type_name(s: str | None, extra_mappings: dict[str, type] | None = None) -> type | None:
+def _parse_type_name(s: Optional[str], extra_mappings: Optional[Dict[str, type]] = None) -> Union[type, None]:
     if extra_mappings is None:
         extra_mappings = {}
     if s is None:
@@ -178,10 +178,10 @@ def add_argument(
     parser: ArgumentParser,
     name: str,
     prefix: str,
-    class_: type | None,
+    class_: Union[type, None],
     is_required: bool,
     default: Any,
-    excluded: list[str] | None = None,
+    excluded: Optional[List[str]] = None,
 ) -> None:
     """Add an argument to the given parser.
 
@@ -195,7 +195,7 @@ def add_argument(
             - lists / tuples / iterables of the above 4 arguments.
         is_required (bool): Whether the argument is required or optional.
         default (Any): the default value if is_required is False.
-        excluded (list[str] | None, optional): argument names to never include. By default 'self' and 'cls' are ignored.
+        excluded (Optional[List[str]]): argument names to never include. By default 'self' and 'cls' are ignored.
     """
     if excluded is None:
         excluded = []
diff --git a/pype/base/experiment/experiment.py b/pype/base/experiment/experiment.py
index 396c528..138cbbe 100644
--- a/pype/base/experiment/experiment.py
+++ b/pype/base/experiment/experiment.py
@@ -5,7 +5,7 @@
 from argparse import ArgumentParser
 from logging import getLogger
 from pathlib import Path
-from typing import Any, Type
+from typing import Any, Dict, List, Optional, Type, Union
 
 from pype.base.constants import Constants
 from pype.base.data.dataset_source import DataSetSource
@@ -22,17 +22,17 @@
 class Experiment:
     def __init__(
         self,
-        data_sources: dict[str, DataSetSource],
+        data_sources: Dict[str, DataSetSource],
         model: Model,
         pipeline: Pipeline,
         evaluator: Evaluator,
         logger: ExperimentLogger,
         input_type_checker: TypeCheckerPipe,
         output_type_checker: TypeCheckerPipe,
-        serialiser: Serialiser | None = None,
-        output_folder: Path | str = "outputs",
-        additional_files_to_store: list[str | Path] | None = None,
-        parameters: dict[str, Any] | None = None,
+        serialiser: Optional[Serialiser] = None,
+        output_folder: Union[Path, str] = "outputs",
+        additional_files_to_store: Optional[List[Union[str, Path]]] = None,
+        parameters: Optional[Dict[str, Any]] = None,
     ):
         """The core of the pype library: run a standardised ML experiment with the given parameters.
 
@@ -42,23 +42,23 @@ def __init__(
                 - `pipeline__<pipe_name>__<arg> for pipeline parameters.
 
         Args:
-            data_sources (dict[str, DataSetSource]): The DataSets to use, in DataSource form.
+            data_sources (Dict[str, DataSetSource]): The DataSets to use, in DataSource form.
                 Should contain at least a 'train' DataSet. These will be initialised in the beginning of the run.
             model (Model): The Model to fit.
             pipeline (Pipeline): The Pipeline to use to transform data before feeding it to the Model.
             evaluator (Evaluator): The evaluator to test how good your Model performs.
             logger (ExperimentLogger): The experiment logger to make sure you record how well your experiment worked,
                 and log any artifacts such as the trained model.
-            serialiser (Serialiser | None, optional): The serialiser to serialise any Python objects (expect the Model).
+            serialiser (Optional[Serialiser]): The serialiser to serialise any Python objects (expect the Model).
                 Defaults to a joblib serialiser.
-            output_folder: (Path | str): The output folder to log artifacts to. Defaults to "outputs".
+            output_folder: (Union[Path, str]): The output folder to log artifacts to. Defaults to "outputs".
             input_type_checker: (TypeCheckerPipe): A type checker for all input data. Will be used to verify incoming
                 data and standardise the order of data. Will be used later to help serialise/deserialise data.
             output_type_checker: (TypeCheckerPipe): A type checker for all output data. Will be used to verify outgoing
                 data and standardise the order of data. Will be used later to help serialise/deserialise data.
-            additional_files_to_store (list[str | Path] | None, optional): Extra files to store, such as python files.
+            additional_files_to_store (Optional[List[Union[str, Path]]]): Extra files to store, such as python files.
                 Defaults to no extra files (None).
-            parameters (dict[str, Any] | None, optional): Any parameters to log as part of this experiment.
+            parameters (Optional[Dict[str, Any]]): Any parameters to log as part of this experiment.
                 Defaults to None.
         """
         assert "train" in data_sources, "Must provide a 'train' entry in the data_sources dictionary."
@@ -91,11 +91,11 @@ def __init__(
         self.output_folder = output_folder
         self.additional_files_to_store = additional_files_to_store
 
-    def run(self) -> dict[str, dict[str, str | float | int | bool]]:
+    def run(self) -> Dict[str, Dict[str, Union[str, float, int, bool]]]:
         """Execute the experiment.
 
         Returns:
-            dict[str, dict[str, str | float | int | bool]]: The performance metrics of this run.
+            Dict[str, Union[str, float, int, bool]]: The performance metrics of this run.
         """
         with self.experiment_logger:
             self.logger.info("Load data")
@@ -188,14 +188,14 @@ def _create_output_folders(self) -> None:
         of.mkdir(exist_ok=True, parents=True)
         (of / Constants.MODEL_FOLDER).mkdir(exist_ok=True)
 
-    def copy(self, parameters: dict[str, Any], seed: int = 1) -> "Experiment":
+    def copy(self, parameters: Dict[str, Any], seed: int = 1) -> "Experiment":
         """Create a fresh copy of this Experiment.
 
         The Model & Pipeline will be recreated, so any trained versions will be
         re-initialised.
 
         Args:
-            parameters (dict[str, Any]): New parameters for the Model and Pipeline
+            parameters (Dict[str, Any]): New parameters for the Model and Pipeline
                 to be re-initialised with.
             seed (int, optional): Training seed. Defaults to 1.
 
@@ -228,7 +228,7 @@ def copy(self, parameters: dict[str, Any], seed: int = 1) -> "Experiment":
     @classmethod
     def from_dictionary(
         cls,
-        data_sources: dict[str, DataSetSource],
+        data_sources: Dict[str, DataSetSource],
         model_class: Type[Model],
         pipeline: Pipeline,
         evaluator: Evaluator,
@@ -236,17 +236,17 @@ def from_dictionary(
         serialiser: Serialiser,
         input_type_checker: TypeCheckerPipe,
         output_type_checker: TypeCheckerPipe,
-        model_inputs: list[str],
-        model_outputs: list[str],
-        parameters: dict[str, Any],
-        output_folder: Path | str = "outputs",
+        model_inputs: List[str],
+        model_outputs: List[str],
+        parameters: Dict[str, Any],
+        output_folder: Union[Path, str] = "outputs",
         seed: int = 1,
-        additional_files_to_store: list[str | Path] | None = None,
+        additional_files_to_store: Optional[List[Union[str, Path]]] = None,
     ) -> "Experiment":
         """Creates an Experiment from a dictionary with parameters.
 
         Args:
-            data_sources (dict[str, DataSetSource]): The DataSets to use, in DataSource form.
+            data_sources (Dict[str, DataSetSource]): The DataSets to use, in DataSource form.
                 Should contain at least a 'train' DataSet. These will be initialised in the beginning of the run.
             model_class (Type[Model]): The class of the Model to fit.
             pipeline (Pipeline): The Pipeline to use to transform data before feeding it to the Model.
@@ -254,17 +254,17 @@ def from_dictionary(
             logger (ExperimentLogger): The experiment logger to make sure you record how well your experiment worked,
                 and log any artifacts such as the trained model.
             serialiser (Serialiser): The serialiser to serialise any Python objects (expect the Model).
-            output_folder: (Path | str): The output folder to log artifacts to.
+            output_folder: (Union[Path, str]): The output folder to log artifacts to.
             input_type_checker: (TypeCheckerPipe): A type checker for all input data. Will be used to verify incoming
                 data and standardise the order of data. Will be used later to help serialise/deserialise data.
             output_type_checker: (TypeCheckerPipe): A type checker for all output data. Will be used to verify outgoing
                 data and standardise the order of data. Will be used later to help serialise/deserialise data.
-            model_inputs: (list[str]): Input dataset names to the model.
-            model_outputs: (list[str]): Output dataset names to the model.
+            model_inputs: (List[str]): Input dataset names to the model.
+            model_outputs: (List[str]): Output dataset names to the model.
             seed (int): The RNG seed to ensure reproducability.
-            parameters (dict[str, Any] | None, optional): Any parameters to log as part of this experiment.
+            parameters (Optional[Dict[str, Any]]): Any parameters to log as part of this experiment.
                 Defaults to None.
-            additional_files_to_store (list[str | Path] | None, optional): Extra files to store, such as python files.
+            additional_files_to_store (Optional[List[Union[str, Path]]]): Extra files to store, such as python files.
                 Defaults to no extra files (None).
 
         Returns:
@@ -293,7 +293,7 @@ def from_dictionary(
     @classmethod
     def from_command_line(
         cls,
-        data_sources: dict[str, DataSetSource],
+        data_sources: Dict[str, DataSetSource],
         model_class: Type[Model],
         pipeline: Pipeline,
         evaluator: Evaluator,
@@ -301,19 +301,19 @@ def from_command_line(
         serialiser: Serialiser,
         input_type_checker: TypeCheckerPipe,
         output_type_checker: TypeCheckerPipe,
-        model_inputs: list[str],
-        model_outputs: list[str],
-        output_folder: Path | str = "outputs",
+        model_inputs: List[str],
+        model_outputs: List[str],
+        output_folder: Union[Path, str] = "outputs",
         seed: int = 1,
-        additional_files_to_store: list[str | Path] | None = None,
-        fixed_arguments: dict[str, Any] | None = None,
+        additional_files_to_store: Optional[List[Union[str, Path]]] = None,
+        fixed_arguments: Optional[Dict[str, Any]] = None,
     ) -> "Experiment":
         """Automatically initialises an Experiment from command line arguments.
 
         # TODO: how to best store these extra files to the output folder as well?
 
         Args:
-            data_sources (dict[str, DataSetSource]): The DataSets to use, in DataSource form.
+            data_sources (Dict[str, DataSetSource]): The DataSets to use, in DataSource form.
                 Should contain at least a 'train' DataSet. These will be initialised in the beginning of the run.
             model_class (Type[Model]): The class of the Model to fit.
             pipeline (Pipeline): The Pipeline to use to transform data before feeding it to the Model.
@@ -321,17 +321,17 @@ def from_command_line(
             logger (ExperimentLogger): The experiment logger to make sure you record how well your experiment worked,
                 and log any artifacts such as the trained model.
             serialiser (Serialiser): The serialiser to serialise any Python objects (expect the Model).
-            output_folder: (Path | str): The output folder to log artifacts to.
+            output_folder: (Union[Path, str]): The output folder to log artifacts to.
             input_type_checker: (TypeCheckerPipe): A type checker for all input data. Will be used to verify incoming
                 data and standardise the order of data. Will be used later to help serialise/deserialise data.
             output_type_checker: (TypeCheckerPipe): A type checker for all output data. Will be used to verify outgoing
                 data and standardise the order of data. Will be used later to help serialise/deserialise data.
-            model_inputs: (list[str]): Input dataset names to the model.
-            model_outputs: (list[str]): Output dataset names to the model.
+            model_inputs: (List[str]): Input dataset names to the model.
+            model_outputs: (List[str]): Output dataset names to the model.
             seed (int): The RNG seed to ensure reproducability.
-            additional_files_to_store (list[str | Path] | None, optional): Extra files to store, such as python files.
-                Defaults to no extra files (None).
-            fixed_arguments (dict[str, Any] | None, optional): Arguments that won't be read from command line.
+            additional_files_to_store (Optional[List[Union[str, Path]]], optional): Extra files to store,
+                such as python files. Defaults to no extra files (None).
+            fixed_arguments (Optional[Dict[str, Any]]): Arguments that won't be read from command line.
                 Useful to pass complex objects that you don't want to optimize. Think of classes, loss functions,
                 metrics, etc. Any value in this dict will overwrite values from the command line. These need to be
                 presented in the same `model__<arg>` or `pipeline__<step>__<arg>` format as the command line arguments.
diff --git a/pype/base/logger/experiment_logger.py b/pype/base/logger/experiment_logger.py
index ee8f885..9fbb9ab 100644
--- a/pype/base/logger/experiment_logger.py
+++ b/pype/base/logger/experiment_logger.py
@@ -2,7 +2,7 @@
 from abc import ABC, abstractmethod
 from pathlib import Path
 from types import TracebackType
-from typing import Any
+from typing import Any, Dict, Optional, Type, Union
 
 from pype.base.model.model import Model
 from pype.base.serialiser import Serialiser
@@ -17,64 +17,64 @@ def __enter__(self) -> None:
     @abstractmethod
     def __exit__(
         self,
-        exc_type: type[BaseException] | None,
-        exc_val: BaseException | None,
-        exc_tb: TracebackType | None,
+        exc_type: Optional[Type[BaseException]],
+        exc_val: Optional[BaseException],
+        exc_tb: Optional[TracebackType],
     ) -> None:
         """Stop the experiment."""
         return
 
-    def log_metrics(self, dataset_name: str, metrics: dict[str, float | int | str | bool]) -> None:
+    def log_metrics(self, dataset_name: str, metrics: Dict[str, Union[str, float, int, str, bool]]) -> None:
         """Logs the metrics for a given dataset.
 
         Metrics will be stored by default as `<dataset_name>_<metric_name>`.
 
         Args:
             dataset_name (str): The name of the dataset. Will be prepended to any metric name.
-            metrics (Dict[str, Union[float, int, str  |  bool]]): A dictionary of metric names and values.
+            metrics (Dict[str, Union[str, float, int, str, bool]]): A dictionary of metric names and values.
         """
         metrics = {f"{dataset_name}_{name}": metric for name, metric in metrics.items()}
         self._log_metrics(metrics)
 
     @abstractmethod
-    def _log_metrics(self, metrics: dict[str, float | int | str | bool]) -> None:
+    def _log_metrics(self, metrics: Dict[str, Union[str, float, int, str, bool]]) -> None:
         """Perform the actual logging of metrics.
 
         Args:
-            metrics (dict[str, float | int | str | bool]): A dictionary of metric names and values.
+            metrics (Dict[str, Union[str, float, int, str, bool]]): A dictionary of metric names and values.
         """
         raise NotImplementedError
 
     @abstractmethod
-    def log_parameters(self, parameters: dict[str, Any]) -> None:
+    def log_parameters(self, parameters: Dict[str, Any]) -> None:
         """Logs the parameters for a given run.
 
         These will by default be passed on with prefixes such as `model__`.
 
         Args:
-            parameters (dict[str, Any]): The parameters of a given run. Ideally these
+            parameters (Dict[str, Any]): The parameters of a given run. Ideally these
                 parameters should be no more complicated than string, float, int, bool, or a
                 list of these.
         """
         raise NotImplementedError
 
-    def log_model(self, model: Model, folder: str | Path) -> None:
+    def log_model(self, model: Model, folder: Union[str, Path]) -> None:
         """Logs a Model for a given experiment.
 
         This function will write the model to the given location as well.
 
         Args:
             model (Model): The Model to be logged.
-            MODEL_FOLDER (str | Path): The file to log the Model to.
+            MODEL_FOLDER (Union[str, Path]): The file to log the Model to.
         """
         model.save(folder)
         self.log_file(folder)
 
-    def log_artifact(self, file: str | Path, serialiser: Serialiser, object: Any) -> None:
+    def log_artifact(self, file: Union[str, Path], serialiser: Serialiser, object: Any) -> None:
         """Logs an artifact/file as part of an experiment.
 
         Args:
-            file (str | Path): The file to serialise to and subsequently log.
+            file (Union[str, Path]): The file to serialise to and subsequently log.
             serialiser (Serialiser): Serialiser used to serialise the given object.
             object (Any): The object to log. This object will be serialised to the given file
                 using the serialiser and logged using `log_file`
@@ -82,15 +82,15 @@ def log_artifact(self, file: str | Path, serialiser: Serialiser, object: Any) ->
         serialiser.serialise(object, file)
         self.log_file(file)
 
-    def log_local_file(self, file: str | Path, output_target: str | Path) -> None:
+    def log_local_file(self, file: Union[str, Path], output_target: Union[str, Path]) -> None:
         """Logs a given local file as part of an experiment.
 
         First the file is copied to the output directory, then
         log_file is called to make it part of the experiment.
 
         Args:
-            file (str | Path): The file to log.
-            output_target (str | Path): The target location of the file.
+            file (Union[str, Path]): The file to log.
+            output_target (Union[str, Path]): The target location of the file.
         """
         output_target = Path(output_target)
         output_target.parent.mkdir(exist_ok=True)
@@ -98,10 +98,10 @@ def log_local_file(self, file: str | Path, output_target: str | Path) -> None:
         self.log_file(output_target)
 
     @abstractmethod
-    def log_file(self, file: str | Path) -> None:
+    def log_file(self, file: Union[str, Path]) -> None:
         """Logs a given file as part of an experiment.
 
         Args:
-            file (str | Path): The file to log.
+            file (Union[str, Path]): The file to log.
         """
         raise NotImplementedError
diff --git a/pype/base/logger/local_logger.py b/pype/base/logger/local_logger.py
index 4771add..631db59 100644
--- a/pype/base/logger/local_logger.py
+++ b/pype/base/logger/local_logger.py
@@ -1,6 +1,6 @@
 from pathlib import Path
 from types import TracebackType
-from typing import Any
+from typing import Any, Dict, Optional, Type, Union
 
 from .experiment_logger import ExperimentLogger
 
@@ -10,7 +10,7 @@ def __enter__(self) -> None:
         """Start the experiment."""
 
     def __exit__(
-        self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
+        self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
     ) -> None:
         """Stop the experiment."""
 
@@ -19,29 +19,29 @@ def __init__(self, min_whitespace: int = 30) -> None:
         super().__init__()
         self.min_whitespace = min_whitespace
 
-    def _log_metrics(self, metrics: dict[str, float | int | str | bool]) -> None:
+    def _log_metrics(self, metrics: Dict[str, Union[str, float, int, str, bool]]) -> None:
         """Prints the metrics to the console.
 
         Args:
-            metrics (dict[str, float | int | str | bool]): A dictionary of metric names and values.
+            metrics (Dict[str, Union[str, float, int, str, bool]]): A dictionary of metric names and values.
         """
         for name, value in metrics.items():
             print(f"{name.ljust(self.min_whitespace)}: {value}")
 
-    def log_parameters(self, parameters: dict[str, Any]) -> None:
+    def log_parameters(self, parameters: Dict[str, Any]) -> None:
         """Prints the parameters to the console.
 
         Args:
-            parameters (dict[str, Any]): The parameters of a given run. Ideally these
+            parameters (Dict[str, Any]): The parameters of a given run. Ideally these
                 parameters should be no more complicated than string, float, int, bool, or a
                 list of these.
         """
         for name, value in parameters.items():
             print(f"{name.ljust(self.min_whitespace)}: {value}")
 
-    def log_file(self, file: str | Path) -> None:
+    def log_file(self, file: Union[str, Path]) -> None:
         """This method does no additional logging.
 
         Args:
-            file (str | Path): The file to log.
+            file (Union[str, Path]): The file to log.
         """
diff --git a/pype/base/model/model.py b/pype/base/model/model.py
index 3aecbaa..29ba1b4 100644
--- a/pype/base/model/model.py
+++ b/pype/base/model/model.py
@@ -2,7 +2,7 @@
 from abc import ABC, abstractclassmethod, abstractmethod
 from argparse import ArgumentParser
 from pathlib import Path
-from typing import Generic, Tuple, TypeVar
+from typing import Generic, List, Tuple, TypeVar, Union
 
 from pype.base.constants import Constants
 from pype.base.data import DataSet
@@ -12,7 +12,7 @@
 
 
 class Model(ABC, Generic[Data]):
-    def __init__(self, inputs: list[str], outputs: list[str], seed: int = 1) -> None:
+    def __init__(self, inputs: List[str], outputs: List[str], seed: int = 1) -> None:
         """An abstraction of a ML model.
 
         This should provide the basic interface for fitting, inference, and serialisation.
@@ -43,7 +43,7 @@ def set_seed(self) -> None:
         """Sets the RNG seed."""
         raise NotImplementedError
 
-    def save(self, folder: str | Path) -> None:
+    def save(self, folder: Union[str, Path]) -> None:
         """Stores this model to the given folder.
 
         This function stores the common inputs and outputs list to the given folder,
@@ -51,7 +51,7 @@ def save(self, folder: str | Path) -> None:
         It will call _save to allow further models to specify how they are stored.
 
         Args:
-            folder (str | Path): The folder to store the Model in.
+            folder (Union[str, Path]): The folder to store the Model in.
         """
         joblib_serialiser = JoblibSerialiser()
 
@@ -81,11 +81,11 @@ def _save(self, folder: Path) -> None:
         raise NotImplementedError
 
     @classmethod
-    def load(cls, folder: str | Path) -> "Model":
+    def load(cls, folder: Union[str, Path]) -> "Model":
         """Loads a model from file into this Model.
 
         Args:
-            folder (str | Path): The folder to load the model from.
+            folder (Union[str, Path]): The folder to load the model from.
         """
         if isinstance(folder, str):
             folder = Path(folder)
@@ -98,11 +98,11 @@ def load(cls, folder: str | Path) -> "Model":
         return model_class._load(folder, lists["inputs"], lists["outputs"])
 
     @abstractclassmethod
-    def _load(cls, folder: Path, inputs: list[str], outputs: list[str]) -> "Model":
+    def _load(cls, folder: Path, inputs: List[str], outputs: List[str]) -> "Model":
         """Loads a model from file into this Model.
 
         Args:
-            folder (str | Path): The folder to load the model from.
+            folder (Union[str, Path]): The folder to load the model from.
         """
         raise NotImplementedError
 
@@ -140,7 +140,7 @@ def transform(self, data: DataSet) -> DataSet:
         return DataSet.from_dict({name: data for name, data in zip(self.outputs, result)})  # type: ignore
 
     @abstractmethod
-    def _transform(self, *data: Data) -> Tuple[Data] | Data:
+    def _transform(self, *data: Data) -> Union[Tuple[Data], Data]:
         raise NotImplementedError
 
     def __str__(self) -> str:
diff --git a/pype/base/pipeline/pipe.py b/pype/base/pipeline/pipe.py
index 37f0a7c..2132da8 100644
--- a/pype/base/pipeline/pipe.py
+++ b/pype/base/pipeline/pipe.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, Type
+from typing import Any, Dict, List, Optional, Type
 
 from pype.base.data import DataSet
 from pype.base.pipeline import Operator
@@ -9,10 +9,10 @@ def __init__(
         self,
         name: str,
         operator: Type[Operator],
-        inputs: list[str],
-        outputs: list[str],
-        kw_args: dict[str, Any] | None = None,
-        fit_inputs: list[str] | None = None,
+        inputs: List[str],
+        outputs: List[str],
+        kw_args: Optional[Dict[str, Any]] = None,
+        fit_inputs: Optional[List[str]] = None,
     ) -> None:
         """A single step in a Pipeline.
 
@@ -21,10 +21,10 @@ def __init__(
         Args:
             name (str): The name of the Pipe.
             operator (Type[Operator]): The Operator class that this Pipe should use.
-            inputs (list[str]): A list of input dataset names used by this Pipe.
-            outputs (list[str]): A list of output dataset names used by this Pipe.
-            kw_args (dict[str, Any] | None): keyword arguments to initialise the Operator.
-            fit_inputs: (list[str] | None): optional additional arguments to fit().
+            inputs (List[str]): A list of input dataset names used by this Pipe.
+            outputs (List[str]): A list of output dataset names used by this Pipe.
+            kw_args (Optional[Dict[str, Any]]): keyword arguments to initialise the Operator.
+            fit_inputs: (Optional[List[str]]): optional additional arguments to fit().
                 Will not be used in transform().
         """
         assert "__" not in name, "Pipe names cannot contain the string `__`"
@@ -108,11 +108,11 @@ def reinitialise(self, args: Dict[str, Any]) -> None:
         """
         self.operator = self.operator_class(**args)
 
-    def copy(self, args: Dict[str, Any] | None = None) -> "Pipe":
+    def copy(self, args: Optional[Dict[str, Any]] = None) -> "Pipe":
         """Create a copy of this Pipe's Operator given a dict of arguments.
 
         Args:
-            args (Dict[str, Any] | None): The dictionary of arguments to use in re-initialisation.
+            args (Optional[Dict[str, Any]]): The dictionary of arguments to use in re-initialisation.
                 If set to None, we'll use the same arguments as before.
 
         Returns:
diff --git a/pype/base/pipeline/pipeline.py b/pype/base/pipeline/pipeline.py
index a778754..3dd46f8 100644
--- a/pype/base/pipeline/pipeline.py
+++ b/pype/base/pipeline/pipeline.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List, Union
+from typing import Any, Dict, List, Optional, Union
 
 from pype.base.data import DataSet
 from pype.base.pipeline.pipe import Pipe
@@ -16,7 +16,7 @@ def __init__(self, pipes: List[Union[Pipe, "Pipeline"]]) -> None:
         self.pipes = pipes
         self._assert_all_names_different()
 
-    def _assert_all_names_different(self, names: set | None = None) -> None:
+    def _assert_all_names_different(self, names: Optional[set] = None) -> None:
         if names is None:
             names = set()
 
@@ -90,7 +90,7 @@ def reinitialise(self, args: Dict[str, Any]) -> None:
                 if len(pipe_args) > 0:
                     pipe.reinitialise(pipe_args)
 
-    def copy(self, args: Dict[str, Any] | None = None) -> "Pipeline":
+    def copy(self, args: Optional[Dict[str, Any]] = None) -> "Pipeline":
         """Create a copy of this Pipeline using the given dictionary.
 
         Args:
@@ -123,14 +123,14 @@ def __iter__(self) -> "Pipeline":
         self.__iter_n = 0
         return self
 
-    def __next__(self) -> "Pipeline | Pipe":
+    def __next__(self) -> Union["Pipeline", Pipe]:
         """Gets the next item in this Pipeline.
 
         Raises:
             StopIteration: If this object has already been iterated.
 
         Returns:
-            Pipeline | Pipe: The next Pipeline/Pipe.
+            Union[Pipeline, Pipe]: The next Pipeline/Pipe.
         """
         if self.__iter_n >= len(self.pipes):
             raise StopIteration
@@ -139,7 +139,7 @@ def __next__(self) -> "Pipeline | Pipe":
             self.__iter_n += 1
             return self[pos]
 
-    def __getitem__(self, pos: int | str) -> "Pipeline | Pipe":
+    def __getitem__(self, pos: Union[int, str]) -> Union["Pipeline", Pipe]:
         """Gets the Pipe/Pipeline at the given position.
 
         Raises:
@@ -150,14 +150,14 @@ def __getitem__(self, pos: int | str) -> "Pipeline | Pipe":
                     important to use different names for Pipes!
 
         Returns:
-            Pipeline | Pipe: The Pipeline/Pipe matching the given position.
+            Union[Pipeline, Pipe]: The Pipeline/Pipe matching the given position.
         """
         value = self._get(pos)
         if value is None:
             raise KeyError(f"{pos} is not found in this pipeline")
         return value
 
-    def _get(self, pos: int | str) -> "Pipeline | Pipe | None":
+    def _get(self, pos: Union[int, str]) -> Union["Pipeline", Pipe, None]:
         if isinstance(pos, int):
             return self.pipes[pos]
         for pipe in self.pipes:
@@ -169,11 +169,11 @@ def _get(self, pos: int | str) -> "Pipeline | Pipe | None":
                 return pipe
         return None
 
-    def __add__(self, obj: "Pipeline | Pipe") -> "Pipeline":
+    def __add__(self, obj: Union["Pipeline", Pipe]) -> "Pipeline":
         """Append a Pipe or Pipeline object and return a new Pipeline.
 
         Args:
-            obj (Pipeline | Pipe): The object to be appended. Can be a Pipeline or Pipe.
+            obj (Union[Pipeline, Pipe]): The object to be appended. Can be a Union[Pipeline, Pipe].
 
         Raises:
             TypeError: If `obj` is not a Pipe or Pipeline.
diff --git a/pype/base/pipeline/type_checker.py b/pype/base/pipeline/type_checker.py
index 787519b..85deb42 100644
--- a/pype/base/pipeline/type_checker.py
+++ b/pype/base/pipeline/type_checker.py
@@ -1,6 +1,6 @@
 import logging
 from abc import ABC, abstractclassmethod, abstractmethod
-from typing import Any
+from typing import Any, Dict, List, Optional, Tuple, Type, Union
 
 from pydantic import BaseModel, create_model
 
@@ -84,26 +84,26 @@ def transform(self, data: Data) -> Data:
         """
 
     @abstractmethod
-    def get_pydantic_type(self) -> type[DataModel]:
+    def get_pydantic_type(self) -> Type[DataModel]:
         """Returns a Pydantic type for data serialisation/deserialistion based on this pipe.
 
         Returns:
-            type[DataModel]: The data model for the data this was fitted on.
+            Type[DataModel]: The data model for the data this was fitted on.
         """
 
 
 class TypeCheckerPipe(Pipe):
     def __init__(
-        self, name: str, input_names: list[str], type_checker_classes: list[tuple[type, type[TypeChecker]]]
+        self, name: str, input_names: List[str], type_checker_classes: List[Tuple[type, Type[TypeChecker]]]
     ) -> None:
         """A pipe that fully checks an incoming DataSet for type consistency.
 
         Args:
             name (str): The name of the pipe.
-            input_names (list[str]): The names of datasets to be checked by this type checker.
+            input_names (List[str]): The names of datasets to be checked by this type checker.
                 We highly recommend to make sure this encompasses the datasets used by
                 the model at inference time (or the predicted datasets for the output type checker).
-            type_checker_classes (list[tuple[type, type[TypeChecker]]]): A list of pairs
+            type_checker_classes (List[Tuple[type, Type[TypeChecker]]]): A list of pairs
                 of data types and the type checker that should be used for that class.
                 E.g. (pandas.DataFrame, PandasTypeChecker) for pandas data.
         """
@@ -119,15 +119,15 @@ def __init__(
         )
         self.input_names = input_names
 
-    def get_pydantic_types(self, names: list[str] | None = None) -> type[DataSetModel]:
+    def get_pydantic_types(self, names: Optional[List[str]] = None) -> Type[DataSetModel]:
         """Generate a Pydantic model for the given dataset names.
 
         Args:
-            names (list[str] | None, optional): The type checker for which we want data models
+            names (Optional[List[str]]): The type checker for which we want data models
                 to be returned. Defaults to all input datasets.
 
         Returns:
-            type[DataSetModel]: A pydantic model to serialise/deserialise data for the given
+            Type[DataSetModel]: A pydantic model to serialise/deserialise data for the given
                 names.
         """
         if names is None:
@@ -137,23 +137,23 @@ def get_pydantic_types(self, names: list[str] | None = None) -> type[DataSetMode
 
 
 class DataSetTypeChecker(Operator[Data]):
-    def __init__(self, input_names: list[str], type_checker_classes: list[tuple[type, type[TypeChecker]]]) -> None:
+    def __init__(self, input_names: List[str], type_checker_classes: List[Tuple[type, Type[TypeChecker]]]) -> None:
         """A TypeChecker that conforms to the Operator class.
 
         Mainly used by TypeCheckerPipe for easy type checking of incoming data.
 
         Args:
-            input_names (list[str]): The names of datasets to be checked by this type checker.
+            input_names (List[str]): The names of datasets to be checked by this type checker.
                 We highly recommend to make sure this encompasses the datasets used by
                 the model at inference time (or the predicted datasets for the output type checker).
-            type_checker_classes (list[tuple[type, type[TypeChecker]]]): A list of pairs
+            type_checker_classes (List[Tuple[type, Type[TypeChecker]]]): A list of pairs
                 of data types and the type checker that should be used for that class.
                 E.g. (pandas.DataFrame, PandasTypeChecker) for pandas data.
         """
         super().__init__()
         self.input_names = input_names
         self.type_checker_classes = type_checker_classes
-        self.type_checkers: dict[str, TypeChecker] = {}
+        self.type_checkers: Dict[str, TypeChecker] = {}
 
     def fit(self, *data: Data) -> "Operator":
         """Fits all type checkers to the given data.
@@ -174,11 +174,11 @@ def fit(self, *data: Data) -> "Operator":
                 self.type_checkers[ds_name] = checker
         return self
 
-    def transform(self, *data: Data) -> tuple[Data, ...]:
+    def transform(self, *data: Data) -> Tuple[Data, ...]:
         """Checks if the given data fits the fitted Type Checkers.
 
         Returns:
-            tuple[Data, ...]: data, if all data fits the Type Checkers.
+            Tuple[Data, ...]: data, if all data fits the Type Checkers.
         """
         for ds_name, dataset in zip(self.input_names, data):
             assert ds_name in self.type_checkers, f"{ds_name} does not have a type checker"
@@ -188,21 +188,21 @@ def transform(self, *data: Data) -> tuple[Data, ...]:
 
         return data
 
-    def _get_type_checker(self, data: Data) -> type[TypeChecker] | None:
+    def _get_type_checker(self, data: Data) -> Union[Type[TypeChecker], None]:
         for ref_type, type_checker in self.type_checker_classes:
             if isinstance(data, ref_type):
                 return type_checker
         return None
 
-    def get_pydantic_types(self, names: list[str] | None = None) -> type[DataSetModel]:
+    def get_pydantic_types(self, names: Optional[List[str]] = None) -> Type[DataSetModel]:
         """Generates a DataModel for the given input datasets after fitting.
 
         Args:
-            names (list[str] | None, optional): The type checker for which we want data models
+            names (Optional[List[str]]): The type checker for which we want data models
                 to be returned. Defaults to all input datasets.
 
         Returns:
-            type[DataSetModel]: A pydantic model to serialise/deserialise data for the given
+            Type[DataSetModel]: A pydantic model to serialise/deserialise data for the given
                 input_names.
         """
         if names is None:
@@ -224,10 +224,10 @@ def get_pydantic_types(self, names: list[str] | None = None) -> type[DataSetMode
 #     Returns:
 #         _type_: _description_
 #     """
-#     registry: dict[str, tuple[type, type[TypeChecker]]] = {}
+#     registry: Dict[str, Tuple[type, Type[TypeChecker]]] = {}
 #
 #     @classmethod
-#     def get_type_checker(cls, type_: type) -> type[TypeChecker] | None:
+#     def get_type_checker(cls, type_: type) -> Type[TypeChecker] or None:
 #         for ref_type, type_checker in cls.registry.values():
 #             if ref_type == type_:
 #                 return type_checker
diff --git a/pype/base/serialiser/joblib_serialiser.py b/pype/base/serialiser/joblib_serialiser.py
index 75eea3a..dc8fedc 100644
--- a/pype/base/serialiser/joblib_serialiser.py
+++ b/pype/base/serialiser/joblib_serialiser.py
@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Any
+from typing import Any, Union
 
 from joblib import dump, load
 
@@ -7,20 +7,20 @@
 
 
 class JoblibSerialiser(Serialiser):
-    def serialise(self, object: Any, file: str | Path) -> None:
+    def serialise(self, object: Any, file: Union[str, Path]) -> None:
         """Serialise the given object to the given file.
 
         Args:
             object (Any): The object to serialise.
-            file (str | Path): The file to serialise to.
+            file (Union[str, Path]): The file to serialise to.
         """
         dump(object, file)
 
-    def deserialise(self, file: str | Path) -> Any:
+    def deserialise(self, file: Union[str, Path]) -> Any:
         """Deserialise the object in the given file.
 
         Args:
-            file (str | Path): The file to deserialise.
+            file (Union[str, Path]): The file to deserialise.
 
         Returns:
             Any: The python object stored in the file.
diff --git a/pype/base/serialiser/serialiser.py b/pype/base/serialiser/serialiser.py
index bb6777c..4bd0d5a 100644
--- a/pype/base/serialiser/serialiser.py
+++ b/pype/base/serialiser/serialiser.py
@@ -1,6 +1,6 @@
 from abc import ABC, abstractmethod
 from pathlib import Path
-from typing import Any
+from typing import Any, Union
 
 
 class Serialiser(ABC):
@@ -9,21 +9,21 @@ def __init__(self) -> None:
         super().__init__()
 
     @abstractmethod
-    def serialise(self, object: Any, file: str | Path) -> None:
+    def serialise(self, object: Any, file: Union[str, Path]) -> None:
         """Serialise the given object to the given file.
 
         Args:
             object (Any): The object to serialise.
-            file (str | Path): The file to serialise to.
+            file (Union[str, Path]): The file to serialise to.
         """
         raise NotImplementedError
 
     @abstractmethod
-    def deserialise(self, file: str | Path) -> Any:
+    def deserialise(self, file: Union[str, Path]) -> Any:
         """Deserialise the object in the given file.
 
         Args:
-            file (str | Path): The file to deserialise.
+            file (Union[str, Path]): The file to deserialise.
 
         Returns:
             Any: The python object stored in the file.
diff --git a/pype/base/setup.py b/pype/base/setup.py
index 7d54352..e02d689 100644
--- a/pype/base/setup.py
+++ b/pype/base/setup.py
@@ -10,5 +10,5 @@
     install_requires=deps,
     extras_require={"dev": strict_deps + dev_deps + test_deps, "test": strict_deps + test_deps, "strict": strict_deps},
     packages=find_namespace_packages(include=["pype.*"]),
-    version="0.1.0",
+    version="0.1.1",
 )
diff --git a/pype/base/utils/workspace.py b/pype/base/utils/workspace.py
index 8a031ab..e778d5c 100644
--- a/pype/base/utils/workspace.py
+++ b/pype/base/utils/workspace.py
@@ -5,11 +5,11 @@
 from contextlib import contextmanager
 from pathlib import Path
 from types import ModuleType
-from typing import Generator, Set
+from typing import Generator, List, Optional, Set, Union
 
 
 @contextmanager
-def switch_directory(directory: str | Path) -> Generator:
+def switch_directory(directory: Union[str, Path]) -> Generator:
     """Allows you to temporarily switch Python over to a different directory.
 
     This changes the following:
@@ -18,7 +18,7 @@ def switch_directory(directory: str | Path) -> Generator:
     These changes will be undone once the context manager ends.
 
     Args:
-        target_workspace (str | Path): The directory to switch to.
+        target_workspace (Union[str, Path]): The directory to switch to.
     """
     # create backups of the workspace and path.
     target_directory = Path(directory)
@@ -31,7 +31,9 @@ def switch_directory(directory: str | Path) -> Generator:
 
 
 @contextmanager
-def switch_workspace(target_workspace: str | Path, extra_files: list[str | Path] | None = None) -> Generator:
+def switch_workspace(
+    target_workspace: Union[str, Path], extra_files: Optional[List[Union[str, Path]]] = None
+) -> Generator:
     """Allows you to temporarily switch Python over to using a different working directory.
 
     This changes the following:
@@ -46,8 +48,8 @@ def switch_workspace(target_workspace: str | Path, extra_files: list[str | Path]
     These changes will be undone once the context manager ends.
 
     Args:
-        target_workspace (str | Path): The directory to switch to.
-        extra_files (list[str | Path] | None, optional): Any additional files whose contents should
+        target_workspace (Union[str, Path]): The directory to switch to.
+        extra_files (Optional[List[Union[str, Path]]]): Any additional files whose contents should
             be added to the current python program. Defaults to no extra files.
     """
     if extra_files is None:
@@ -68,7 +70,7 @@ def switch_workspace(target_workspace: str | Path, extra_files: list[str | Path]
 
 
 def _reset_workspace(
-    old_workspace: str, old_sys_path: list[str], main_lib: ModuleType, main_to_remove: Set[str]
+    old_workspace: str, old_sys_path: List[str], main_lib: ModuleType, main_to_remove: Set[str]
 ) -> None:
     os.chdir(old_workspace)
     sys.path = old_sys_path
@@ -77,13 +79,13 @@ def _reset_workspace(
 
 
 def _create_temporary_workspace(
-    target_workspace: Path, extra_files: list[str | Path], main_lib: ModuleType
+    target_workspace: Path, extra_files: List[Union[str, Path]], main_lib: ModuleType
 ) -> Set[str]:
     """Perform the actual workspace switching.
 
     Args:
         target_workspace (Path): The directory to switch to.
-        extra_files (list[str  |  Path]): Any additional files whose contents should
+        extra_files (List[Union[str ,  Path]]): Any additional files whose contents should
             be added to the current python program. Empty lists are accepted.
         main_lib (ModuleType): The __main__ module.
 
diff --git a/pype/fastapi/deploy/app.py b/pype/fastapi/deploy/app.py
index c454cfc..0c58949 100644
--- a/pype/fastapi/deploy/app.py
+++ b/pype/fastapi/deploy/app.py
@@ -1,7 +1,7 @@
 from dataclasses import dataclass
 from logging import Logger, getLogger
 from pathlib import Path
-from typing import Any
+from typing import Any, Dict, Optional, Type, Union
 
 from fastapi.applications import FastAPI
 from fastapi.background import BackgroundTasks
@@ -15,8 +15,8 @@
 @dataclass
 class PypeApp:
     name: str
-    folder: str | Path
-    tracking_servers: dict[str, DataSink] | None = None
+    folder: Union[str, Path]
+    tracking_servers: Optional[Dict[str, DataSink]] = None
 
     def __post_init__(self):  # type: ignore
         """Makes sure the folder is an actual Path."""
@@ -33,8 +33,8 @@ def create_app(self) -> FastAPI:
         inferencer = self._load_model()
         logger = getLogger(self.name)
 
-        InputType: type[DataSetModel] = inferencer.input_type_checker.get_pydantic_types()
-        OutputType: type[DataSetModel] = inferencer.output_type_checker.get_pydantic_types()
+        InputType: Type[DataSetModel] = inferencer.input_type_checker.get_pydantic_types()
+        OutputType: Type[DataSetModel] = inferencer.output_type_checker.get_pydantic_types()
         self._verify_tracking_servers(InputType, OutputType, logger)
 
         @app.get("/")
@@ -61,7 +61,7 @@ def _handle_tracking(self, inputs: DataSet, outputs: DataSet, logger: Logger, ba
             for name, sink in self.tracking_servers.items():
                 try:
                     # Try to find the name of the dataset in the tracking servers.
-                    ds: DataModel | None = None
+                    ds: Union[DataModel, None] = None
                     if name in outputs:
                         ds = outputs[name]
                     elif name in inputs:
@@ -78,8 +78,8 @@ def _handle_tracking(self, inputs: DataSet, outputs: DataSet, logger: Logger, ba
 
     def _verify_tracking_servers(
         self,
-        inputs: type[DataSetModel],
-        outputs: type[DataSetModel],
+        inputs: Type[DataSetModel],
+        outputs: Type[DataSetModel],
         logger: Logger,
     ) -> None:
         # Since it could be possible someone wants to log an intermediate dataset,
diff --git a/pype/fastapi/setup.py b/pype/fastapi/setup.py
index 19fdcd4..7a879cf 100644
--- a/pype/fastapi/setup.py
+++ b/pype/fastapi/setup.py
@@ -1,7 +1,9 @@
+from typing import List
+
 from setuptools import find_namespace_packages, setup
 
-version = "0.1.0"
-deps: list[str] = [f"pype-base=={version}", "fastapi>=0.79.0"]
+version = "0.1.1"
+deps: List[str] = [f"pype-base=={version}", "fastapi>=0.79.0"]
 dev_deps = ["uvicorn==0.18.2"]
 strict_deps = [s.replace(">=", "==") for s in deps]
 
diff --git a/pype/hyperopt/optimise.py b/pype/hyperopt/optimise.py
index 49edc4d..968b356 100644
--- a/pype/hyperopt/optimise.py
+++ b/pype/hyperopt/optimise.py
@@ -1,4 +1,4 @@
-from typing import Any, Callable
+from typing import Any, Callable, Dict, Optional, Tuple, Union
 
 from hyperopt.base import Trials
 from hyperopt.fmin import fmin, space_eval
@@ -9,37 +9,37 @@
 
 def create_optimisation_function(
     experiment_template: Experiment,
-    target_metric: tuple[str, str],
+    target_metric: Tuple[str, str],
     minimise_target: bool,
     seed: int,
-) -> Callable[[dict], float | int]:
+) -> Callable[[dict], Union[float, int]]:
     """Create an optimisation function that hyperopt can use to find the best model.
 
     Args:
         experiment_template (Experiment): An example Experiment that will be run
             multiple times to find the best hyperparameters. We will create clean copies
             of this Experiment for every run.
-        target_metric (tuple[str, str]): The target metric to optimise. Should be of the form
+        target_metric (Tuple[str, str]): The target metric to optimise. Should be of the form
             (dataset name, metric name). The dataset name needs to match one in the DataSet from the Experiment.
             The metric name should match a metric from the Evaluator in the Experiment.
         minimise_target (bool): A boolean indicating if the target metric should be minimised or not.
         seed (int): The seed used for training.
 
     Returns:
-        Callable[[dict], float | int]: The optimisation function to be used by hyperopt's fmin
+        Callable[[dict], Union[float, int]]: The optimisation function to be used by hyperopt's fmin
     """
 
-    def optimise(hyper_params: dict[str, Any]) -> float | int:
+    def optimise(hyper_params: Dict[str, Any]) -> Union[float, int]:
         """Optimise a given Experiment at the give hyperparameters.
 
         Args:
-            hyper_params (dict[str, Any]): The hyperparameters to initialise
+            hyper_params (Dict[str, Any]): The hyperparameters to initialise
                 a new Experiment with. Make sure these are properly prefixed with
                 model__ and pipeline__<pipe name>__, as you would initialise a
                 regular Experiment from arguments.
 
         Returns:
-            float | int: The performance of this Experiment on the given parameters
+            Union[float, int]: The performance of this Experiment on the given parameters
                 on one metric and dataset.
         """
         experiment = experiment_template.copy(hyper_params, seed=seed)
@@ -60,17 +60,17 @@ def optimise(hyper_params: dict[str, Any]) -> float | int:
 
 def optimise_experiment(
     experiment_template: Experiment,
-    search_space: dict[str, tuple[str, Callable]],
-    target_metric: tuple[str, str],
+    search_space: Dict[str, Tuple[str, Callable]],
+    target_metric: Tuple[str, str],
     max_evals: int,
     minimise_target: bool,
     trial_type: str = "normal",
-    mongo_url: str | None = None,
+    mongo_url: Optional[str] = None,
     mongo_exp_key: str = "experiment",
-    trials: Trials | None = None,
+    trials: Optional[Trials] = None,
     training_seed: int = 1,
     **kwargs: Any,
-) -> tuple[int | None, float | None, dict[str, Any], Trials]:
+) -> Tuple[Union[int, None], Union[float, None], Dict[str, Any], Trials]:
     """Optimise a pype Experiment given a search space using Hyperopt.
 
     We attempt to create a clean copy of Experiments between runs to preserve run independency.
@@ -82,12 +82,12 @@ def optimise_experiment(
             We try to ensure a new, clean Experiment is created for each run. Please allow this by
             not sharing important, trained variables between Models or Operators of the same class.
             Each call to any DataSource provided should also return the same dataset.
-        search_space (dict[str, tuple[str, Callable]]): A search space defined in the standard hyperopt way.
+        search_space (Dict[str, Tuple[str, Callable]]): A search space defined in the standard hyperopt way.
             We suggest providing this as a dictionary in the form:
                 {<argument name>: (<name>, <hyperopt callable like hp.choice(...)>)}
             Make sure these are properly prefixed with model__ and pipeline__<pipe name>__, as you would
             initialise a regular Experiment from arguments.
-        target_metric (tuple[str, str]): The target metric to optimise. Should be of the form
+        target_metric (Tuple[str, str]): The target metric to optimise. Should be of the form
             (dataset name, metric name). The dataset name needs to match one in the DataSet from the Experiment.
             The metric name should match a metric from the Evaluator in the Experiment.
         max_evals (int): The maximum number of runs to perform for this hyperoptimisation search. Note that
@@ -101,13 +101,14 @@ def optimise_experiment(
                 # TODO: SparkTrials integration.
                 - 'spark': A SparkTrials object will be created. Currently not supported yet, WIP. Providing your own
                     SparkTrials object should still work.
-        mongo_url (str | None): The link to the mongodb that should be used. Only used if `trial_type` == 'mongo', and
-            is required in that case.
+        mongo_url (Optional[str]): The link to the mongodb that should be used.
+            Only used if `trial_type` == 'mongo', and is required in that case.
         mongo_exp_key (str): The experiment name in the mongodb that should be used.
             Only used if `trial_type` == 'mongo'. We encourage you to set this to distinguish between runs.
             The default is "experiment"
-        trials (Trials | None): An existing Trials object with previous runs of the optimisation. Useful if you want to
-            continue an old optimisation run without losing information. By default a new Trials object will be created.
+        trials (Optional[Trials]): An existing Trials object with previous runs of the optimisation.
+            Useful if you want to continue an old optimisation run without losing information.
+            By default a new Trials object will be created.
         training_seed (int): Seed used to initialise a new Experiment (and run).
         **kwargs: Additional arguments to be provided to hyperopt's `fmin`.
 
diff --git a/pype/hyperopt/setup.py b/pype/hyperopt/setup.py
index a6170e2..2dce7ac 100644
--- a/pype/hyperopt/setup.py
+++ b/pype/hyperopt/setup.py
@@ -1,7 +1,9 @@
+from typing import List
+
 from setuptools import find_namespace_packages, setup
 
-version = "0.1.0"
-deps: list[str] = [f"pype-base=={version}", "hyperopt>=0.2.7"]
+version = "0.1.1"
+deps: List[str] = [f"pype-base=={version}", "hyperopt>=0.2.7"]
 strict_deps = [s.replace(">=", "==") for s in deps]
 
 setup(
diff --git a/pype/mlflow/deploy/load_experiment.py b/pype/mlflow/deploy/load_experiment.py
index dff92a8..bb5ebe4 100644
--- a/pype/mlflow/deploy/load_experiment.py
+++ b/pype/mlflow/deploy/load_experiment.py
@@ -1,5 +1,6 @@
 from pathlib import Path
 from tempfile import TemporaryDirectory
+from typing import Optional, Union
 
 from mlflow.artifacts import download_artifacts
 from mlflow.tracking import set_tracking_uri
@@ -12,7 +13,7 @@ def load_experiment_from_mlflow(
     url: str,
     experiment_name: str,
     run_id: str,
-    directory: Path | str | None = None,
+    directory: Optional[Union[Path, str]] = None,
 ) -> Inferencer:
     """Download and import a trained Pype Model from mlflow's artifact store.
 
@@ -21,7 +22,7 @@ def load_experiment_from_mlflow(
         experiment_name (str): The name of the experiment. Used to verify that the
             run_id is the correct one.
         run_id (str): The run id. Used to pull the actual experiment.
-        directory (Path | str | None, optional): Optional directory to store the results
+        directory (Optional[Union[Path, str]]): Optional directory to store the results
             in. By default, a temporary directory is used.
 
     Returns:
diff --git a/pype/mlflow/logger/mlflow_logger.py b/pype/mlflow/logger/mlflow_logger.py
index fce7dc6..ed81ca5 100644
--- a/pype/mlflow/logger/mlflow_logger.py
+++ b/pype/mlflow/logger/mlflow_logger.py
@@ -1,7 +1,7 @@
 import logging
 from pathlib import Path
 from types import TracebackType
-from typing import Any
+from typing import Any, Dict, Optional, Type, Union
 
 from git import InvalidGitRepositoryError
 from git.repo import Repo
@@ -19,7 +19,7 @@
 
 
 class MlflowLogger(ExperimentLogger):
-    def __init__(self, name: str, uri: str, artifact_location: str | None = None) -> None:
+    def __init__(self, name: str, uri: str, artifact_location: Optional[str] = None) -> None:
         """A logger using mlflow for pype.
 
         Args:
@@ -27,7 +27,7 @@ def __init__(self, name: str, uri: str, artifact_location: str | None = None) ->
                 this with a workspace, like /Users/<your databricks user name>/<experiment name>.
             uri (str): The tracking uri. Should most likely start with `http://` or `databricks`.
                 It can also equal `databricks`.
-            artifact_location (str | None, optional): The artificat uri location. This is needed if you set up
+            artifact_location (Optional[str]): The artificat uri location. This is needed if you set up
                 your own artifact location, to make sure files are copied over to any remote tracking server.
                 This is not required when using hosted databricks. Defaults to None, like in
         """
@@ -69,7 +69,7 @@ def log_branch(self) -> None:
 
         set_tag("git_branch", branch_name)
 
-    def _find_encapsulating_repo(self, directory: Path) -> Repo | None:
+    def _find_encapsulating_repo(self, directory: Path) -> Union[Repo, None]:
         """Finds the current repo we're in, if any.
 
         We'll recursively move up from `directory` until we find a valid
@@ -80,7 +80,7 @@ def _find_encapsulating_repo(self, directory: Path) -> Repo | None:
                 to the current directory.
 
         Returns:
-            Repo | None: None if no repo is found, otherwise a GitPython Repo.
+            Repo or None: None if no repo is found, otherwise a GitPython Repo.
         """
         repo = None
         directory = directory.absolute()
@@ -93,39 +93,39 @@ def _find_encapsulating_repo(self, directory: Path) -> Repo | None:
         return repo
 
     def __exit__(
-        self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
+        self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
     ) -> None:
         """Exits a run started by __enter__.
 
         Args:
-            exc_type (type[BaseException] | None): Passed to ActiveRun.__exit__()
-            exc_val (BaseException | None): Passed to ActiveRun.__exit__()
-            exc_tb (TracebackType | None): Passed to ActiveRun.__exit__()
+            exc_type (Optional[Type[BaseException]]): Passed to ActiveRun.__exit__()
+            exc_val (Optional[BaseException]): Passed to ActiveRun.__exit__()
+            exc_tb (Optional[TracebackType]): Passed to ActiveRun.__exit__()
         """
         assert self.run is not None, "Run not started, cannot exit!"
         self.run.__exit__(exc_type, exc_val, exc_tb)
 
-    def _log_metrics(self, metrics: dict[str, float | int | str | bool]) -> None:
+    def _log_metrics(self, metrics: Dict[str, Union[str, float, int, str, bool]]) -> None:
         """Perform the actual logging of metrics in mlflow.
 
         Args:
-            metrics (dict[str, float | int | str | bool]): A dictionary of metric names and values.
+            metrics (Dict[str, Union[str, float, int, str, bool]]): A dictionary of metric names and values.
         """
         log_metrics(metrics)
 
-    def log_parameters(self, parameters: dict[str, Any]) -> None:
+    def log_parameters(self, parameters: Dict[str, Any]) -> None:
         """Logs the parameters for a given run.
 
         These will by default be passed on with prefixes such as `model__`.
 
         Args:
-            parameters (dict[str, Any]): The parameters of a given run. Ideally these
+            parameters (Dict[str, Any]): The parameters of a given run. Ideally these
                 parameters should be no more complicated than string, float, int, bool, or a
                 list of these.
         """
         log_params(parameters)
 
-    def log_file(self, file: str | Path) -> None:
+    def log_file(self, file: Union[str, Path]) -> None:
         """Logs a given file as part of an experiment.
 
         This only works well if:
@@ -134,6 +134,6 @@ def log_file(self, file: str | Path) -> None:
             - you have run mlflow ui/server at the same folder where mlruns is located.
 
         Args:
-            file (str | Path): The file to log.
+            file (Union[str, Path]): The file to log.
         """
         log_artifact(str(file))
diff --git a/pype/mlflow/setup.py b/pype/mlflow/setup.py
index b6943d9..c489578 100644
--- a/pype/mlflow/setup.py
+++ b/pype/mlflow/setup.py
@@ -1,6 +1,6 @@
 from setuptools import find_namespace_packages, setup
 
-version = "0.1.0"
+version = "0.1.1"
 
 deps = [
     f"pype-base=={version}",
diff --git a/pype/sklearn/data/sklearn_data.py b/pype/sklearn/data/sklearn_data.py
index ece9c21..90c7d8f 100644
--- a/pype/sklearn/data/sklearn_data.py
+++ b/pype/sklearn/data/sklearn_data.py
@@ -1,4 +1,4 @@
 import numpy as np
 import pandas as pd
 
-SklearnData = pd.DataFrame | np.ndarray
+SklearnData = pd.DataFrame or np.ndarray
diff --git a/pype/sklearn/model/linear_regression_model.py b/pype/sklearn/model/linear_regression_model.py
index 5561122..7c5fcd5 100644
--- a/pype/sklearn/model/linear_regression_model.py
+++ b/pype/sklearn/model/linear_regression_model.py
@@ -1,4 +1,4 @@
-from typing import Any
+from typing import Any, Dict
 
 from sklearn.linear_model import LinearRegression
 
@@ -6,5 +6,5 @@
 
 
 class LinearRegressionModel(SklearnModel[LinearRegression]):
-    def _init_model(self, args: dict[str, Any]) -> LinearRegression:
+    def _init_model(self, args: Dict[str, Any]) -> LinearRegression:
         return LinearRegression(**args)
diff --git a/pype/sklearn/model/logistic_regression_model.py b/pype/sklearn/model/logistic_regression_model.py
index e719b0b..608e61a 100644
--- a/pype/sklearn/model/logistic_regression_model.py
+++ b/pype/sklearn/model/logistic_regression_model.py
@@ -1,4 +1,4 @@
-from typing import Any
+from typing import Any, Dict
 
 from sklearn.linear_model import LogisticRegression
 
@@ -6,5 +6,5 @@
 
 
 class LogisticRegressionModel(SklearnModel[LogisticRegression]):
-    def _init_model(self, args: dict[str, Any]) -> LogisticRegression:
+    def _init_model(self, args: Dict[str, Any]) -> LogisticRegression:
         return LogisticRegression(**args)
diff --git a/pype/sklearn/model/sklearn_base_type.py b/pype/sklearn/model/sklearn_base_type.py
index 46b03f3..e58b0a2 100644
--- a/pype/sklearn/model/sklearn_base_type.py
+++ b/pype/sklearn/model/sklearn_base_type.py
@@ -1,5 +1,5 @@
 from abc import ABC, abstractmethod
-from typing import Any, Iterable
+from typing import Any, Iterable, Union
 
 from pype.sklearn.data.sklearn_data import SklearnData
 
@@ -10,5 +10,5 @@ def fit(self, *x: SklearnData) -> Any:
         """Fit a model to the given data."""
 
     @abstractmethod
-    def predict(self, *x: SklearnData) -> Iterable[SklearnData] | SklearnData:
+    def predict(self, *x: SklearnData) -> Union[Iterable[SklearnData], SklearnData]:
         """Predict for given data using a trained model."""
diff --git a/pype/sklearn/model/sklearn_model.py b/pype/sklearn/model/sklearn_model.py
index 62c9a9e..431a56d 100644
--- a/pype/sklearn/model/sklearn_model.py
+++ b/pype/sklearn/model/sklearn_model.py
@@ -2,7 +2,7 @@
 from abc import ABC, abstractmethod
 from argparse import ArgumentParser
 from pathlib import Path
-from typing import Any, Generic, Iterable, Type, TypeVar
+from typing import Any, Dict, Generic, Iterable, List, Optional, Type, TypeVar, Union
 
 import numpy as np
 
@@ -20,9 +20,9 @@ class SklearnModel(Model[SklearnData], ABC, Generic[T]):
 
     def __init__(
         self,
-        inputs: list[str],
-        outputs: list[str],
-        model: T | None = None,
+        inputs: List[str],
+        outputs: List[str],
+        model: Optional[T] = None,
         seed: int = 1,
         **model_args: Any,
     ) -> None:
@@ -42,7 +42,7 @@ def _init_model(self, **args) -> LinearRegression:
                 used to fit the model.
             outputs (List[str]): A list of names of output Data. This determines the names of
                 output variables.
-            model (SklearnModelBaseType | None): An object that has fit() and predict() methods. If none,
+            model (Optional[SklearnModelBaseType]): An object that has fit() and predict() methods. If none,
                 we will use the model_args to instantiate a new model.
             seed (int, optional): The RNG seed to ensure reproducability.. Defaults to 1.
             **model_args (Any): Optional keyword arguments passed to the model class to instantiate a new
@@ -54,7 +54,7 @@ def _init_model(self, **args) -> LinearRegression:
         self.model = model
 
     @abstractmethod
-    def _init_model(self, args: dict[str, Any]) -> T:
+    def _init_model(self, args: Dict[str, Any]) -> T:
         raise NotImplementedError
 
     @classmethod
@@ -68,7 +68,7 @@ def set_seed(self) -> None:
     def _fit(self, *data: SklearnData) -> None:
         self.model.fit(*data)
 
-    def _transform(self, *data: SklearnData) -> Iterable[SklearnData] | SklearnData:
+    def _transform(self, *data: SklearnData) -> Union[Iterable[SklearnData], SklearnData]:
         return self.model.predict(*data)
 
     def _save(self, folder: Path) -> None:
@@ -76,7 +76,7 @@ def _save(self, folder: Path) -> None:
         serialiser.serialise(self.model, folder / self.SKLEARN_MODEL_FILE)
 
     @classmethod
-    def _load(cls: Type["SklearnModel"], folder: Path, inputs: list[str], outputs: list[str]) -> "SklearnModel":
+    def _load(cls: Type["SklearnModel"], folder: Path, inputs: List[str], outputs: List[str]) -> "SklearnModel":
         serialiser = JoblibSerialiser()
         model = serialiser.deserialise(folder / cls.SKLEARN_MODEL_FILE)
         return cls(inputs=inputs, outputs=outputs, model=model, seed=1)
diff --git a/pype/sklearn/pipeline/numpy_type_checker.py b/pype/sklearn/pipeline/numpy_type_checker.py
index 89a48f5..16760cf 100644
--- a/pype/sklearn/pipeline/numpy_type_checker.py
+++ b/pype/sklearn/pipeline/numpy_type_checker.py
@@ -1,3 +1,5 @@
+from typing import List, Type, Union
+
 import numpy as np
 from pydantic import create_model
 
@@ -38,7 +40,7 @@ def __init__(self) -> None:
         """
         super().__init__()
         self.dims: tuple = tuple()
-        self.dtype: type | None = None
+        self.dtype: Union[type, None] = None
 
     def fit(self, data: np.ndarray) -> "NumpyTypeChecker":
         """Fit this Numpy TypeChecker to the given data.
@@ -81,16 +83,16 @@ def _convert_dtype(self, dtype: np.dtype) -> type:
         else:
             return str
 
-    def get_pydantic_type(self) -> type[NumpyData]:
+    def get_pydantic_type(self) -> Type[NumpyData]:
         """Creates a Pydantic model for this data to handle serialisation/deserialisation.
 
         Returns:
-            type[NumpyData]: A NumpyData model that fits the data this wat fitted on.
+            Type[NumpyData]: A NumpyData model that fits the data this wat fitted on.
         """
-        base_iter: type = list[self.dtype]  # type: ignore
+        base_iter: type = List[self.dtype]  # type: ignore
 
         for _ in range(len(self.dims)):
-            base_iter = list[base_iter]  # type: ignore
+            base_iter = List[base_iter]  # type: ignore
 
         model = create_model("NumpyData", data=(base_iter, ...), __base__=NumpyData)
 
diff --git a/pype/sklearn/pipeline/pandas_type_checker.py b/pype/sklearn/pipeline/pandas_type_checker.py
index 71e4590..aeea122 100644
--- a/pype/sklearn/pipeline/pandas_type_checker.py
+++ b/pype/sklearn/pipeline/pandas_type_checker.py
@@ -1,5 +1,5 @@
 from datetime import datetime
-from typing import Callable
+from typing import Callable, Dict, List, Tuple, Type, Union
 
 import numpy as np
 import pandas as pd
@@ -70,10 +70,10 @@ def transform(self, data: pd.DataFrame) -> pd.DataFrame:
             assert checker(data[name]), f"Dtypes did not match up for col {name}."
         return data
 
-    def _convert_raw_types(self, types: dict[str, type]) -> dict[str, tuple[type, Callable]]:
+    def _convert_raw_types(self, types: Dict[str, type]) -> Dict[str, Tuple[type, Callable]]:
         return {name: self._convert_raw_type(type_) for name, type_ in types.items()}
 
-    def _convert_raw_type(self, type_: type) -> tuple[type, Callable]:
+    def _convert_raw_type(self, type_: type) -> Tuple[type, Callable]:
         str_type = str(type_)
         if "int" in str_type:
             return (int, is_integer_dtype)
@@ -88,14 +88,14 @@ def _convert_raw_type(self, type_: type) -> tuple[type, Callable]:
         else:
             return (str, is_object_dtype)
 
-    def get_pydantic_type(self) -> type[PandasData]:
+    def get_pydantic_type(self) -> Type[PandasData]:
         """Creates a Pydantic model for this data to handle serialisation/deserialisation.
 
         Returns:
-            type[PandasData]: A PandasData model that fits the data this wat fitted on.
+            Type[PandasData]: A PandasData model that fits the data this wat fitted on.
         """
         data_type = {
-            name: (list[dtype] | dict[str | int, dtype], ...)  # type: ignore
+            name: (Union[List[dtype], Dict[str or int, dtype]], ...)  # type: ignore
             for name, (dtype, _) in self.raw_types.items()
         }
 
diff --git a/pype/sklearn/setup.py b/pype/sklearn/setup.py
index 4c3e3f8..7dc1cc2 100644
--- a/pype/sklearn/setup.py
+++ b/pype/sklearn/setup.py
@@ -1,6 +1,6 @@
 from setuptools import find_namespace_packages, setup
 
-version = "0.1.0"
+version = "0.1.1"
 
 deps = [
     f"pype-base=={version}",
diff --git a/pype/spark/data/spark_read.py b/pype/spark/data/spark_read.py
index da61db0..81e8619 100644
--- a/pype/spark/data/spark_read.py
+++ b/pype/spark/data/spark_read.py
@@ -1,4 +1,4 @@
-from typing import Any
+from typing import Any, Dict
 
 from pyspark.sql import DataFrame as SparkDataFrame
 from pyspark.sql import SparkSession
@@ -12,8 +12,16 @@ def __init__(
         spark_session: SparkSession,
         file: str,
         format: str,
-        options: dict[str, Any],
+        options: Dict[str, Any],
     ) -> None:
+        """Read a file through Spark.
+
+        Args:
+            spark_session (SparkSession): The active SparkSessionl.
+            file (str): The file path.
+            format (str): The file format.
+            options (Dict[str, Any]): Any additional options for `load`.
+        """
         super().__init__()
         self.spark_session = spark_session
         self.file = file
@@ -21,4 +29,9 @@ def __init__(
         self.options = options
 
     def read(self) -> SparkDataFrame:
+        """Read the file at the given location.
+
+        Returns:
+            SparkDataFrame: A DataFrame based on the given file.
+        """
         return self.spark_session.read.format(self.format).load(self.file, **self.options)
diff --git a/pype/spark/model/linear_spark_model.py b/pype/spark/model/linear_spark_model.py
index 6562112..230afc1 100644
--- a/pype/spark/model/linear_spark_model.py
+++ b/pype/spark/model/linear_spark_model.py
@@ -1,4 +1,4 @@
-from typing import Any
+from typing import Any, Dict
 
 from pyspark.ml.regression import LinearRegression
 
@@ -6,5 +6,5 @@
 
 
 class LinearSparkModel(SparkModel[LinearRegression]):
-    def _init_model(self, args: dict[str, Any]) -> LinearRegression:
+    def _init_model(self, args: Dict[str, Any]) -> LinearRegression:
         return LinearRegression(**args)
diff --git a/pype/spark/model/spark_model.py b/pype/spark/model/spark_model.py
index a81cc39..b0c80da 100644
--- a/pype/spark/model/spark_model.py
+++ b/pype/spark/model/spark_model.py
@@ -3,7 +3,7 @@
 from abc import ABC, abstractmethod
 from argparse import ArgumentParser
 from pathlib import Path
-from typing import Any, Generic, Iterable, Type, TypeVar
+from typing import Any, Dict, Generic, Iterable, List, Optional, Type, TypeVar, Union
 
 from pyspark.ml import Model as BaseSparkModel
 from pyspark.ml import Predictor
@@ -25,25 +25,25 @@ class SparkModel(Model[SparkDataFrame], ABC, Generic[T]):
 
     def __init__(
         self,
-        inputs: list[str],
-        outputs: list[str],
-        output_col: str | None = None,
-        model: BaseSparkModel | None = None,
-        predictor: T | None = None,
+        inputs: List[str],
+        outputs: List[str],
+        output_col: Optional[str] = None,
+        model: Optional[BaseSparkModel] = None,
+        predictor: Optional[T] = None,
         seed: int = 1,
         **model_args: Any,
     ) -> None:
         """A Pype-compliant framework for using Spark Models.
 
         Args:
-            inputs (list[str]): The name of the input DataFrame. Should be a list of 1 string.
-            outputs (list[str]): The name of the output DataFrame. Should be a list of 1 string, the same as `inputs`
-            output_col (str | None, optional): The name of the column where the model will put the output.
+            inputs (List[str]): The name of the input DataFrame. Should be a list of 1 string.
+            outputs (List[str]): The name of the output DataFrame. Should be a list of 1 string, the same as `inputs`
+            output_col (Optional[str]): The name of the column where the model will put the output.
                 Defaults to None, which means we won't select any columns and instead return the full output
                 of the model.
-            predictor (Predictor, optional): The Spark Predictor. If not set, we try to instantiate it
+            predictor (Optional[Predictor]): The Spark Predictor. If not set, we try to instantiate it
                 using `model_args`
-            model (BaseSparkModel, optional): The Spark Model. Defaults to None. If set to None,
+            model (Optional[BaseSparkModel]): The Spark Model. Defaults to None. If set to None,
                 this model can't be serialised or used for inference.
             seed (int, optional): Spark Seed. Currently ignored, unfortunately. Defaults to 1.
         """
@@ -59,7 +59,7 @@ def __init__(
         self.output_col = output_col
 
     @abstractmethod
-    def _init_model(self, args: dict[str, Any]) -> T:
+    def _init_model(self, args: Dict[str, Any]) -> T:
         raise NotImplementedError
 
     @classmethod
@@ -87,7 +87,7 @@ def _fit(self, *data: SparkDataFrame) -> None:
         assert len(data) == 1, f"SparkML needs a single DataFrame as input, got {len(data)}"
         self.model = self.predictor.fit(data[0])
 
-    def _transform(self, *data: SparkDataFrame) -> Iterable[SparkDataFrame] | SparkDataFrame:
+    def _transform(self, *data: SparkDataFrame) -> Union[Iterable[SparkDataFrame], SparkDataFrame]:
         assert len(data) == 1, f"SparkML needs a single DataFrame as input, got {len(data)}"
         assert self.model is not None, "Please fit this model before transforming data."
 
@@ -109,14 +109,14 @@ def _save(self, folder: Path) -> None:
         serialiser.serialise(type(self.model), str(folder / self.SPARK_MODEL_CLASS_PATH))
 
     @classmethod
-    def _load(cls: Type["SparkModel"], folder: Path, inputs: list[str], outputs: list[str]) -> "SparkModel":
+    def _load(cls: Type["SparkModel"], folder: Path, inputs: List[str], outputs: List[str]) -> "SparkModel":
         serialiser = JoblibSerialiser()
 
         with open(folder / cls.PYPE_MODEL_CONFIG, "r") as f:
             config = json.load(f)
         output_col = config["output_col"]
 
-        predictor_class: type[Predictor] = cls._get_annotated_class()
+        predictor_class: Type[Predictor] = cls._get_annotated_class()
         model_class: Type[BaseSparkModel] = serialiser.deserialise(str(folder / cls.SPARK_MODEL_CLASS_PATH))
 
         predictor: Predictor = predictor_class.load(str(folder / cls.SPARK_PREDICTOR_PATH))
diff --git a/pype/spark/pipeline/spark_pipe.py b/pype/spark/pipeline/spark_pipe.py
index 84480a6..2976972 100644
--- a/pype/spark/pipeline/spark_pipe.py
+++ b/pype/spark/pipeline/spark_pipe.py
@@ -1,4 +1,4 @@
-from typing import Any, Type
+from typing import Any, Dict, List, Optional, Type, Union
 
 from pyspark.ml import Estimator, Transformer
 
@@ -12,10 +12,10 @@ def __init__(
         self,
         name: str,
         operator: Type[Operator],
-        inputs: list[str],
-        outputs: list[str],
-        kw_args: dict[str, Any] | None = None,
-        fit_inputs: list[str] | None = None,
+        inputs: List[str],
+        outputs: List[str],
+        kw_args: Optional[Dict[str, Any]] = None,
+        fit_inputs: Optional[List[str]] = None,
     ) -> None:
         """Same init as Pipe."""
         super().__init__(name, operator, inputs, outputs, kw_args, fit_inputs)
@@ -30,7 +30,7 @@ def fit(self, data: DataSet) -> "SparkPipe":
         Returns:
             SparkPipe: This object.
         """
-        op: Transformer | Estimator = self.operator
+        op: Union[Transformer, Estimator] = self.operator
 
         if isinstance(op, Transformer):
             self.fitted = op
@@ -61,25 +61,25 @@ def transform(self, data: DataSet) -> DataSet:
         result.set_all(self.outputs, transformed)
         return result
 
-    def __setstate__(self, state: dict[str, Any]) -> None:
+    def __setstate__(self, state: Dict[str, Any]) -> None:
         """Sets the state of this object from a dictionary.
 
         Used by pickle to properly prepare the "fitted" and "operator" fields as None's.
 
         Args:
-            state (dict[str, Any]): _description_
+            state (Dict[str, Any]): _description_
         """
         state["fitted"] = None
         state["operator"] = None
         self.__dict__ = state
 
-    def __getstate__(self) -> dict[str, Any]:
+    def __getstate__(self) -> Dict[str, Any]:
         """Gets the state of this object, excluding any Spark objects.
 
         This is to make sure serialisation works as expected.
 
         Returns:
-            dict[str, Any]: A dict representation of this object.
+            Dict[str, Any]: A dict representation of this object.
         """
         dct = self.__dict__.copy()
         if "fitted" in dct:
diff --git a/pype/spark/pipeline/spark_type_checker.py b/pype/spark/pipeline/spark_type_checker.py
index 569d7cf..cc76f7c 100644
--- a/pype/spark/pipeline/spark_type_checker.py
+++ b/pype/spark/pipeline/spark_type_checker.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List, Type
+from typing import Any, Dict, List, Tuple, Type, Union
 
 from pydantic import create_model
 from pyspark.sql import DataFrame as SparkDataFrame
@@ -64,11 +64,11 @@ def fit(self, data: SparkDataFrame) -> "SparkTypeChecker":
         self.raw_types = self._convert_dtypes(dict(data.dtypes))
         return self
 
-    def _convert_dtypes(self, dct: dict[str, str]) -> dict[str, tuple[str, type]]:
+    def _convert_dtypes(self, dct: Dict[str, str]) -> Dict[str, Tuple[str, type]]:
         return {name: self._convert_dtype(string_type) for name, string_type in dct.items()}
 
-    def _convert_dtype(self, string_type: str) -> tuple[str, type]:
-        conv_type: Type | None = None
+    def _convert_dtype(self, string_type: str) -> Tuple[str, type]:
+        conv_type: Union[Type, None] = None
         if string_type in ["str", "string"]:
             conv_type = str
         elif string_type in ["int", "bigint"]:
@@ -106,14 +106,14 @@ def transform(self, data: SparkDataFrame) -> SparkDataFrame:
             ), f"Dtypes did not match up for col {name}: Expected {spark_type}, got {actual_dtype}"
         return data
 
-    def get_pydantic_type(self) -> type[SparkData]:
+    def get_pydantic_type(self) -> Type[SparkData]:
         """Creates a Pydantic model for this data to handle serialisation/deserialisation.
 
         Returns:
-            type[SparkData]: A SparkData model that fits the data this wat fitted on.
+            Type[SparkData]: A SparkData model that fits the data this wat fitted on.
         """
         data_type = {
-            name: (list[dtype] | dict[str | int, dtype], ...)  # type: ignore
+            name: (Union[List[dtype], Dict[str or int, dtype]], ...)  # type: ignore
             for name, (_, dtype) in self.raw_types.items()
         }
 
diff --git a/pype/spark/serialisation/spark_serialiser.py b/pype/spark/serialisation/spark_serialiser.py
index f24341b..ee6eff3 100644
--- a/pype/spark/serialisation/spark_serialiser.py
+++ b/pype/spark/serialisation/spark_serialiser.py
@@ -1,6 +1,6 @@
 import json
 from pathlib import Path
-from typing import Any
+from typing import Any, List, Type, Union
 
 import joblib
 from pyspark.ml import Transformer
@@ -17,7 +17,7 @@ class SparkSerialiser(Serialiser):
     SPARK_TRANSFORMER_CLASS_FILE = "spark_transformer_class"
     STEPS_FILE = "steps.json"
 
-    def serialise(self, object: Any, file: str | Path) -> None:
+    def serialise(self, object: Any, file: Union[str, Path]) -> None:
         """Serialse a given object.
 
         If the object is:
@@ -26,20 +26,20 @@ def serialise(self, object: Any, file: str | Path) -> None:
 
         Args:
             object (Any): The object to serialise.
-            file (str | Path): The path to serialise to.
+            file (Union[str, Path]): The path to serialise to.
         """
         if isinstance(object, Pipeline):
             self._serialise_pipeline(object, file)
         else:
             return self._serialise_joblib(object, file)
 
-    def deserialise(self, file: str | Path) -> Any:
+    def deserialise(self, file: Union[str, Path]) -> Any:
         """Deserialise the object in the given file.
 
         This function can handle Pype Pipelines with PySpark elements.
 
         Args:
-            file (str | Path): The file containing a python object to deserialise.
+            file (Union[str, Path]): The file containing a python object to deserialise.
 
         Returns:
             Any: The deserialised object.
@@ -50,7 +50,7 @@ def deserialise(self, file: str | Path) -> Any:
         else:
             return self._deserialise_joblib(file)
 
-    def _serialise_pipeline(self, pipeline: Pipeline, file: str | Path) -> None:
+    def _serialise_pipeline(self, pipeline: Pipeline, file: Union[str, Path]) -> None:
         file = Path(file)
 
         file.mkdir(exist_ok=True)
@@ -80,21 +80,21 @@ def _serialise_pipeline(self, pipeline: Pipeline, file: str | Path) -> None:
         with open(file / self.STEPS_FILE, "w") as f:
             json.dump({"steps": steps}, f)
 
-    def _serialise_joblib(self, object: Any, file: str | Path) -> None:
+    def _serialise_joblib(self, object: Any, file: Union[str, Path]) -> None:
         """Serialise the given object to the given file.
 
         Args:
             object (Any): The object to serialise.
-            file (str | Path): The file to serialise to.
+            file (Union[str, Path]): The file to serialise to.
         """
         joblib.dump(object, file)
 
-    def _deserialise_pipeline(self, file: str | Path) -> Pipeline:
+    def _deserialise_pipeline(self, file: Union[str, Path]) -> Pipeline:
         file = Path(file)
         assert file.is_dir()
 
         with open(file / self.STEPS_FILE, "r") as f:
-            steps: list[str] = json.load(f)["steps"]
+            steps: List[str] = json.load(f)["steps"]
 
         pipes = []
         for step in steps:
@@ -106,7 +106,7 @@ def _deserialise_pipeline(self, file: str | Path) -> Pipeline:
                     pipes.append(self._deserialise_pipeline(step_path))
                 else:
                     pipe: SparkPipe = self._deserialise_joblib(step_path / self.BASE_PIPE_FILE)
-                    fitted_class: type[Transformer] = self._deserialise_joblib(
+                    fitted_class: Type[Transformer] = self._deserialise_joblib(
                         step_path / self.SPARK_TRANSFORMER_CLASS_FILE
                     )
                     pipe.fitted = fitted_class.load(str(step_path / self.SPARK_TRANSFORMER_FILE))
@@ -116,11 +116,11 @@ def _deserialise_pipeline(self, file: str | Path) -> Pipeline:
 
         return Pipeline(pipes)
 
-    def _deserialise_joblib(self, file: str | Path) -> Any:
+    def _deserialise_joblib(self, file: Union[str, Path]) -> Any:
         """Deserialise the object in the given file.
 
         Args:
-            file (str | Path): The file to deserialise.
+            file (Union[str, Path]): The file to deserialise.
 
         Returns:
             Any: The python object stored in the file.
diff --git a/pype/spark/setup.py b/pype/spark/setup.py
index 3127621..fa7e18b 100644
--- a/pype/spark/setup.py
+++ b/pype/spark/setup.py
@@ -1,6 +1,6 @@
 from setuptools import find_namespace_packages, setup
 
-version = "0.1.0"
+version = "0.1.1"
 
 deps = [
     f"pype-base=={version}",
diff --git a/pype/tensorflow/model/keras_pype_model.py b/pype/tensorflow/model/keras_pype_model.py
index 71d7850..39e3e6d 100644
--- a/pype/tensorflow/model/keras_pype_model.py
+++ b/pype/tensorflow/model/keras_pype_model.py
@@ -2,7 +2,18 @@
 from abc import ABC, abstractmethod
 from argparse import ArgumentParser
 from pathlib import Path
-from typing import Any, Callable, Generic, Iterable, Type, TypeVar
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    Generic,
+    Iterable,
+    List,
+    Optional,
+    Type,
+    TypeVar,
+    Union,
+)
 
 from keras import Model as KerasBaseModel
 from keras.losses import Loss
@@ -26,31 +37,31 @@ class KerasPypeModel(Model[Tensor], ABC, Generic[T]):
 
     def __init__(
         self,
-        inputs: list[str],
-        outputs: list[str],
-        loss: Callable | Loss,
+        inputs: List[str],
+        outputs: List[str],
+        loss: Union[Callable, Loss],
         optimizer_class: Type[Optimizer],
         epochs: int = 10,
         batch_size: int = 32,
         learning_rate: float = 0.001,
-        metrics: list[Metric] | dict[str, Metric] | None = None,
-        model: T | None = None,
+        metrics: Optional[List[Union[Metric, Dict[str, Metric]]]] = None,
+        model: Optional[T] = None,
         seed: int = 1,
         **model_args: Any,
     ) -> None:
         """A Model to integrate Keras models with the Pype framework.
 
         Args:
-            inputs (list[str]): Input names from the DataSet to use.
-            outputs (list[str]): Output names from the DataSet to use.
-            loss (Callable | Loss): The loss for your Keras Model.
+            inputs (List[str]): Input names from the DataSet to use.
+            outputs (List[str]): Output names from the DataSet to use.
+            loss (Union[Callable, Loss]): The loss for your Keras Model.
             optimizer_class (Type[Optimizer]): The optimization class to use.
             epochs (int, optional): The number of epochs to use for training. Defaults to 10.
             batch_size (int, optional): The batch size to use for training. Defaults to 32.
             learning_rate (float, optional): The learning rate to use for training. Defaults to 0.001.
-            metrics (list[Metric] | dict[str, Metric] | None, optional): Additional metrics to
+            metrics (Optional[List[Union[Metric, Dict[str, Metric]]]]): Additional metrics to
                 use while training th emodel. Defaults to no metrics.
-            model (T | None, optional): The Keras Model to train. By Default we'll use excess arguments
+            model (Optional[T]): The Keras Model to train. By Default we'll use excess arguments
                 to initialise the default model.
             seed (int, optional): The seed to initialise tensorflow with. Defaults to 1.
         """
@@ -72,7 +83,7 @@ def __init__(
         self.batch_size = batch_size
 
     @abstractmethod
-    def _init_model(self, args: dict[str, Any]) -> T:
+    def _init_model(self, args: Dict[str, Any]) -> T:
         raise NotImplementedError
 
     @classmethod
@@ -89,7 +100,7 @@ def _fit(self, *data: Tensor) -> None:
         self.model.compile(optimizer=self.optimizer, loss=self.loss, metrics=self.metrics)
         self.model.fit(dataset, epochs=self.epochs)
 
-    def _transform(self, *data: Tensor) -> Iterable[Tensor] | Tensor:
+    def _transform(self, *data: Tensor) -> Union[Iterable[Tensor], Tensor]:
         return self.model.call(*data)
 
     def _save(self, folder: Path) -> None:
@@ -98,7 +109,7 @@ def _save(self, folder: Path) -> None:
         self.model.save(folder / self.KERAS_MODEL_FILE)
 
     @classmethod
-    def _load(cls: Type["KerasPypeModel"], folder: Path, inputs: list[str], outputs: list[str]) -> "KerasPypeModel":
+    def _load(cls: Type["KerasPypeModel"], folder: Path, inputs: List[str], outputs: List[str]) -> "KerasPypeModel":
         model: KerasBaseModel = keras_load_model(folder / cls.KERAS_MODEL_FILE)
 
         serialiser = JoblibSerialiser()
diff --git a/pype/tensorflow/model/mlp_pype_model.py b/pype/tensorflow/model/mlp_pype_model.py
index a3322ba..4c32296 100644
--- a/pype/tensorflow/model/mlp_pype_model.py
+++ b/pype/tensorflow/model/mlp_pype_model.py
@@ -1,4 +1,4 @@
-from typing import Any
+from typing import Any, Dict
 
 from .keras_pype_model import KerasPypeModel
 from .mlp_keras import MLPKeras
@@ -7,5 +7,5 @@
 class MLPPypeModel(KerasPypeModel[MLPKeras]):
     """A Keras model integrated with Pype's APIs."""
 
-    def _init_model(self, args: dict[str, Any]) -> MLPKeras:
+    def _init_model(self, args: Dict[str, Any]) -> MLPKeras:
         return MLPKeras(**args)
diff --git a/pype/tensorflow/pipeline/tensor_checker.py b/pype/tensorflow/pipeline/tensor_checker.py
index 0ae2d4d..e2300ac 100644
--- a/pype/tensorflow/pipeline/tensor_checker.py
+++ b/pype/tensorflow/pipeline/tensor_checker.py
@@ -1,3 +1,5 @@
+from typing import List, Type, Union
+
 import tensorflow as tf
 from pydantic import create_model
 from tensorflow import DType, Tensor  # type: ignore
@@ -39,7 +41,7 @@ def __init__(self) -> None:
         """
         super().__init__()
         self.dims: tuple = tuple()
-        self.dtype: type | None = None
+        self.dtype: Union[type, None] = None
 
     def fit(self, data: Tensor) -> "TensorflowTypeChecker":
         """Fit this Tensorflow TypeChecker to the given data.
@@ -84,16 +86,16 @@ def _convert_dtype(self, dtype: DType) -> type:
         else:
             return str
 
-    def get_pydantic_type(self) -> type[TensorflowData]:
+    def get_pydantic_type(self) -> Type[TensorflowData]:
         """Creates a Pydantic model for this data to handle serialisation/deserialisation.
 
         Returns:
-            type[TensorflowData]: A TensorflowData model that fits the data this wat fitted on.
+            Type[TensorflowData]: A TensorflowData model that fits the data this wat fitted on.
         """
-        base_iter: type = list[self.dtype]  # type: ignore
+        base_iter: type = List[self.dtype]  # type: ignore
 
         for _ in range(len(self.dims)):
-            base_iter = list[base_iter]  # type: ignore
+            base_iter = List[base_iter]  # type: ignore
 
         model = create_model("TensorflowData", data=(base_iter, ...), __base__=TensorflowData)
 
diff --git a/pype/tensorflow/setup.py b/pype/tensorflow/setup.py
index 4e7e572..46106f3 100644
--- a/pype/tensorflow/setup.py
+++ b/pype/tensorflow/setup.py
@@ -1,6 +1,6 @@
 from setuptools import find_namespace_packages, setup
 
-version = "0.1.0"
+version = "0.1.1"
 
 deps = [
     f"pype-base=={version}",
diff --git a/pype/xgboost/model/xgboost_classifier.py b/pype/xgboost/model/xgboost_classifier.py
index 186b908..6952c2b 100644
--- a/pype/xgboost/model/xgboost_classifier.py
+++ b/pype/xgboost/model/xgboost_classifier.py
@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Any, Type
+from typing import Any, Dict, List, Type
 
 from xgboost.sklearn import XGBClassifier
 
@@ -9,7 +9,7 @@
 class XGBClassifierModel(SklearnModel[XGBClassifier]):
     XGB_MODEL_FILE = "model.txt"
 
-    def _init_model(self, args: dict[str, Any]) -> XGBClassifier:
+    def _init_model(self, args: Dict[str, Any]) -> XGBClassifier:
         return XGBClassifier(**args)
 
     def _save(self, folder: Path) -> None:
@@ -17,7 +17,7 @@ def _save(self, folder: Path) -> None:
         self.model.save_model(folder / self.XGB_MODEL_FILE)
 
     @classmethod
-    def _load(cls: Type["XGBClassifierModel"], folder: Path, inputs: list[str], outputs: list[str]) -> "SklearnModel":
+    def _load(cls: Type["XGBClassifierModel"], folder: Path, inputs: List[str], outputs: List[str]) -> "SklearnModel":
         model = XGBClassifier()
         model.load_model(folder / cls.XGB_MODEL_FILE)
         return cls(inputs=inputs, outputs=outputs, model=model, seed=1)
diff --git a/pype/xgboost/model/xgboost_regressor.py b/pype/xgboost/model/xgboost_regressor.py
index 0afe900..6dc18d7 100644
--- a/pype/xgboost/model/xgboost_regressor.py
+++ b/pype/xgboost/model/xgboost_regressor.py
@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Any, Type
+from typing import Any, Dict, List, Type
 
 from xgboost.sklearn import XGBRegressor
 
@@ -9,7 +9,7 @@
 class XGBRegressorModel(SklearnModel[XGBRegressor]):
     XGB_MODEL_FILE = "model.txt"
 
-    def _init_model(self, args: dict[str, Any]) -> XGBRegressor:
+    def _init_model(self, args: Dict[str, Any]) -> XGBRegressor:
         return XGBRegressor(**args)
 
     def _save(self, folder: Path) -> None:
@@ -17,7 +17,7 @@ def _save(self, folder: Path) -> None:
         self.model.save_model(folder / self.XGB_MODEL_FILE)
 
     @classmethod
-    def _load(cls: Type["XGBRegressorModel"], folder: Path, inputs: list[str], outputs: list[str]) -> "SklearnModel":
+    def _load(cls: Type["XGBRegressorModel"], folder: Path, inputs: List[str], outputs: List[str]) -> "SklearnModel":
         model = XGBRegressor()
         model.load_model(folder / cls.XGB_MODEL_FILE)
         return cls(inputs=inputs, outputs=outputs, model=model, seed=1)
diff --git a/pype/xgboost/setup.py b/pype/xgboost/setup.py
index 9c2fdc2..f3a1044 100644
--- a/pype/xgboost/setup.py
+++ b/pype/xgboost/setup.py
@@ -1,6 +1,6 @@
 from setuptools import find_namespace_packages, setup
 
-version = "0.1.0"
+version = "0.1.1"
 
 deps = [
     f"pype-base=={version}",
diff --git a/pyproject.toml b/pyproject.toml
index 0735012..b3c088d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -38,6 +38,6 @@ exclude = '''
 mypy_path = "pype"
 ignore_missing_imports = true
 disallow_untyped_defs = true
-python_version = "3.10"
+python_version = "3.8"
 implicit_reexport = true
 
diff --git a/scripts/dev_install.sh b/scripts/dev_install.sh
index 22a7e65..fba5c18 100755
--- a/scripts/dev_install.sh
+++ b/scripts/dev_install.sh
@@ -13,7 +13,7 @@ function install_package () {
     if [ $2 != "local" ];
     then
         echo "Installing $1 from host $2"
-        ip=`echo $2 | grep -oE [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`
+        ip=`echo $2 or grep -oE [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`
         echo "IP: $ip"
         pip install -i $2 $1 --trusted-host $ip
     else
diff --git a/setup.py b/setup.py
index 2d218af..20d919e 100644
--- a/setup.py
+++ b/setup.py
@@ -4,8 +4,8 @@
     name="pype",
     packages=find_namespace_packages(include=["pype.*"]),
     namespace_packages=["pype"],
-    python_requires=">=3.10",
-    version="0.1.0",
+    python_requires=">=3.8",
+    version="0.1.1",
     license="MIT",
     author="Jeroen van den Hoven",
     url="https://github.com/jeroenvdhoven/pype",


Test patch:
diff --git a/tests/base/experiment/test_argument_parsing.py b/tests/base/experiment/test_argument_parsing.py
index 554f6cc..0159212 100644
--- a/tests/base/experiment/test_argument_parsing.py
+++ b/tests/base/experiment/test_argument_parsing.py
@@ -148,8 +148,8 @@ def test_none_class(self):
     @mark.parametrize(
         ["class_", "conversion"],
         [
-            [list[int], int],
-            [tuple[float], float],
+            [List[int], int],
+            [Tuple[float], float],
             [Tuple[float], float],
             [Iterable[str], str],
             [List[bool], _convert_bool],
diff --git a/tests/base/experiment/test_experiment.py b/tests/base/experiment/test_experiment.py
index 0fe2211..b55b38a 100644
--- a/tests/base/experiment/test_experiment.py
+++ b/tests/base/experiment/test_experiment.py
@@ -1,7 +1,7 @@
 import sys
 from dataclasses import dataclass
 from pathlib import Path
-from typing import Iterable
+from typing import Iterable, Tuple
 from unittest.mock import MagicMock, call, mock_open, patch
 
 from pytest import fixture, mark
@@ -16,7 +16,7 @@
 
 class Test_run:
     @fixture(scope="class")
-    def run_experiment(self, dummy_experiment: Experiment) -> Iterable[tuple[Experiment, dict]]:
+    def run_experiment(self, dummy_experiment: Experiment) -> Iterable[Tuple[Experiment, dict]]:
         metrics = dummy_experiment.run()
 
         yield dummy_experiment, metrics
@@ -111,7 +111,7 @@ def test_unit(self):
         mock_log_extra_files.assert_called_once_with()
         mock_log_requirements.assert_called_once_with()
 
-    def test_integration(self, run_experiment: tuple[Experiment, dict]):
+    def test_integration(self, run_experiment: Tuple[Experiment, dict]):
         experiment, metrics = run_experiment
         for ds_name in ["train", "test"]:
             assert ds_name in metrics
diff --git a/tests/base/model/test_model.py b/tests/base/model/test_model.py
index 29c68d8..82b1022 100644
--- a/tests/base/model/test_model.py
+++ b/tests/base/model/test_model.py
@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Iterable, List
+from typing import Iterable, List, Union
 from unittest.mock import patch
 
 from pytest import fixture
@@ -22,7 +22,7 @@ def _fit(self, *data: int) -> None:
     def set_seed(self) -> None:
         pass
 
-    def _save(self, file: str | Path) -> None:
+    def _save(self, file: Union[str, Path]) -> None:
         pass
 
     @classmethod
diff --git a/tests/base/pipeline/test_pipeline.py b/tests/base/pipeline/test_pipeline.py
index 688630a..8946318 100644
--- a/tests/base/pipeline/test_pipeline.py
+++ b/tests/base/pipeline/test_pipeline.py
@@ -1,3 +1,4 @@
+from typing import List
 from unittest.mock import MagicMock, call, patch
 
 from pytest import fixture
@@ -62,7 +63,7 @@ def test_assert_all_names_different_with_names(self, pipeline: Pipeline):
         with pytest_assert(AssertionError, f"{name} is used multiple times."):
             pipeline._assert_all_names_different(set([name]))
 
-    def test_fit(self, pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMock]):
+    def test_fit(self, pipeline: Pipeline, input_data: DataSet, pipes: List[MagicMock]):
         result = pipeline.fit(input_data)
 
         assert result == pipeline
@@ -76,7 +77,7 @@ def test_fit(self, pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMoc
         pipes[2].fit.assert_not_called()
         pipes[2].transform.assert_not_called()
 
-    def test_fit_with_pipeline(self, pipeline_with_pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMock]):
+    def test_fit_with_pipeline(self, pipeline_with_pipeline: Pipeline, input_data: DataSet, pipes: List[MagicMock]):
         result = pipeline_with_pipeline.fit(input_data)
 
         assert result == pipeline_with_pipeline
@@ -95,7 +96,7 @@ def test_fit_with_pipeline(self, pipeline_with_pipeline: Pipeline, input_data: D
         pipes[2].fit.assert_called_once_with(pipes[1].transform.return_value)
         pipes[2].transform.assert_called_once_with(pipes[1].transform.return_value)
 
-    def test_transform(self, pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMock]):
+    def test_transform(self, pipeline: Pipeline, input_data: DataSet, pipes: List[MagicMock]):
         pipeline.transform(input_data)
 
         pipes[0].transform.assert_called_once_with(input_data)
@@ -103,7 +104,7 @@ def test_transform(self, pipeline: Pipeline, input_data: DataSet, pipes: list[Ma
         pipes[2].transform.assert_not_called()
 
     def test_transform_with_pipeline(
-        self, pipeline_with_pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMock]
+        self, pipeline_with_pipeline: Pipeline, input_data: DataSet, pipes: List[MagicMock]
     ):
         pipeline_with_pipeline.transform(input_data)
 
@@ -111,7 +112,7 @@ def test_transform_with_pipeline(
         pipes[1].transform.assert_called_once_with(pipes[0].transform.return_value)
         pipes[2].transform.assert_called_once_with(pipes[1].transform.return_value)
 
-    def test_inverse_transform(self, pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMock]):
+    def test_inverse_transform(self, pipeline: Pipeline, input_data: DataSet, pipes: List[MagicMock]):
         pipeline.inverse_transform(input_data)
 
         pipes[1].inverse_transform.assert_called_once_with(input_data)
@@ -120,7 +121,7 @@ def test_inverse_transform(self, pipeline: Pipeline, input_data: DataSet, pipes:
         pipes[2].inverse_transform.assert_not_called()
 
     def test_inverse_transform_with_pipeline(
-        self, pipeline_with_pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMock]
+        self, pipeline_with_pipeline: Pipeline, input_data: DataSet, pipes: List[MagicMock]
     ):
         pipeline_with_pipeline.inverse_transform(input_data)
 
@@ -128,7 +129,7 @@ def test_inverse_transform_with_pipeline(
         pipes[1].inverse_transform.assert_called_once_with(pipes[2].inverse_transform.return_value)
         pipes[0].inverse_transform.assert_called_once_with(pipes[1].inverse_transform.return_value)
 
-    def test_reinitialise(self, pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMock]):
+    def test_reinitialise(self, pipeline: Pipeline, input_data: DataSet, pipes: List[MagicMock]):
         args = {
             "step0__a": 1,
             "step0__b": 2,
@@ -153,7 +154,7 @@ def test_reinitialise(self, pipeline: Pipeline, input_data: DataSet, pipes: list
         )
 
     def test_reinitialise_with_pipeline(
-        self, pipeline_with_pipeline: Pipeline, input_data: DataSet, pipes: list[MagicMock]
+        self, pipeline_with_pipeline: Pipeline, input_data: DataSet, pipes: List[MagicMock]
     ):
         args = {
             "step0__a": 1,
@@ -219,21 +220,21 @@ def test_copy(self):
             assert actual.outputs == expected.outputs
             assert actual.fit_inputs == expected.fit_inputs
 
-    def test_get_item_int(self, pipeline: Pipeline, pipes: list[MagicMock]):
+    def test_get_item_int(self, pipeline: Pipeline, pipes: List[MagicMock]):
         assert pipeline[0] == pipes[0]
         assert pipeline[1] == pipes[1]
 
-    def test_get_item_str(self, pipeline: Pipeline, pipes: list[MagicMock]):
+    def test_get_item_str(self, pipeline: Pipeline, pipes: List[MagicMock]):
         assert pipeline["step0"] == pipes[0]
         assert pipeline["step1"] == pipes[1]
 
-    def test_get_item_nested(self, pipeline_with_pipeline: Pipeline, pipeline: Pipeline, pipes: list[MagicMock]):
+    def test_get_item_nested(self, pipeline_with_pipeline: Pipeline, pipeline: Pipeline, pipes: List[MagicMock]):
         assert pipeline_with_pipeline[0] == pipeline
         assert pipeline_with_pipeline["step0"] == pipes[0]
         assert pipeline_with_pipeline["step1"] == pipes[1]
         assert pipeline_with_pipeline["step2"] == pipes[2]
 
-    def test_iter(self, pipeline: Pipeline, pipes: list[MagicMock]):
+    def test_iter(self, pipeline: Pipeline, pipes: List[MagicMock]):
         for p_in_pipeline, pipe in zip(pipeline, pipes[:2]):
             assert pipe == p_in_pipeline
 
@@ -243,13 +244,13 @@ def test_len(self, pipeline: Pipeline):
     def test_len_with_pipeline(self, pipeline_with_pipeline: Pipeline):
         assert len(pipeline_with_pipeline) == 3
 
-    def test_add_pipe(self, pipeline: Pipeline, pipes: list[MagicMock]):
+    def test_add_pipe(self, pipeline: Pipeline, pipes: List[MagicMock]):
         result = pipeline + pipes[2]
         assert len(result) == 3
         assert result[2] == pipes[2]
         assert result != pipeline
 
-    def test_add_pipeline(self, pipeline: Pipeline, pipes: list[MagicMock]):
+    def test_add_pipeline(self, pipeline: Pipeline, pipes: List[MagicMock]):
         pipe_3 = MagicMock(spec=Pipe)
         pipe_3.name = "another step"
         pipeline_2 = Pipeline([pipes[2], pipe_3])
diff --git a/tests/base/pipeline/test_type_checker.py b/tests/base/pipeline/test_type_checker.py
index bd1245d..313e478 100644
--- a/tests/base/pipeline/test_type_checker.py
+++ b/tests/base/pipeline/test_type_checker.py
@@ -1,4 +1,4 @@
-from typing import Type
+from typing import List, Type
 from unittest.mock import call, patch
 
 from pydantic import create_model
@@ -15,13 +15,13 @@
 
 
 class DummyDataStr(DataModel):
-    data: list[str]
+    data: List[str]
 
-    def convert(self) -> list[str]:
+    def convert(self) -> List[str]:
         return self.data
 
     @classmethod
-    def to_model(cls, data: list[str]) -> "DataModel":
+    def to_model(cls, data: List[str]) -> "DataModel":
         return cls(data=data)
 
 
diff --git a/tests/fastapi/test_app.py b/tests/fastapi/test_app.py
index ce13cdd..b0764f1 100644
--- a/tests/fastapi/test_app.py
+++ b/tests/fastapi/test_app.py
@@ -1,6 +1,7 @@
 import json
 from datetime import datetime
 from pathlib import Path
+from typing import Dict
 from unittest.mock import MagicMock, call, patch
 
 from data.data_sink import DataSink
@@ -46,7 +47,7 @@ def tracking():
 
 
 @fixture
-def app_with_tracking(experiment_path: Path, tracking: dict[str, DataSink]):
+def app_with_tracking(experiment_path: Path, tracking: Dict[str, DataSink]):
     return PypeApp("dummy-app", experiment_path, tracking_servers=tracking)
 
 
@@ -115,7 +116,7 @@ def test_handle_tracking_without_tracking(self, app: PypeApp):
         # should just run.
         app._handle_tracking(DataSet(x=data["x"]), DataSet(y=data["y"]), MagicMock(), MagicMock())
 
-    def test_handle_tracking_with_tracking(self, app_with_tracking: PypeApp, tracking: dict[str, MagicMock]):
+    def test_handle_tracking_with_tracking(self, app_with_tracking: PypeApp, tracking: Dict[str, MagicMock]):
         data = get_dummy_data(10, 2, 1).read()
 
         background_tasks = MagicMock()
@@ -130,7 +131,7 @@ def test_handle_tracking_with_tracking(self, app_with_tracking: PypeApp, trackin
             any_order=True,
         )
 
-    def test_handle_tracking_handles_crashes(self, app_with_tracking: PypeApp, tracking: dict[str, MagicMock]):
+    def test_handle_tracking_handles_crashes(self, app_with_tracking: PypeApp, tracking: Dict[str, MagicMock]):
         data = get_dummy_data(10, 2, 1).read()
 
         logger = MagicMock()
@@ -206,7 +207,7 @@ def test_predict_with_tracking(self, app_with_tracking: PypeApp):
         mock_handle.assert_called_once_with(DataSet(x=x), DataSet(y=prediction), mock_get_logger.return_value, AnyArg())
 
     def test_predict_with_tracking_integration(
-        self, test_client_with_tracking: TestClient, tracking: dict[str, DummyDataSink]
+        self, test_client_with_tracking: TestClient, tracking: Dict[str, DummyDataSink]
     ):
         x = [1.0, 2.0, 3.0, 4.0]
 
diff --git a/tests/mlflow/deploy/test_load_experiment.py b/tests/mlflow/deploy/test_load_experiment.py
index d451d95..e287506 100644
--- a/tests/mlflow/deploy/test_load_experiment.py
+++ b/tests/mlflow/deploy/test_load_experiment.py
@@ -1,4 +1,5 @@
 from pathlib import Path
+from typing import Union
 from unittest.mock import MagicMock, patch
 
 from pytest import mark
@@ -52,7 +53,7 @@ def test_assertion_valid_run_id(self):
                 load_experiment_from_mlflow(url, name, run_id)
 
     @mark.parametrize(["directory"], [["str-directory"], [Path("path-directory")]])
-    def test_with_folder(self, directory: str | Path):
+    def test_with_folder(self, directory: Union[str, Path]):
         url = "some_url"
         name = "some name"
         run_id = "jkldsf9032sd"
diff --git a/tests/sklearn/model/test_sklearn_model.py b/tests/sklearn/model/test_sklearn_model.py
index af670c5..39b0a7a 100644
--- a/tests/sklearn/model/test_sklearn_model.py
+++ b/tests/sklearn/model/test_sklearn_model.py
@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Any
+from typing import Any, Dict
 from unittest.mock import MagicMock, patch
 
 import numpy as np
@@ -63,7 +63,7 @@ def test_load(self, model: SklearnModel, sklearn_model: MagicMock):
 
     def test_get_parameters_from_object(self):
         class DummyModel(SklearnModel[LinearRegression]):
-            def _init_model(self, args: dict[str, Any]) -> LinearRegression:
+            def _init_model(self, args: Dict[str, Any]) -> LinearRegression:
                 return LinearRegression(**args)
 
         parser = MagicMock()
@@ -77,7 +77,7 @@ def _init_model(self, args: dict[str, Any]) -> LinearRegression:
 
     def test_get_parameters_from_class(self):
         class DummyModel(SklearnModel[LinearRegression]):
-            def _init_model(self, args: dict[str, Any]) -> LinearRegression:
+            def _init_model(self, args: Dict[str, Any]) -> LinearRegression:
                 return LinearRegression(**args)
 
         parser = MagicMock()
diff --git a/tests/sklearn/pipeline/test_pandas_type_checker copy.py b/tests/sklearn/pipeline/test_pandas_type_checker copy.py
index f2a5294..cd7a907 100644
--- a/tests/sklearn/pipeline/test_pandas_type_checker copy.py
+++ b/tests/sklearn/pipeline/test_pandas_type_checker copy.py
@@ -1,5 +1,5 @@
 from datetime import datetime
-from typing import Type
+from typing import Dict, List, Type, Union
 
 import pandas as pd
 from pandas.api import types as pandas_types
@@ -24,8 +24,8 @@ class Test_PandasData:
     def model_class(self) -> Type[PandasData]:
         return create_model(
             "PandasData",
-            x=(list[int] | dict[str | int, int], ...),
-            y=(list[str] | dict[str | int, str], ...),
+            x=(Union[List[int], Dict[str or int, int]], ...),
+            y=(Union[List[str], Dict[str or int, str]], ...),
             __base__=PandasData,
         )
 
diff --git a/tests/sklearn/pipeline/test_pandas_type_checker.py b/tests/sklearn/pipeline/test_pandas_type_checker.py
new file mode 100644
index 0000000..aeaf46d
--- /dev/null
+++ b/tests/sklearn/pipeline/test_pandas_type_checker.py
@@ -0,0 +1,181 @@
+from datetime import datetime
+from typing import Dict, List, Type, Union
+
+import pandas as pd
+from pandas.api import types as pandas_types
+from pandas.api.types import (
+    is_bool_dtype,
+    is_datetime64_any_dtype,
+    is_float_dtype,
+    is_integer_dtype,
+    is_object_dtype,
+    is_string_dtype,
+)
+from pandas.testing import assert_frame_equal
+from pydantic import create_model
+from pytest import fixture
+
+from pype.sklearn.pipeline.pandas_type_checker import PandasData, PandasTypeChecker
+from tests.utils import pytest_assert
+
+
+class Test_PandasData:
+    @fixture
+    def model_class(self) -> Type[PandasData]:
+        return create_model(
+            "PandasData",
+            x=(Union[List[int], Dict[Union[str, int], int]], ...),
+            y=(Union[List[str], Dict[Union[str, int], str]], ...),
+            __base__=PandasData,
+        )
+
+    def test_convert(self, model_class: Type[PandasData]):
+        model = model_class(x=[1, 2, 3, 4], y=["a", "b", "c", "e"])
+
+        expected = pd.DataFrame(
+            {
+                "x": [1, 2, 3, 4],
+                "y": ["a", "b", "c", "e"],
+            }
+        )
+
+        result = model.convert()
+
+        assert_frame_equal(expected, result)
+
+    def test_to_model(self, model_class: Type[PandasData]):
+        df = pd.DataFrame(
+            {
+                "x": [1, 2, 3, 4],
+                "y": ["a", "b", "c", "e"],
+            }
+        )
+        result = model_class.to_model(df)
+
+        expected = model_class(
+            x=[1, 2, 3, 4],
+            y=["a", "b", "c", "e"],
+        )
+
+        assert expected == result
+
+
+class Test_PandasTypeChecker:
+    def test_fit(self):
+        type_checker = PandasTypeChecker()
+        df = pd.DataFrame(
+            {
+                "x": [1, 2, 3, 4],
+                "y": ["a", "b", "c", "e"],
+            }
+        )
+
+        type_checker.fit(df)
+        expected_dict = {
+            "x": (int, is_integer_dtype),
+            "y": (str, is_object_dtype),
+        }
+        assert type_checker.raw_types == expected_dict
+
+    def test_transform(self):
+        type_checker = PandasTypeChecker()
+        df = pd.DataFrame(
+            {
+                "x": [1, 2, 3, 4],
+                "y": ["a", "b", "c", "e"],
+            }
+        )
+
+        type_checker.fit(df)
+        # works
+        result = type_checker.transform(df)
+
+        # errors #
+        # other type of object
+        with pytest_assert(AssertionError, "Please provide a pandas DataFrame!"):
+            type_checker.transform([])
+
+        # missing columns
+        missing_df = pd.DataFrame({"x": [1, 2, 3]})  # missing
+        with pytest_assert(AssertionError, "Not all columns are present."):
+            type_checker.transform(missing_df)
+
+        other_type = df.copy()
+        other_type["x"] = other_type["x"].astype(str)
+
+        with pytest_assert(AssertionError, "Dtypes did not match up for col x."):
+            type_checker.transform(other_type)
+
+    def test_transform_reduces_columns(self):
+        type_checker = PandasTypeChecker()
+        df = pd.DataFrame(
+            {
+                "x": [1, 2, 3, 4],
+                "y": ["a", "b", "c", "e"],
+            }
+        )
+
+        type_checker.fit(df)
+        # works
+        extra_col_df = df.copy()
+        extra_col_df["z"] = 1
+        result = type_checker.transform(extra_col_df)
+
+        assert "z" not in df.columns
+
+    def test_convert_raw_types(self):
+        type_checker = PandasTypeChecker()
+        df = pd.DataFrame(
+            {
+                "x": [1, 2, 3],
+                "y": [1, 2, 3.0],
+                "z": ["1", "2", "3"],
+                "a": [True, False, True],
+                "b": [1, "1", 1],
+                "c": [datetime(2018, 1, 1, 1), datetime(2018, 1, 1, 0), datetime(2018, 1, 1, 2)],
+            }
+        )
+
+        result = type_checker._convert_raw_types(dict(df.dtypes))
+        expected = {
+            "x": (int, pandas_types.is_integer_dtype),
+            "y": (float, pandas_types.is_float_dtype),
+            "z": (str, pandas_types.is_object_dtype),
+            "a": (bool, pandas_types.is_bool_dtype),
+            "b": (str, pandas_types.is_object_dtype),
+            "c": (datetime, pandas_types.is_datetime64_any_dtype),
+        }
+        assert result == expected
+
+    def test_get_pydantic_types(self):
+        type_checker = PandasTypeChecker()
+        df = pd.DataFrame(
+            {
+                "x": [1, 2, 3, 4],
+                "y": ["a", "b", "c", "e"],
+            }
+        )
+        type_checker.fit(df.copy())
+
+        PandasSpecificType = type_checker.get_pydantic_type()
+
+        # succeed
+        result = PandasSpecificType(x=[1, 2, 3, 4], y=["a", "b", "c", "e"]).convert()
+
+        assert_frame_equal(df, result)
+
+    # def test_get_pydantic_types_strict_on_bool(self):
+    #     type_checker = PandasTypeChecker()
+    #     array = np.array([
+    #         [True, False]
+    #     ])
+    #     type_checker.fit(array)
+
+    #     PandasSpecificType = type_checker.get_pydantic_type()
+
+    #     # succeed
+    #     PandasSpecificType(data=[[False, False]])
+
+    #     # fails
+    #     with pytest_assert(ValidationError):
+    #         PandasSpecificType(data=[[1, 2 ,3]])
diff --git a/tests/spark/model/test_spark_model.py b/tests/spark/model/test_spark_model.py
index e847134..a457d8e 100644
--- a/tests/spark/model/test_spark_model.py
+++ b/tests/spark/model/test_spark_model.py
@@ -1,7 +1,7 @@
 import json
 from pathlib import Path
 from tempfile import TemporaryDirectory
-from typing import List
+from typing import List, Optional
 from unittest.mock import MagicMock, patch
 
 import numpy as np
@@ -112,7 +112,7 @@ def test_transform_assertions_unfitted(self):
             ["return only column", "pred_col"],
         ],
     )
-    def test_transform(self, name: str, prediction_col: str | None):
+    def test_transform(self, name: str, prediction_col: Optional[str]):
         predictor = MagicMock()
         model = LinearSparkModel(
             inputs=["x"],
diff --git a/tests/spark/pipeline/test_spark_type_checker.py b/tests/spark/pipeline/test_spark_type_checker.py
index 06a527d..4d355a8 100644
--- a/tests/spark/pipeline/test_spark_type_checker.py
+++ b/tests/spark/pipeline/test_spark_type_checker.py
@@ -1,3 +1,4 @@
+from typing import Dict, List, Union
 from unittest.mock import MagicMock, call, patch
 
 import pandas as pd
@@ -16,8 +17,8 @@
 class Test_SparkData:
     def test_convert(self, spark_session: SparkSession):
         data_type = {
-            "a": (list[int] | dict[str | int, int], ...),
-            "b": (list[str] | dict[str | int, str], ...),
+            "a": (Union[List[int], Dict[str or int, int]], ...),
+            "b": (Union[List[str], Dict[str or int, str]], ...),
         }
 
         CustomSparkData = create_model("CustomSparkData", **data_type, __base__=SparkData)
@@ -40,8 +41,8 @@ def test_convert(self, spark_session: SparkSession):
 
     def test_to_model(self, spark_session: SparkSession):
         data_type = {
-            "a": (list[int] | dict[str | int, int], ...),
-            "b": (list[str] | dict[str | int, str], ...),
+            "a": (Union[List[int], Dict[str or int, int]], ...),
+            "b": (Union[List[str], Dict[str or int, str]], ...),
         }
         CustomSparkData = create_model("CustomSparkData", **data_type, __base__=SparkData)
 
diff --git a/tests/tensorflow/model/test_keras_pype_model.py b/tests/tensorflow/model/test_keras_pype_model.py
index 1799d10..9756ac4 100644
--- a/tests/tensorflow/model/test_keras_pype_model.py
+++ b/tests/tensorflow/model/test_keras_pype_model.py
@@ -1,6 +1,6 @@
 import shutil
 from pathlib import Path
-from typing import Any
+from typing import Any, Dict
 from unittest.mock import MagicMock, call, patch
 
 import numpy as np
@@ -16,7 +16,7 @@
 
 
 class DummyKerasModel(KerasPypeModel[MLPKeras]):
-    def _init_model(self, args: dict[str, Any]) -> MLPKeras:
+    def _init_model(self, args: Dict[str, Any]) -> MLPKeras:
         return MLPKeras(10, 2, 2)
 
 
diff --git a/tests/utils.py b/tests/utils.py
index eced4f6..064932b 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,5 +1,6 @@
 from contextlib import contextmanager
 from pathlib import Path
+from typing import List, Optional, Tuple, Type, Union
 
 from data.data_sink import DataSink
 
@@ -25,7 +26,7 @@ def __eq__(self, __o: object) -> bool:
 
 
 @contextmanager
-def pytest_assert(error_class, message: str | None = None, exact: bool = True):
+def pytest_assert(error_class, message: Optional[str] = None, exact: bool = True):
     try:
         yield
         raise ValueError("No error was raised!")
@@ -38,20 +39,20 @@ def pytest_assert(error_class, message: str | None = None, exact: bool = True):
                 assert message in error_message
 
 
-class DummyModel(Model[list[int | float]]):
+class DummyModel(Model[List[Union[int, float]]]):
     mean_file = "mean.txt"
 
-    def __init__(self, inputs: list[str], outputs: list[str], seed: int = 1, a: int = 3) -> None:
+    def __init__(self, inputs: List[str], outputs: List[str], seed: int = 1, a: int = 3) -> None:
         super().__init__(inputs, outputs, seed)
         self.a = a
 
     def set_seed(self) -> None:
         pass
 
-    def _fit(self, x: list[int | float], y: list[int | float]) -> None:
+    def _fit(self, x: List[Union[int, float]], y: List[Union[int, float]]) -> None:
         self.prediction = sum(y) / len(y) + self.a
 
-    def _transform(self, x: list[int | float]) -> list[int | float]:
+    def _transform(self, x: List[Union[int, float]]) -> List[Union[int, float]]:
         return [self.prediction for _ in x]
 
     def _save(self, folder: Path) -> None:
@@ -59,19 +60,19 @@ def _save(self, folder: Path) -> None:
             f.write(str(self.prediction))
 
     @classmethod
-    def _load(cls, folder: Path, inputs: list[str], outputs: list[str]) -> "DummyModel":
+    def _load(cls, folder: Path, inputs: List[str], outputs: List[str]) -> "DummyModel":
         result = cls(inputs=inputs, outputs=outputs)
         with open(folder / cls.mean_file, "r") as f:
             result.prediction = float(f.read())
         return result
 
 
-class DummyDataSource(DataSource[list[float]]):
+class DummyDataSource(DataSource[List[float]]):
     def __init__(self, l) -> None:
         super().__init__()
         self.l = l
 
-    def read(self) -> list[float]:
+    def read(self) -> List[float]:
         return self.l
 
     def __eq__(self, __o: object) -> bool:
@@ -85,19 +86,19 @@ def get_dummy_data(n: int, x_offset: int, y_offset: int) -> DataSetSource:
     )
 
 
-class DummyDataSink(DataSink[list[float]]):
+class DummyDataSink(DataSink[List[float]]):
     def __init__(self) -> None:
         self.data = None
 
-    def write(self, data: list[float]) -> None:
+    def write(self, data: List[float]) -> None:
         self.data = data
 
 
-class DummyOperator(Operator[list[float]]):
-    def fit(self, x: list[float]) -> "Operator":
+class DummyOperator(Operator[List[float]]):
+    def fit(self, x: List[float]) -> "Operator":
         return self
 
-    def transform(self, x: list[float]) -> list[float]:
+    def transform(self, x: List[float]) -> List[float]:
         return reverse([i - 1 for i in x])
 
 
@@ -110,13 +111,13 @@ def get_dummy_evaluator() -> Evaluator:
 
 
 class DummyDataModel(DataModel):
-    data: list[float]
+    data: List[float]
 
-    def convert(self) -> list[float]:
+    def convert(self) -> List[float]:
         return self.data
 
     @classmethod
-    def to_model(cls, data: list[float]) -> "DataModel":
+    def to_model(cls, data: List[float]) -> "DataModel":
         return cls(data=data)
 
 
@@ -126,19 +127,19 @@ class DummyDataSet(DataSetModel):
 
 
 class DummyTypeChecker(TypeChecker):
-    def fit(self, data: list[float]) -> "Operator":
+    def fit(self, data: List[float]) -> "Operator":
         return super().fit(data)
 
-    def transform(self, data: list[float]) -> list[float]:
+    def transform(self, data: List[float]) -> List[float]:
         assert isinstance(data, list), "Provide a list!"
         assert isinstance(data[0], float) or isinstance(data[0], int), "Provide a list with ints/floats!"
         return data
 
-    def get_pydantic_type(self) -> type[DataModel]:
+    def get_pydantic_type(self) -> Type[DataModel]:
         return DummyDataModel
 
 
-def get_dummy_type_checkers() -> tuple[TypeCheckerPipe, TypeCheckerPipe]:
+def get_dummy_type_checkers() -> Tuple[TypeCheckerPipe, TypeCheckerPipe]:
     return TypeCheckerPipe("input", ["x"], type_checker_classes=[(list, DummyTypeChecker)]), TypeCheckerPipe(
         "output", ["y"], type_checker_classes=[(list, DummyTypeChecker)]
     )
diff --git a/tests/utils_training_support.py b/tests/utils_training_support.py
index 8a45155..492f854 100644
--- a/tests/utils_training_support.py
+++ b/tests/utils_training_support.py
@@ -1,5 +1,8 @@
 """Custom 'function' to test importing / exporting data files."""
 
 
-def reverse(x: list[float]) -> list[float]:
+from typing import List
+
+
+def reverse(x: List[float]) -> List[float]:
     return x[::-1]