Repository: AI-SDC/SACRO-ML (language: python)

Issue:
format with black and fix implicit str conversions
Install pre-commit
```
$ pip install pre-commit
```
Run this before every commit / push
```
$ pre-commit run -a
```


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 1fd0327d..9451bfb0 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -20,5 +20,5 @@ jobs:
 
       - name: pylint
         run: |
-          pylint safemodel **/*.py --fail-under 10
+          pylint -v --recursive=True safemodel attacks tests --fail-under 10
 ...
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a697e377..72d20872 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,12 @@
 ---
 # configuration for https://pre-commit.com
+# To use: pre-commit run -a
+# Or to run every time you commit: pre-commit install
+# To update this file: pre-commit autoupdate
+
 repos:
+
+  # Standard hooks
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v4.3.0
     hooks:
@@ -12,27 +18,42 @@ repos:
       - id: check-json
       - id: pretty-format-json
         args: ["--autofix", "--no-sort-keys", "--indent=4"]
+
+  # Check for spelling
   - repo: https://github.com/codespell-project/codespell
     rev: v2.2.1
     hooks:
       - id: codespell
-        args: ["-L", "fpr, tre, sav"]
+        args: ["-L", "fpr, tre, sav, provid"]
         exclude: >
             (?x)^(
                 .*\.svg|
                 .*\.ipynb
             )$
-#  - repo: https://github.com/asottile/pyupgrade
-#    rev: v2.37.3
-#    hooks:
-#      - id: pyupgrade
-#  - repo: https://github.com/PyCQA/isort
-#    rev: 5.10.1
-#    hooks:
-#      - id: isort
-#        args: ["--profile", "black"]
-#  - repo: https://github.com/psf/black
-#    rev: 22.6.0
-#    hooks:
-#      - id: black-jupyter
+
+  # Autoremoves unused imports
+  - repo: https://github.com/hadialqattan/pycln
+    rev: "v2.1.1"
+    hooks:
+      - id: pycln
+        stages: [manual]
+
+  # Sort includes
+  - repo: https://github.com/asottile/pyupgrade
+    rev: v2.38.2
+    hooks:
+      - id: pyupgrade
+
+  # Upgrade old Python syntax
+  - repo: https://github.com/PyCQA/isort
+    rev: 5.10.1
+    hooks:
+      - id: isort
+        args: ["--profile", "black"]
+
+  # Black format Python and notebooks
+  - repo: https://github.com/psf/black
+    rev: 22.8.0
+    hooks:
+      - id: black-jupyter
 ...
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 232cd9c6..8c74ce7f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,46 +1,72 @@
 # General guidance for contributors
 
+## Style Guide
 
-# Adding your files to the automatically created documentation website
-Automatic Documentation
-=======================
+Python code should be linted with [pylint](https://github.com/PyCQA/pylint).
 
-The documentation is automatically built using sphinx and github actions.
+A [pre-commit](https://pre-commit.com) configuration [file](../main/.pre-commit-config.yaml) is provided to automatically:
+* Trim trailing whitespace and fix line endings;
+* Check for spelling errors with [codespell](https://github.com/codespell-project/codespell);
+* Check and format JSON files;
+* Format Python and notebooks with [black](https://github.com/psf/black);
+* Upgrade Python syntax with [pyupgrade](https://github.com/asottile/pyupgrade);
+* Automatically remove unused Python imports;
+* Sort Python imports.
 
-The source files in docs/source are parsed/compiled into HTML files in docs/_build.
-The contents of docs/_build is pushed to the gh-pages branch which is then automatically
-deployed to the github.io site.
+Pre-commit can be setup as follows:
+```
+$ pip install pre-commit
+```
+Then to run on all files in the repository:
+```
+$ pre-commit run -a
+```
+To install as a hook that executes with every `git commit`:
+```
+$ pre-commit install
+```
+
+*******************************************************************************
 
-The main configuration file is docs/source/conf.py
+## Automatic Documentation
+
+The documentation is automatically built using [Sphinx](https://www.sphinx-doc.org) and github actions.
+
+The source files in `docs/source` are parsed/compiled into HTML files in `docs/_build`.
+The contents of `docs/_build` is pushed to the gh-pages branch which is then automatically
+deployed to the [github.io site](https://ai-sdc.github.io/AI-SDC/).
+
+The main configuration file is `docs/source/conf.py`
 Most commonly the path variable will pick up any source to document
-occasionally directories might need adding top the path. Please ensure to use abspath()
+occasionally directories might need adding top the path. Please ensure to use `abspath()`
 
-Sphinx reads the docstrings in the python source.
+Sphinx reads the docstrings in the Python source.
 
 It uses the numpydoc format. Your code should be documented with numpydoc comments.
 [NumpyDoc](https://numpydoc.readthedocs.io/en/latest/format.html).
 
-Quick Start
-===========
+### Quick Start
 
 Need to get your documentation into the generated docs?
 If your DocStrings are in the right format, this method should work for most cases:
 
-1. Go to docs/source
-2. Make a copy of an rst file (E.g. (safedecisiontree.rst)
-3. edit the new file and change the title and automodule line.
+1. Go to `docs/source`
+2. Make a copy of an rst file, e.g., `safedecisiontree.rst`
+3. Edit the new file and change the title and automodule line.
+
 ```
 Data Interface
 ==============
 
-An example Python Notebook is available  `Here <https://github.com/jim-smith/GRAIMatter/blob/main/WP2/wrapper/wrapper-concept.ipynb>`_
+An example Python Notebook is available  `Here <https://github.com/jim-smith/GRAIMatter/blob/main/WP2/wrapper/wrapper-concept.ipynb>`__
 
 .. automodule:: preprocessing.loaders
    :members:
 ```
 
 4. Save the new file
-5. Edit the index.rst and insert the new filename(without the .rst) into the correct position in the list.
+5. Edit the `index.rst` and insert the new filename (without the .rst) into the correct position in the list.
+
 ```
 .. toctree::
    :maxdepth: 2
@@ -55,11 +81,10 @@ An example Python Notebook is available  `Here <https://github.com/jim-smith/GRA
    datainterface
 ```
 
-6. Save index.rst
+6. Save `index.rst`
 7. Push your updates to main
 
-DocStrings
-==========
+### DocStrings
 
 An example docstring from
 the safemodel source is below:
@@ -101,10 +126,9 @@ class SafeModel:
       """
 ```
 
-Static and Generated Content
-============================
+### Static and Generated Content
 
-The .rst files in docs/source/ are a mixture of static and generated content
+The .rst files in `docs/source/` are a mixture of static and generated content
 Static content should be written in ReStructuredText (.rst) format.
 
 A short primer
@@ -117,13 +141,12 @@ Automatic code documentation from the docstrings uses sphinx directives in the .
 .. automodule:: safemodel.classifiers.safedecisiontreeclassifier
    :members:
 ```
-Images
-------
+
+### Images
 
 It is possible to include images like this
 
 ```
-
 .. image:: stars.jpg
     :width: 200px
     :align: center
@@ -131,15 +154,13 @@ It is possible to include images like this
     :alt: alternate text
 ```
 
-Generating docs locally
-=======================
+### Generating docs locally
 
-It is useful to be able to generate your docs locally (to check for bugs etc)
+It is useful to be able to generate your docs locally (to check for bugs etc.)
 
-In linux, navigate to the `docs` folder and then issue the command
-`make html`
+On GNU/Linux, navigate to the `docs` folder and then issue the command `make html`
 
-In windows, navigate to the `docs` folder than then issue the command
+On Windows, navigate to the `docs` folder than then issue the command
 `sphinx-build source _build`
 
 The generated html will be in the folder `docs/_build` and can be opened in any browser.
diff --git a/attacks/attack.py b/attacks/attack.py
index 435416ae..7beb0630 100644
--- a/attacks/attack.py
+++ b/attacks/attack.py
@@ -1,11 +1,13 @@
 """attack.py - base class for an attack object"""
 
 import sklearn
+
 from attacks.dataset import Data
 
 
 class Attack:
     """Base (abstract) class to represent an attack"""
+
     def attack(self, dataset: Data, target_model: sklearn.base.BaseEstimator) -> None:
         """Method to run an attack"""
         raise NotImplementedError
diff --git a/attacks/attribute_attack.py b/attacks/attribute_attack.py
index 68a71c27..0875c921 100644
--- a/attacks/attribute_attack.py
+++ b/attacks/attribute_attack.py
@@ -404,7 +404,7 @@ def _attack_brute_force(
     args = [
         (target_model, dataset, feature_id, attack_threshold) for feature_id in features
     ]
-    with mp.Pool(processes=n_cpu) as pool:
+    with mp.Pool(processes=n_cpu) as pool:  # pylint:disable=not-callable
         results = pool.starmap(_infer_categorical, args)
     return results
 
@@ -536,7 +536,7 @@ def _get_bounds_risks(
         )
         for feature_id in features
     ]
-    with mp.Pool(processes=n_cpu) as pool:
+    with mp.Pool(processes=n_cpu) as pool:  # pylint:disable=not-callable
         results = pool.starmap(_get_bounds_risk, args)
     return results
 
diff --git a/attacks/likelihood_attack.py b/attacks/likelihood_attack.py
index a7e4118a..004816c5 100644
--- a/attacks/likelihood_attack.py
+++ b/attacks/likelihood_attack.py
@@ -1,34 +1,36 @@
-'''
+"""
 Likelihood testing scenario from https://arxiv.org/pdf/2112.03570.pdf
-'''
+"""
 # pylint: disable = invalid-name
+# pylint: disable = too-many-branches
 import argparse
+import importlib
 import json
 import logging
-import importlib
+from collections.abc import Hashable, Iterable
 from typing import Any
 
-from collections.abc import Iterable, Hashable
 import numpy as np
-from scipy.stats import norm
 import sklearn
+from scipy.stats import norm
+from sklearn.datasets import load_breast_cancer
 from sklearn.ensemble import RandomForestClassifier
 from sklearn.model_selection import train_test_split
-from  sklearn.datasets import load_breast_cancer
 
-from attacks import report
-from attacks import metrics
+from attacks import metrics, report
 from attacks.attack import Attack
 from attacks.dataset import Data
 
 logging.basicConfig(level=logging.INFO)
 
-N_SHADOW_MODELS = 100 # Number of shadow models that should be trained
-EPS = 1e-16 # Used to avoid numerical issues in logit function
-P_THRESH = 0.05 # default significance threshold
+N_SHADOW_MODELS = 100  # Number of shadow models that should be trained
+EPS = 1e-16  # Used to avoid numerical issues in logit function
+P_THRESH = 0.05  # default significance threshold
+
 
 class DummyClassifier:
     """A Dummy Classifier to allow this code to work with get_metrics"""
+
     def predict(self, test_X):
         """Return an array of 1/0 depending on value in second column"""
         return 1 * (test_X[:, 1] > 0.5)
@@ -37,6 +39,7 @@ def predict_proba(self, test_X):
         """Simply return the test_X"""
         return test_X
 
+
 def _logit(p: float) -> float:
     """Standard logit function
 
@@ -64,15 +67,18 @@ def _logit(p: float) -> float:
 
 class LIRAAttackArgs:
     """LIRA Attack arguments"""
+
     def __init__(self, **kwargs):
-        self.__dict__['n_shadow_models'] = N_SHADOW_MODELS
-        self.__dict__['p_thresh'] = 0.05
-        self.__dict__['report_name'] = None
-        self.__dict__['json_file'] = "config.json"
+        self.__dict__["n_shadow_models"] = N_SHADOW_MODELS
+        self.__dict__["p_thresh"] = 0.05
+        self.__dict__["report_name"] = None
+        self.__dict__["json_file"] = "config.json"
         self.__dict__.update(kwargs)
 
     def __str__(self):
-        return ",".join([f"{str(key)}: {str(value)}" for key, value in self.__dict__.items()])
+        return ",".join(
+            [f"{str(key)}: {str(value)}" for key, value in self.__dict__.items()]
+        )
 
     def set_param(self, key: Hashable, value: Any) -> None:
         """Set a parameter"""
@@ -82,9 +88,11 @@ def get_args(self) -> dict:
         """Return arguments"""
         return self.__dict__
 
+
 class LIRAAttack(Attack):
     """The main LIRA Attack class"""
-    def __init__(self, args: LIRAAttackArgs=LIRAAttackArgs()) -> None:
+
+    def __init__(self, args: LIRAAttackArgs = LIRAAttackArgs()) -> None:
         self.attack_metrics = None
         self.dummy_attack_metrics = None
         self.metadata = None
@@ -93,10 +101,7 @@ def __init__(self, args: LIRAAttackArgs=LIRAAttackArgs()) -> None:
     def __str__(self):
         return "LIRA Attack"
 
-    def attack(
-        self,
-        dataset: Data,
-        target_model: sklearn.base.BaseEstimator) -> None:
+    def attack(self, dataset: Data, target_model: sklearn.base.BaseEstimator) -> None:
         """Programmatic attack running
         Runs a LIRA attack from a dataset object and a target model
 
@@ -121,13 +126,12 @@ def attack(
             target_model.predict_proba(dataset.x_train),
             dataset.x_test,
             dataset.y_test,
-            target_model.predict_proba(dataset.x_test)
+            target_model.predict_proba(dataset.x_test),
         )
 
     def _check_and_update_dataset(
-        self,
-        dataset: Data,
-        target_model: sklearn.base.BaseEstimator) -> tuple[np.ndarray, np.ndarray]:
+        self, dataset: Data, target_model: sklearn.base.BaseEstimator
+    ) -> tuple[np.ndarray, np.ndarray]:
         """Makes sure that it is ok to use the class variables to index the prediction
         arrays. This has two steps:
         1. Replacing the values in y_train with their position in target_model.classes (will
@@ -137,12 +141,14 @@ def _check_and_update_dataset(
         """
 
         y_train_new = []
-        classes = list(target_model.classes_) # pylint: disable = protected-access
+        classes = list(target_model.classes_)  # pylint: disable = protected-access
         for y in dataset.y_train:
             y_train_new.append(classes.index(y))
 
         dataset.y_train = np.array(y_train_new, int)
-        print(f'new ytrain has values and counts: {np.unique(dataset.y_train,return_counts=True)}')
+        print(
+            f"new ytrain has values and counts: {np.unique(dataset.y_train,return_counts=True)}"
+        )
         ok_pos = []
         y_test_new = []
         for i, y in enumerate(dataset.y_test):
@@ -153,12 +159,13 @@ def _check_and_update_dataset(
         if len(y_test_new) != len(dataset.x_test):
             dataset.x_test = dataset.x_test[ok_pos, :]
         dataset.y_test = np.array(y_test_new, int)
-        print(f'new ytest has values and counts: {np.unique(dataset.y_test,return_counts=True)}')
-
+        print(
+            f"new ytest has values and counts: {np.unique(dataset.y_test,return_counts=True)}"
+        )
 
         return dataset
 
-    def run_scenario_from_preds( # pylint: disable = too-many-statements
+    def run_scenario_from_preds(  # pylint: disable = too-many-statements
         self,
         shadow_clf: sklearn.base.BaseEstimator,
         X_target_train: Iterable[float],
@@ -166,11 +173,8 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
         target_train_preds: Iterable[float],
         X_shadow_train: Iterable[float],
         y_shadow_train: Iterable[float],
-        shadow_train_preds: Iterable[float]) -> tuple[
-            np.ndarray,
-            np.ndarray,
-            sklearn.base.BaseEstimator
-        ]:
+        shadow_train_preds: Iterable[float],
+    ) -> tuple[np.ndarray, np.ndarray, sklearn.base.BaseEstimator]:
 
         # def run_scenario_from_preds( # pylint: disable = too-many-locals, too-many-arguments
         #     shadow_clf,
@@ -241,14 +245,8 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
         indices = np.arange(0, n_train_rows + n_shadow_rows, 1)
 
         # Combine taregt and shadow train, from which to sample datasets
-        combined_X_train = np.vstack((
-            X_target_train,
-            X_shadow_train
-        ))
-        combined_y_train = np.hstack((
-            y_target_train,
-            y_shadow_train
-        ))
+        combined_X_train = np.vstack((X_target_train, X_shadow_train))
+        combined_y_train = np.hstack((y_target_train, y_shadow_train))
 
         train_row_to_confidence = {i: [] for i in range(n_train_rows)}
         shadow_row_to_confidence = {i: [] for i in range(n_shadow_rows)}
@@ -259,7 +257,7 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
             if model_idx % 10 == 0:
                 logger.info("Trained %d models", model_idx)
             # Pick the indices to use for training this one
-            np.random.seed(model_idx) # Reproducibility
+            np.random.seed(model_idx)  # Reproducibility
             these_idx = np.random.choice(indices, n_train_rows, replace=False)
             temp_X_train = combined_X_train[these_idx, :]
             temp_y_train = combined_y_train[these_idx]
@@ -269,14 +267,11 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
             shadow_clf.fit(temp_X_train, temp_y_train)
 
             # map a class to a column
-            class_map = {
-                c: i for i, c in enumerate(shadow_clf.classes_)
-            }
-
+            class_map = {c: i for i, c in enumerate(shadow_clf.classes_)}
 
             # Get the predicted probabilities on the training data
             confidences = shadow_clf.predict_proba(X_target_train)
-            #print(f'shadow clf returned confidences with shape {confidences.shape}')
+            # print(f'shadow clf returned confidences with shape {confidences.shape}')
 
             these_idx = set(these_idx)
             for i in range(n_train_rows):
@@ -286,9 +281,7 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
                     cl_pos = class_map.get(y_target_train[i], -1)
                     if cl_pos >= 0:
                         train_row_to_confidence[i].append(
-                            _logit(
-                                confidences[i, y_target_train[cl_pos]]
-                            )
+                            _logit(confidences[i, y_target_train[cl_pos]])
                         )
                     else:
                         train_row_to_confidence[i].append(_logit(0))
@@ -299,9 +292,7 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
                     cl_pos = class_map.get(y_shadow_train[i], -1)
                     if cl_pos >= 0:
                         shadow_row_to_confidence[i].append(
-                            _logit(
-                                shadow_confidences[i, y_shadow_train[cl_pos]]
-                            )
+                            _logit(shadow_confidences[i, y_shadow_train[cl_pos]])
                         )
                     else:
                         shadow_row_to_confidence[i].append(_logit(0))
@@ -313,8 +304,12 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
         for i in range(n_train_rows):
             true_score = _logit(target_train_preds[i, y_target_train[i]])
             null_scores = np.array(train_row_to_confidence[i])
-            mean_null = null_scores.mean()
-            var_null = max(null_scores.var(), EPS) # var can be zero in some cases
+            mean_null = 0.0
+            var_null = 0.0
+            if not np.isnan(null_scores).all():
+                mean_null = np.nanmean(null_scores)  # null_scores.mean()
+                var_null = np.nanvar(null_scores)  #
+            var_null = max(var_null, EPS)  # var can be zero in some cases
             prob = norm.cdf(true_score, loc=mean_null, scale=np.sqrt(var_null))
             mia_scores.append([1 - prob, prob])
             mia_labels.append(1)
@@ -324,7 +319,7 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
             true_score = _logit(shadow_train_preds[i, y_shadow_train[i]])
             null_scores = np.array(shadow_row_to_confidence[i])
             mean_null = null_scores.mean()
-            var_null = max(null_scores.var(), EPS) # var can be zeros in some cases
+            var_null = max(null_scores.var(), EPS)  # var can be zeros in some cases
             prob = norm.cdf(true_score, loc=mean_null, scale=np.sqrt(var_null))
             mia_scores.append([1 - prob, prob])
             mia_labels.append(0)
@@ -335,7 +330,7 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements
             metrics.get_metrics(mia_clf, np.array(mia_scores), np.array(mia_labels))
         ]
 
-    def example(self) -> None: # pylint: disable = too-many-locals
+    def example(self) -> None:  # pylint: disable = too-many-locals
         """Runs an example attack using data from sklearn
 
         Generates example data, trains a classifier and tuns the attack
@@ -353,35 +348,42 @@ def example(self) -> None: # pylint: disable = too-many-locals
             rf.predict_proba(train_X),
             test_X,
             test_y,
-            rf.predict_proba(test_X)
+            rf.predict_proba(test_X),
         )
 
     def _construct_metadata(self) -> None:
         """Constructs the metadata object. Called by the reporting method"""
         self.metadata = {}
-        self.metadata['experiment_details'] = {}
-        self.metadata['experiment_details'].update(self.args.__dict__)
-        if 'func' in self.metadata['experiment_details']:
-            del self.metadata['experiment_details']['func']
+        self.metadata["experiment_details"] = {}
+        self.metadata["experiment_details"].update(self.args.__dict__)
+        if "func" in self.metadata["experiment_details"]:
+            del self.metadata["experiment_details"]["func"]
 
-        self.metadata['global_metrics'] = {}
+        self.metadata["global_metrics"] = {}
 
-        pdif = np.exp(-self.attack_metrics[0]['PDIF01'])
+        pdif = np.exp(-self.attack_metrics[0]["PDIF01"])
 
-        self.metadata['global_metrics']['PDIF_sig'] = f"Significant at p={self.args.p_thresh}" if \
-            pdif <= self.args.p_thresh else f"Not significant at p={self.args.p_thresh}"
+        self.metadata["global_metrics"]["PDIF_sig"] = (
+            f"Significant at p={self.args.p_thresh}"
+            if pdif <= self.args.p_thresh
+            else f"Not significant at p={self.args.p_thresh}"
+        )
 
         auc_p, auc_std = metrics.auc_p_val(
-            self.attack_metrics[0]['AUC'],
-            self.attack_metrics[0]['n_pos_test_examples'],
-            self.attack_metrics[0]['n_neg_test_examples']
+            self.attack_metrics[0]["AUC"],
+            self.attack_metrics[0]["n_pos_test_examples"],
+            self.attack_metrics[0]["n_neg_test_examples"],
         )
-        self.metadata['global_metrics']['AUC_sig'] = f"Significant at p={self.args.p_thresh}" if \
-            auc_p <= self.args.p_thresh else f"Not significant at p={self.args.p_thresh}"
-        self.metadata['global_metrics']['AUC NULL 3sd range'] = \
-            f"{0.5 - 3 * auc_std} -> {0.5 + 3 * auc_std}"
+        self.metadata["global_metrics"]["AUC_sig"] = (
+            f"Significant at p={self.args.p_thresh}"
+            if auc_p <= self.args.p_thresh
+            else f"Not significant at p={self.args.p_thresh}"
+        )
+        self.metadata["global_metrics"][
+            "AUC NULL 3sd range"
+        ] = f"{0.5 - 3 * auc_std} -> {0.5 + 3 * auc_std}"
 
-        self.metadata['attack'] = str(self)
+        self.metadata["attack"] = str(self)
 
     def make_report(self) -> dict:
         """Create the report
@@ -398,17 +400,17 @@ def make_report(self) -> dict:
         logger = logging.getLogger("reporting")
         output = {}
         logger.info("Starting report, report_name = %s", self.args.report_name)
-        output['attack_metrics'] = self.attack_metrics
+        output["attack_metrics"] = self.attack_metrics
         self._construct_metadata()
-        output['metadata'] = self.metadata
+        output["metadata"] = self.metadata
         if self.args.report_name is not None:
             json_report = report.create_json_report(output)
-            with open(f"{self.args.report_name}.json", 'w', encoding='utf-8') as f:
+            with open(f"{self.args.report_name}.json", "w", encoding="utf-8") as f:
                 f.write(json_report)
             logger.info("Wrote report to %s", f"{self.args.report_name}.json")
 
             pdf = report.create_lr_report(output)
-            pdf.output(f"{self.args.report_name}.pdf", 'F')
+            pdf.output(f"{self.args.report_name}.pdf", "F")
             logger.info("Wrote pdf report to %s", f"{self.args.report_name}.pdf")
         return output
 
@@ -436,64 +438,53 @@ def setup_example_data(self) -> None:
         np.savetxt("train_preds.csv", train_preds, delimiter=",")
         np.savetxt("test_preds.csv", test_preds, delimiter=",")
 
-
         config = {
             "training_data_file": "train_data.csv",
             "testing_data_file": "test_data.csv",
             "training_preds_file": "train_preds.csv",
             "testing_preds_file": "test_preds.csv",
             "target_model": ["sklearn.ensemble", "RandomForestClassifier"],
-            "target_hyppars": {
-                "min_samples_split": 2,
-                "min_samples_leaf": 1
-            }
+            "target_hyppars": {"min_samples_split": 2, "min_samples_leaf": 1},
         }
 
-        with open('config.json', 'w', encoding='utf-8') as f:
+        with open("config.json", "w", encoding="utf-8") as f:
             f.write(json.dumps(config))
 
-    def attack_from_config(self) -> None: # pylint: disable = too-many-locals
-        """Runs an attack based on the args parsed from the command line
-        """
+    def attack_from_config(self) -> None:  # pylint: disable = too-many-locals
+        """Runs an attack based on the args parsed from the command line"""
         logger = logging.getLogger("run-attack")
         logger.info("Reading config from %s", self.args.json_file)
-        with open(self.args.json_file, encoding='utf-8') as f:
+        with open(self.args.json_file, encoding="utf-8") as f:
             config = json.loads(f.read())
 
-        logger.info("Loading training data csv from %s", config['training_data_file'])
-        training_data = np.loadtxt(config['training_data_file'], delimiter=",")
+        logger.info("Loading training data csv from %s", config["training_data_file"])
+        training_data = np.loadtxt(config["training_data_file"], delimiter=",")
         train_X = training_data[:, :-1]
         train_y = training_data[:, -1].flatten().astype(int)
         logger.info("Loaded %d rows", len(train_X))
 
-        logger.info("Loading test data csv from %s", config['testing_data_file'])
-        test_data = np.loadtxt(config['testing_data_file'], delimiter=",")
+        logger.info("Loading test data csv from %s", config["testing_data_file"])
+        test_data = np.loadtxt(config["testing_data_file"], delimiter=",")
         test_X = test_data[:, :-1]
         test_y = test_data[:, -1].flatten().astype(int)
         logger.info("Loaded %d rows", len(test_X))
 
-        logger.info("Loading train predictions form %s", config['training_preds_file'])
-        train_preds = np.loadtxt(config['training_preds_file'], delimiter=",")
+        logger.info("Loading train predictions form %s", config["training_preds_file"])
+        train_preds = np.loadtxt(config["training_preds_file"], delimiter=",")
         assert len(train_preds) == len(train_X)
 
-        logger.info("Loading test predictions form %s", config['testing_preds_file'])
-        test_preds = np.loadtxt(config['testing_preds_file'], delimiter=",")
+        logger.info("Loading test predictions form %s", config["testing_preds_file"])
+        test_preds = np.loadtxt(config["testing_preds_file"], delimiter=",")
         assert len(test_preds) == len(test_X)
 
-        clf_module_name, clf_class_name = config['target_model']
+        clf_module_name, clf_class_name = config["target_model"]
         module = importlib.import_module(clf_module_name)
         clf_class = getattr(module, clf_class_name)
-        clf_params = config['target_hyppars']
+        clf_params = config["target_hyppars"]
         clf = clf_class(**clf_params)
         logger.info("Created model: %s", str(clf))
         self.run_scenario_from_preds(
-            clf,
-            train_X,
-            train_y,
-            train_preds,
-            test_X,
-            test_y,
-            test_preds
+            clf, train_X, train_y, train_preds, test_X, test_y, test_preds
         )
         logger.info("Computing metrics")
 
@@ -505,6 +496,7 @@ def _setup_example_data(args):
     attack_obj = LIRAAttack(lira_args)
     attack_obj.setup_example_data()
 
+
 def _example(args):
     """Call the methods to run an example"""
     lira_args = LIRAAttackArgs(**args.__dict__)
@@ -512,6 +504,7 @@ def _example(args):
     attack_obj.example()
     attack_obj.make_report()
 
+
 def _run_attack(args):
     """Run a command line attack based on saved files described in .json file"""
     lira_args = LIRAAttackArgs(**args.__dict__)
@@ -521,56 +514,56 @@ def _run_attack(args):
 
 
 def main():
-    """Main method to parse args and invoke relevant code
-    """
+    """Main method to parse args and invoke relevant code"""
     parser = argparse.ArgumentParser(add_help=False)
-    parser.add_argument('-s', '--n-shadow-models',
+    parser.add_argument(
+        "-s",
+        "--n-shadow-models",
         type=int,
         required=False,
         default=N_SHADOW_MODELS,
-        action='store',
-        dest='n_shadow_models',
-        help=(
-            'The number of shadow models to train (default = %(default)d)'
-        )
+        action="store",
+        dest="n_shadow_models",
+        help=("The number of shadow models to train (default = %(default)d)"),
     )
-    parser.add_argument('--report-name',
+    parser.add_argument(
+        "--report-name",
         type=str,
         action="store",
         dest="report_name",
         required=False,
         default="lr_report",
-        help=(
-            'Output name for the report. Default = %(default)s'
-        )
+        help=("Output name for the report. Default = %(default)s"),
     )
-    parser.add_argument('-p', '--p-thresh',
+    parser.add_argument(
+        "-p",
+        "--p-thresh",
         type=float,
-        action='store',
-        dest='p_thresh',
+        action="store",
+        dest="p_thresh",
         required=False,
         default=P_THRESH,
-        help=(
-            'Significance threshold for p-value comparisons. Default = %(default)f'
-        )
+        help=("Significance threshold for p-value comparisons. Default = %(default)f"),
     )
     subparsers = parser.add_subparsers()
-    example_parser = subparsers.add_parser('run-example', parents=[parser])
+    example_parser = subparsers.add_parser("run-example", parents=[parser])
     example_parser.set_defaults(func=_example)
 
-    attack_parser = subparsers.add_parser('run-attack', parents=[parser])
-    attack_parser.add_argument('-j', '--json-file',
+    attack_parser = subparsers.add_parser("run-attack", parents=[parser])
+    attack_parser.add_argument(
+        "-j",
+        "--json-file",
         action="store",
         required=True,
         dest="json_file",
         type=str,
         help=(
-            'Name of the .json file containing details for the run. Default = %(default)s'
-        )
+            "Name of the .json file containing details for the run. Default = %(default)s"
+        ),
     )
     attack_parser.set_defaults(func=_run_attack)
 
-    example_data_parser = subparsers.add_parser('setup-example-data')
+    example_data_parser = subparsers.add_parser("setup-example-data")
     example_data_parser.set_defaults(func=_setup_example_data)
 
     args = parser.parse_args()
@@ -581,5 +574,5 @@ def main():
         print("Invalid command. Try --help to get more details")
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
diff --git a/attacks/metrics.py b/attacks/metrics.py
index 5a5c2637..2962f588 100644
--- a/attacks/metrics.py
+++ b/attacks/metrics.py
@@ -1,20 +1,21 @@
-'''
+"""
 Calculate metrics.
-'''
+"""
 
 from collections.abc import Iterable
+
 import numpy as np
-from sklearn.metrics import confusion_matrix
-from sklearn.metrics import roc_auc_score
-from sklearn.metrics import roc_curve
-from scipy.stats import norm
 from scipy import interpolate
+from scipy.stats import norm
+from sklearn.metrics import confusion_matrix, roc_auc_score, roc_curve
+
 from .mia_extremecase import min_max_disc
 
 # pylint: disable = invalid-name
 
 VAR_THRESH = 1e-2
 
+
 def _div(x: float, y: float, default: float) -> float:
     """Solve the problem of division by 0 and round up.
     If y is non-zero, perform x/y and round to 8dp. If it is zero, return the default
@@ -39,11 +40,13 @@ def _div(x: float, y: float, default: float) -> float:
         division = float(default)
     return division
 
+
 def _tpr_at_fpr(
     y_true: Iterable[float],
     y_score: Iterable[float],
-    fpr: float=0.001,
-    fpr_perc: bool=False) -> float:
+    fpr: float = 0.001,
+    fpr_perc: bool = False,
+) -> float:
     """Compute the TPR at a fixed FPR.
     In particular, returns the TPR value corresponding to a particular FPR. Uses interpolation
     to fill in gaps.
@@ -66,8 +69,7 @@ def _tpr_at_fpr(
     """
 
     if fpr_perc:
-        fpr /= 100.
-
+        fpr /= 100.0
 
     fpr_vals, tpr_vals, thresh_vals = roc_curve(y_true, y_score)
     thresh_from_fpr = interpolate.interp1d(fpr_vals, thresh_vals)
@@ -78,8 +80,9 @@ def _tpr_at_fpr(
 
     return tpr
 
+
 def _expected_auc_var(auc: float, num_pos: int, num_neg: int) -> float:
-    """"Compute variance of AUC under assumption of uniform probabilities
+    """ "Compute variance of AUC under assumption of uniform probabilities
     uses the expression given as eqn (2) in  https://cs.nyu.edu/~mohri/pub/area.pdf
 
     Parameters
@@ -97,11 +100,15 @@ def _expected_auc_var(auc: float, num_pos: int, num_neg: int) -> float:
     var: float
         null variance of AUC
     """
-    p_xxy = p_xyy = 1/3
-    var = (auc * (1 - auc) + (num_pos - 1) * (p_xxy - auc**2) + (num_neg - 1) * (p_xyy - auc**2)) /\
-        (num_pos * num_neg)
+    p_xxy = p_xyy = 1 / 3
+    var = (
+        auc * (1 - auc)
+        + (num_pos - 1) * (p_xxy - auc**2)
+        + (num_neg - 1) * (p_xyy - auc**2)
+    ) / (num_pos * num_neg)
     return var
 
+
 def auc_p_val(auc: float, n_pos: int, n_neg: int) -> tuple[float, float]:
     """Compute the p-value for a given AUC
 
@@ -122,20 +129,17 @@ def auc_p_val(auc: float, n_pos: int, n_neg: int) -> tuple[float, float]:
         standard deviation of the NULL AUC diustribution (mean = 0.5)
 
     """
-    auc_std = np.sqrt(
-        _expected_auc_var(
-            0.5,
-            n_pos,
-            n_neg
-        )
-    )
+    auc_std = np.sqrt(_expected_auc_var(0.5, n_pos, n_neg))
     auc_p = 1 - norm.cdf(auc, loc=0.5, scale=auc_std)
     return auc_p, auc_std
 
-def get_metrics(clf, # pylint: disable = too-many-locals
-                X_test:np.ndarray,
-                y_test:np.ndarray,
-                permute_rows:bool=True):
+
+def get_metrics(  # pylint: disable=too-many-locals
+    clf,
+    X_test: np.ndarray,
+    y_test: np.ndarray,
+    permute_rows: bool = True,
+):
     """
     Calculate metrics, including attacker advantage for MIA binary.
     Implemented as Definition 4 on https://arxiv.org/pdf/1709.01604.pdf
@@ -157,98 +161,100 @@ def get_metrics(clf, # pylint: disable = too-many-locals
 
     Notes
     -----
-    Includes the following metrics
-
-    True positive rate or recall (TPR)
-    False positive rate (FPR), proportion of negative examples incorrectly classified as positives
-    False alarm rate (FAR), proportion of objects classified as positives that are incorrect,
-        also known as false discovery rate
-    True neagative rate (TNR)
-    Positive predictive value or precision (PPV)
-    Negative predictive value (NPV)
-    False neagative rate (FNR)
-    Accuracy (ACC)
-    F1 Score - harmonic mean of precision and recall.
-    Advantage
+    Includes the following metrics:
+
+    * True positive rate or recall (TPR).
+    * False positive rate (FPR), proportion of negative examples incorrectly \
+    classified as positives.
+    * False alarm rate (FAR), proportion of objects classified as positives \
+    that are incorrect, also known as false discovery rate.
+    * True neagative rate (TNR).
+    * Positive predictive value or precision (PPV).
+    * Negative predictive value (NPV).
+    * False neagative rate (FNR).
+    * Accuracy (ACC).
+    * F1 Score - harmonic mean of precision and recall.
+    * Advantage.
     """
     metrics = {}
     if permute_rows:
         N, _ = X_test.shape
-        order = np.random.RandomState(seed=10).permutation(N) # pylint: disable = no-member
+        order = np.random.RandomState(  # pylint: disable = no-member
+            seed=10
+        ).permutation(N)
         X_test = X_test[order, :]
         y_test = y_test[order]
     y_pred = clf.predict(X_test)
     tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
-    #print('tn', tn, 'fp',fp,'fn', fn,'tp', tp)
+    # print('tn', tn, 'fp',fp,'fn', fn,'tp', tp)
 
     # true positive rate or recall
-    metrics['TPR'] = round(float(tp/(tp + fn)), 8)
+    metrics["TPR"] = round(float(tp / (tp + fn)), 8)
     # false positive rate, proportion of negative examples incorrectly classified as positives
-    metrics['FPR'] = round(float(fp / (fp + tn)), 8)
+    metrics["FPR"] = round(float(fp / (fp + tn)), 8)
     # False alarm rate, proportion of things classified as positives that are incorrect,
     # also known as false discovery rate
-    metrics['FAR'] = _div(fp, (fp + tp), 0)
+    metrics["FAR"] = _div(fp, (fp + tp), 0)
     # true negative rate or specificity
-    metrics['TNR'] = round(float(tn / (tn + fp)), 8)
+    metrics["TNR"] = round(float(tn / (tn + fp)), 8)
     # precision or positive predictive value
-    metrics['PPV'] = _div(tp, (tp + fp), 0)
+    metrics["PPV"] = _div(tp, (tp + fp), 0)
     # negative predictive value
-    metrics['NPV'] = _div(tn, (tn + fn), 0)
+    metrics["NPV"] = _div(tn, (tn + fn), 0)
     # false negative rate
-    metrics['FNR'] = round(float(fn / (tp + fn)), 8)
+    metrics["FNR"] = round(float(fn / (tp + fn)), 8)
     # overall accuracy
-    metrics['ACC'] = round(float((tp + tn) / (tp + fp + fn + tn)), 8)
+    metrics["ACC"] = round(float((tp + tn) / (tp + fp + fn + tn)), 8)
     # harmonic mean of precision and sensitivity
-    metrics['F1score'] = _div(2*metrics['PPV']*metrics['TPR'], metrics['PPV']+metrics['TPR'], 0)
+    metrics["F1score"] = _div(
+        2 * metrics["PPV"] * metrics["TPR"], metrics["PPV"] + metrics["TPR"], 0
+    )
     # Advantage: TPR - FPR
-    metrics['Advantage'] = float(abs(metrics['TPR']-metrics['FPR']))
+    metrics["Advantage"] = float(abs(metrics["TPR"] - metrics["FPR"]))
 
-    #calculate AUC of model
+    # calculate AUC of model
     y_pred_proba = clf.predict_proba(X_test)[:, 1]
-    metrics['AUC'] = round(roc_auc_score(y_test, y_pred_proba),8)
+    metrics["AUC"] = round(roc_auc_score(y_test, y_pred_proba), 8)
 
     # Calculate AUC p-val
-    metrics['P_HIGHER_AUC'], _ = auc_p_val(
-        metrics['AUC'],
-        y_test.sum(),
-        len(y_test) - y_test.sum()
+    metrics["P_HIGHER_AUC"], _ = auc_p_val(
+        metrics["AUC"], y_test.sum(), len(y_test) - y_test.sum()
     )
 
     fmax, fmin, fdif, pdif = min_max_disc(y_test, y_pred_proba)
-    metrics['FMAX01'] = fmax
-    metrics['FMIN01'] = fmin
-    metrics['FDIF01'] = fdif
-    metrics['PDIF01'] = -pdif # use -log(p) so answer is positive
+    metrics["FMAX01"] = fmax
+    metrics["FMIN01"] = fmin
+    metrics["FDIF01"] = fdif
+    metrics["PDIF01"] = -pdif  # use -log(p) so answer is positive
 
     fmax, fmin, fdif, pdif = min_max_disc(y_test, y_pred_proba, x_prop=0.2)
-    metrics['FMAX02'] = fmax
-    metrics['FMIN02'] = fmin
-    metrics['FDIF02'] = fdif
-    metrics['PDIF02'] = -pdif # use -log(p) so answer is positive
+    metrics["FMAX02"] = fmax
+    metrics["FMIN02"] = fmin
+    metrics["FDIF02"] = fdif
+    metrics["PDIF02"] = -pdif  # use -log(p) so answer is positive
 
     fmax, fmin, fdif, pdif = min_max_disc(y_test, y_pred_proba, x_prop=0.01)
-    metrics['FMAX001'] = fmax
-    metrics['FMIN001'] = fmin
-    metrics['FDIF001'] = fdif
-    metrics['PDIF001'] = -pdif # use -log(p) so answer is positive
-
+    metrics["FMAX001"] = fmax
+    metrics["FMIN001"] = fmin
+    metrics["FDIF001"] = fdif
+    metrics["PDIF001"] = -pdif  # use -log(p) so answer is positive
 
     # Add some things useful for debugging / filtering
-    metrics['pred_prob_var'] = y_pred_proba.var()
+    metrics["pred_prob_var"] = y_pred_proba.var()
 
     # TPR at various FPR
     fpr_vals = [0.5, 0.2, 0.1, 0.01, 0.001, 0.00001]
     for fpr in fpr_vals:
         tpr = _tpr_at_fpr(y_test, y_pred_proba, fpr=fpr)
-        name = f'TPR@{fpr}'
+        name = f"TPR@{fpr}"
         metrics[name] = tpr
 
     fpr, tpr, roc_thresh = roc_curve(y_test, y_pred_proba)
-    metrics['fpr'] = fpr
-    metrics['tpr'] = tpr
-    metrics['roc_thresh'] = roc_thresh
+    metrics["fpr"] = fpr
+    metrics["tpr"] = tpr
+    metrics["roc_thresh"] = roc_thresh
 
-    metrics['n_pos_test_examples'] = y_test.sum()
-    metrics['n_neg_test_examples'] = (1 - y_test).sum()
+    metrics["n_pos_test_examples"] = y_test.sum()
+    metrics["n_neg_test_examples"] = (1 - y_test).sum()
 
     return metrics
diff --git a/attacks/mia_extremecase.py b/attacks/mia_extremecase.py
index 55c4c28b..6c399766 100644
--- a/attacks/mia_extremecase.py
+++ b/attacks/mia_extremecase.py
@@ -2,11 +2,10 @@
 import numpy as np
 from scipy.stats import norm
 
+
 def min_max_disc(
-    y_true: np.ndarray,
-    pred_probs: np.ndarray,
-    x_prop: float=0.1,
-    log_p: bool=True) -> tuple[float, float, float, float]: #pylint: disable = line-too-long
+    y_true: np.ndarray, pred_probs: np.ndarray, x_prop: float = 0.1, log_p: bool = True
+) -> tuple[float, float, float, float]:  # pylint: disable = line-too-long
     """
     Non-average-case methods for MIA attacks. Considers actual frequency of membership
     amongst samples with highest- and lowest- assessed probability of membership. If an
@@ -57,8 +56,8 @@ def min_max_disc(
     """
 
     n_examples = int(np.ceil(len(y_true) * x_prop))
-    pos_frequency = np.mean(y_true) # average frequency
-    y_order = np.argsort(pred_probs) # ordering permutation
+    pos_frequency = np.mean(y_true)  # average frequency
+    y_order = np.argsort(pred_probs)  # ordering permutation
 
     # Frequencies
     # y values corresponding to lowest k values of yp
@@ -72,12 +71,12 @@ def min_max_disc(
     # P-value
     # mmd is asymptotically distributed as N(0,sdm^2) under null.
     sdm = np.sqrt(2 * pos_frequency * (1 - pos_frequency) / n_examples)
-    pval = 1 - norm.cdf(mmd, loc=0, scale=sdm) # normal CDF
+    pval = 1 - norm.cdf(mmd, loc=0, scale=sdm)  # normal CDF
     if log_p:
-        if pval< 1e-50:
-            pval=-115.13
+        if pval < 1e-50:
+            pval = -115.13
         else:
-            pval=np.log(pval)
+            pval = np.log(pval)
 
     # Return
     return maxd, mind, mmd, pval
diff --git a/attacks/report.py b/attacks/report.py
index ec22d5ef..1c6a2b81 100644
--- a/attacks/report.py
+++ b/attacks/report.py
@@ -1,7 +1,8 @@
 """Code for automatic report generation"""
 import json
-import pylab as plt
+
 import numpy as np
+import pylab as plt
 from fpdf import FPDF
 
 # Adds a border to all pdf cells of set to 1 -- useful for debugging
@@ -16,64 +17,64 @@
     "TPR@0.1",
     "TPR@0.01",
     "TPR@0.001",
-    "TPR@1e-05"
+    "TPR@1e-05",
 ]
 
-MAPPINGS = {
-    'PDIF': lambda x: np.exp(-x)
-}
+MAPPINGS = {"PDIF": lambda x: np.exp(-x)}
 
 INTRODUCTION = (
-    'This report provides a summary of a series of simulated attack experiments performed '
-    'on the model outputs provided. An attack model is trained to attempt to distinguish '
-    'between outputs from training (in-sample) and testing (out-of-sample) data. The metrics '
-    'below describe the success of this classifier. A successful classifier indicates that the '
-    'original model is unsafe and should not be allowed to be released from the TRE.\n'
-    'In particular, the simulation splits the data provided into test and train sets (each will '
-    'in- and out-of-sample examples). The classifier is trained on the train set and evaluated '
-    'on the test set. This is repeated with different train/test splits a user-specified number '
-    'of times.\n'
-    'To help place the results in context, the code may also have run a series of baseline '
-    'experiments. In these, random model outputs for hypothetical in- and out-of-sample data are '
-    'generated with identical statistical properties. In these baseline cases, there is no signal '
-    'that an attacker could leverage and therefore these values provide a baseline against '
-    'which the actual values can be compared.\n'
-    'For some metrics (FDIF and AUC), we are able to compute p-values. In each case, shown below '
-    '(in the Global metrics sections) is the number of repetitions that exceeded the p-value '
-    'threshold both without, and with correction for multiple testing (Benjamini-Hochberg '
-    'procedure).\n'
-    'ROC curves for all real (red) and dummy (blue) repetitions are provided. These are shown in '
-    'log space (as reommended here [ADD URL]) to emphasise the region in which risk is highest -- '
-    'the bottom left (are high true positive rates possible with low false positive rates).\n'
-    'A description of the metrics and how to interpret them within the context of an attack is '
-    'given below.'
+    "This report provides a summary of a series of simulated attack experiments performed "
+    "on the model outputs provided. An attack model is trained to attempt to distinguish "
+    "between outputs from training (in-sample) and testing (out-of-sample) data. The metrics "
+    "below describe the success of this classifier. A successful classifier indicates that the "
+    "original model is unsafe and should not be allowed to be released from the TRE.\n"
+    "In particular, the simulation splits the data provided into test and train sets (each will "
+    "in- and out-of-sample examples). The classifier is trained on the train set and evaluated "
+    "on the test set. This is repeated with different train/test splits a user-specified number "
+    "of times.\n"
+    "To help place the results in context, the code may also have run a series of baseline "
+    "experiments. In these, random model outputs for hypothetical in- and out-of-sample data are "
+    "generated with identical statistical properties. In these baseline cases, there is no signal "
+    "that an attacker could leverage and therefore these values provide a baseline against "
+    "which the actual values can be compared.\n"
+    "For some metrics (FDIF and AUC), we are able to compute p-values. In each case, shown below "
+    "(in the Global metrics sections) is the number of repetitions that exceeded the p-value "
+    "threshold both without, and with correction for multiple testing (Benjamini-Hochberg "
+    "procedure).\n"
+    "ROC curves for all real (red) and dummy (blue) repetitions are provided. These are shown in "
+    "log space (as reommended here [ADD URL]) to emphasise the region in which risk is highest -- "
+    "the bottom left (are high true positive rates possible with low false positive rates).\n"
+    "A description of the metrics and how to interpret them within the context of an attack is "
+    "given below."
 )
 
 LOGROC_CAPTION = (
-    'This plot shows the False Positive Rate (x) versus the True Positive Rate (y). '
-    'The axes are in log space enabling us to focus on areas where the False Positive Rate is low '
-    '(left hand area). Curves above the y = x line (black dashes) in this region represent a '
-    'disclosure risk as an attacker can obtain many more true than false positives. '
-    'The solid coloured lines show the curves for the attack simulations with the true model '
-    'outputs. The lighter grey lines show the curves for randomly generated outputs with '
-    'no structure (i.e. in- and out-of- sample predictions are generated from the same '
-    'distributions. Solid curves consistently higher than the grey curves in the left hand '
-    'part of the plot are a sign of concern.'
+    "This plot shows the False Positive Rate (x) versus the True Positive Rate (y). "
+    "The axes are in log space enabling us to focus on areas where the False Positive Rate is low "
+    "(left hand area). Curves above the y = x line (black dashes) in this region represent a "
+    "disclosure risk as an attacker can obtain many more true than false positives. "
+    "The solid coloured lines show the curves for the attack simulations with the true model "
+    "outputs. The lighter grey lines show the curves for randomly generated outputs with "
+    "no structure (i.e. in- and out-of- sample predictions are generated from the same "
+    "distributions. Solid curves consistently higher than the grey curves in the left hand "
+    "part of the plot are a sign of concern."
 )
 
 GLOSSARY = {
-    'AUC': 'Area Under the ROC curve',
-    'True Positive Rate (TPR)': (
-        'The true positive rate is the number of True Positives that are predicted as positive as '
-        'a proportion of the total number of positives. If an attacker has N examples that were '
-        'actually in the training set, the TPR is the proportion of these that they predict as '
-        'being in the training set.'
+    "AUC": "Area Under the ROC curve",
+    "True Positive Rate (TPR)": (
+        "The true positive rate is the number of True Positives that are predicted as positive as "
+        "a proportion of the total number of positives. If an attacker has N examples that were "
+        "actually in the training set, the TPR is the proportion of these that they predict as "
+        "being in the training set."
     ),
-    'ACC': 'The proportion of predictions that the attacker makes that are correct.'
+    "ACC": "The proportion of predictions that the attacker makes that are correct.",
 }
 
+
 class NumpyArrayEncoder(json.JSONEncoder):
     """Json encoder that can cope with numpy arrays"""
+
     def default(self, o):
         """If an object is an np.ndarray, convert to list"""
         if isinstance(o, np.ndarray):
@@ -88,50 +89,59 @@ def default(self, o):
 def _write_dict(pdf, input_dict, indent=0, border=BORDER):
     """Write a dictionary to the pdf"""
     for key, value in input_dict.items():
-        pdf.set_font('arial', 'B', 14)
-        pdf.cell(75, 5, key, border, 1, 'L')
+        pdf.set_font("arial", "B", 14)
+        pdf.cell(75, 5, key, border, 1, "L")
         pdf.cell(indent, 0)
-        pdf.set_font('arial', '', 12)
-        pdf.multi_cell(150, 5, value, border, 'L')
+        pdf.set_font("arial", "", 12)
+        pdf.multi_cell(150, 5, value, border, "L")
         pdf.ln(h=5)
 
-def title(pdf, text, border=BORDER, font_size=24, font_style='B'):
+
+def title(pdf, text, border=BORDER, font_size=24, font_style="B"):
     """Write a title block"""
-    pdf.set_font('arial', font_style, font_size)
+    pdf.set_font("arial", font_style, font_size)
     pdf.ln(h=5)
-    pdf.cell(0, 0, text, border, 1, 'C')
+    pdf.cell(0, 0, text, border, 1, "C")
     pdf.ln(h=5)
 
-def subtitle(pdf, text, indent=10, border=BORDER, font_size=12, font_style='B'): # pylint: disable = too-many-arguments
+
+def subtitle(
+    pdf, text, indent=10, border=BORDER, font_size=12, font_style="B"
+):  # pylint: disable = too-many-arguments
     """Write a subtitle block"""
     pdf.cell(indent, border=border)
-    pdf.set_font('arial', font_style, font_size)
+    pdf.set_font("arial", font_style, font_size)
     pdf.cell(75, 10, text, border, 1)
 
-def line(pdf, text, indent=0, border=BORDER, font_size=11, font_style='', font='arial'): # pylint: disable = too-many-arguments
+
+def line(
+    pdf, text, indent=0, border=BORDER, font_size=11, font_style="", font="arial"
+):  # pylint: disable = too-many-arguments
     """Write a standard block"""
     if indent > 0:
         pdf.cell(indent, border=border)
     pdf.set_font(font, font_style, font_size)
     pdf.multi_cell(0, 5, text, border, 1)
 
+
 def _roc_plot_single(metrics, save_name):
     """Create a roc_plot for a single experiment"""
     plt.figure()
-    plt.plot([0, 1], [0, 1], 'k--')
-    plt.plot(metrics['fpr'], metrics['tpr'], 'r', linewidth=2)
-    plt.xscale('log')
-    plt.yscale('log')
+    plt.plot([0, 1], [0, 1], "k--")
+    plt.plot(metrics["fpr"], metrics["tpr"], "r", linewidth=2)
+    plt.xscale("log")
+    plt.yscale("log")
     plt.grid()
     plt.xlabel("True Positive Rate")
     plt.ylabel("False Positive Rate")
     plt.tight_layout()
     plt.savefig(save_name)
 
+
 def _roc_plot(metrics, dummy_metrics, save_name):
     """Create a roc plot for multiple repetitions"""
     plt.figure()
-    plt.plot([0, 1], [0, 1], 'k--')
+    plt.plot([0, 1], [0, 1], "k--")
     if dummy_metrics is None or len(dummy_metrics) == 0:
         do_dummy = False
     else:
@@ -142,43 +152,47 @@ def _roc_plot(metrics, dummy_metrics, save_name):
     # base_fpr = np.logspace(-4, 0, 1000)
     all_tpr = np.zeros((len(metrics), len(base_fpr)), float)
     for i, metric_set in enumerate(metrics):
-        all_tpr[i, :] = np.interp(base_fpr, metric_set['fpr'], metric_set['tpr'])
+        all_tpr[i, :] = np.interp(base_fpr, metric_set["fpr"], metric_set["tpr"])
 
     if do_dummy:
         all_tpr_dummy = np.zeros((len(dummy_metrics), len(base_fpr)), float)
         for i, metric_set in enumerate(dummy_metrics):
-            all_tpr_dummy[i, :] = np.interp(base_fpr, metric_set['fpr'], metric_set['tpr'])
+            all_tpr_dummy[i, :] = np.interp(
+                base_fpr, metric_set["fpr"], metric_set["tpr"]
+            )
 
         for _, metric_set in enumerate(dummy_metrics):
             plt.plot(
-                metric_set['fpr'],
-                metric_set['tpr'],
-                color='lightsteelblue',
+                metric_set["fpr"],
+                metric_set["tpr"],
+                color="lightsteelblue",
                 linewidth=0.5,
-                alpha=0.5
+                alpha=0.5,
             )
 
     for _, metric_set in enumerate(metrics):
-        plt.plot(metric_set['fpr'], metric_set['tpr'], color='lightsalmon', linewidth=0.5)
-
+        plt.plot(
+            metric_set["fpr"], metric_set["tpr"], color="lightsalmon", linewidth=0.5
+        )
 
     tpr_mu = all_tpr.mean(axis=0)
-    plt.plot(base_fpr, tpr_mu, 'r')
+    plt.plot(base_fpr, tpr_mu, "r")
 
     if do_dummy:
         dummy_mu = all_tpr_dummy.mean(axis=0)
-        plt.plot(base_fpr, dummy_mu, 'b')
+        plt.plot(base_fpr, dummy_mu, "b")
 
-    plt.xscale('log')
-    plt.yscale('log')
-    plt.xlabel('False Positive Rate')
-    plt.ylabel('True Positive Rate')
+    plt.xscale("log")
+    plt.yscale("log")
+    plt.xlabel("False Positive Rate")
+    plt.ylabel("True Positive Rate")
     plt.tight_layout()
     plt.grid()
     plt.savefig(save_name)
 
+
 def create_mia_report(attack_output: dict) -> FPDF:
-    '''make a worst case membership inference report
+    """make a worst case membership inference report
 
     Parameters
     ----------
@@ -201,11 +215,11 @@ def create_mia_report(attack_output: dict) -> FPDF:
     pdf: fpdf.FPDF
         fpdf document object
 
-    '''
+    """
 
-    dummy_metrics = attack_output['dummy_attack_metrics']
-    mia_metrics = attack_output['attack_metrics']
-    metadata = attack_output['metadata']
+    dummy_metrics = attack_output["dummy_attack_metrics"]
+    mia_metrics = attack_output["attack_metrics"]
+    metadata = attack_output["metadata"]
     if dummy_metrics is None or len(dummy_metrics) == 0:
         do_dummy = False
     else:
@@ -213,7 +227,6 @@ def create_mia_report(attack_output: dict) -> FPDF:
 
     _roc_plot(mia_metrics, dummy_metrics, "log_roc.png")
 
-
     pdf = FPDF()
     pdf.add_page()
     pdf.set_xy(0, 0)
@@ -221,21 +234,21 @@ def create_mia_report(attack_output: dict) -> FPDF:
     subtitle(pdf, "Introduction")
     line(pdf, INTRODUCTION)
     subtitle(pdf, "Experiment summary")
-    for key, value in metadata['experiment_details'].items():
+    for key, value in metadata["experiment_details"].items():
         line(pdf, f"{key:>30s}: {str(value):30s}", font="courier")
     subtitle(pdf, "Global metrics")
-    for key, value in metadata['global_metrics'].items():
+    for key, value in metadata["global_metrics"].items():
         line(pdf, f"{key:>30s}: {str(value):30s}", font="courier")
     if do_dummy:
         subtitle(pdf, "Baseline global metrics")
-        for key, value in metadata['baseline_global_metrics'].items():
+        for key, value in metadata["baseline_global_metrics"].items():
             line(pdf, f"{key:>30s}: {str(value):30s}", font="courier")
 
     subtitle(pdf, "Metrics")
     line(
         pdf,
         "The following show summaries of the attack metrics over the repetitions",
-        font="arial"
+        font="arial",
     )
     for metric in DISPLAY_METRICS:
         vals = np.array([m[metric] for m in mia_metrics])
@@ -245,15 +258,20 @@ def create_mia_report(attack_output: dict) -> FPDF:
             f"{metric:>12} mean = {vals.mean():.2f}, var = {vals.var():.4f}, "
             f"min = {vals.min():.2f}, max = {vals.max():.2f}"
         )
-        line(pdf, text, font='courier')
+        line(pdf, text, font="courier")
 
     if do_dummy:
         subtitle(pdf, "Baseline metrics")
-        line(pdf, (
-            'The following show summaries of the attack metrics over the repetitions where there '
-            'is no statistical difference between predictions in the training and test sets. '
-            'Simulation was done with training and test set sizes equal to the real ones'
-        ), font="arial")
+        line(
+            pdf,
+            (
+                "The following show summaries of the attack metrics over the "
+                "repetitions where there is no statistical difference between "
+                "predictions in the training and test sets. Simulation was done "
+                "with training and test set sizes equal to the real ones"
+            ),
+            font="arial",
+        )
         for metric in DISPLAY_METRICS:
             vals = np.array([m[metric] for m in dummy_metrics])
             if metric in MAPPINGS:
@@ -262,31 +280,31 @@ def create_mia_report(attack_output: dict) -> FPDF:
                 f"{metric:>12} mean = {vals.mean():.2f}, var = {vals.var():.4f}, "
                 f"min = {vals.min():.2f}, max = {vals.max():.2f}"
             )
-            line(pdf, text, font='courier')
+            line(pdf, text, font="courier")
 
     pdf.add_page()
     subtitle(pdf, "Log ROC")
-    pdf.image('log_roc.png', x = None, y = None, w = 0, h = 140, type = '', link = '')
-    pdf.set_font('arial', '', 12)
+    pdf.image("log_roc.png", x=None, y=None, w=0, h=140, type="", link="")
+    pdf.set_font("arial", "", 12)
     line(pdf, LOGROC_CAPTION)
 
     pdf.add_page()
     title(pdf, "Glossary")
     _write_dict(pdf, GLOSSARY)
 
-
     return pdf
 
+
 def create_json_report(output):
-    '''Create a report in json format for injestion by other tools
-    '''
+    """Create a report in json format for injestion by other tools"""
     # Initial work, just dump mia_metrics and dummy_metrics into a json structure
     return json.dumps(output, cls=NumpyArrayEncoder)
 
+
 def create_lr_report(output):
     """TODO"""
-    mia_metrics = output['attack_metrics'][0]
-    metadata = output['metadata']
+    mia_metrics = output["attack_metrics"][0]
+    metadata = output["metadata"]
     _roc_plot_single(mia_metrics, "log_roc.png")
     pdf = FPDF()
     pdf.add_page()
@@ -294,17 +312,19 @@ def create_lr_report(output):
     title(pdf, "Likelihood Ratio Attack Report")
     subtitle(pdf, "Introduction")
     subtitle(pdf, "Metadata")
-    for key, value in metadata['experiment_details'].items():
+    for key, value in metadata["experiment_details"].items():
         line(pdf, f"{key:>30s}: {str(value):30s}", font="courier")
-    for key, value in metadata['global_metrics'].items():
+    for key, value in metadata["global_metrics"].items():
         line(pdf, f"{key:>30s}: {str(value):30s}", font="courier")
     subtitle(pdf, "Metrics")
-    sub_metrics_dict = {key: val for key, val in mia_metrics.items() if isinstance(val, float)}
+    sub_metrics_dict = {
+        key: val for key, val in mia_metrics.items() if isinstance(val, float)
+    }
     for key, value in sub_metrics_dict.items():
         if key in MAPPINGS:
             value = MAPPINGS[key](value)
         line(pdf, f"{key:>30s}: {value:.4f}", font="courier")
     pdf.add_page()
     subtitle(pdf, "ROC Curve")
-    pdf.image('log_roc.png', x = None, y = None, w = 0, h = 140, type = '', link = '')
+    pdf.image("log_roc.png", x=None, y=None, w=0, h=140, type="", link="")
     return pdf
diff --git a/attacks/worst_case_attack.py b/attacks/worst_case_attack.py
index c465c489..2827736a 100644
--- a/attacks/worst_case_attack.py
+++ b/attacks/worst_case_attack.py
@@ -1,23 +1,20 @@
-'''
+"""
 worst_case_attack.py
 
 Runs a worst case attack based upon predictive probabilities stored in two .csv files
-'''
+"""
 
 import argparse
 import logging
-
-from typing import Any
-
 from collections.abc import Hashable
+from typing import Any
 
 import numpy as np
 import sklearn
 from sklearn.ensemble import RandomForestClassifier
 from sklearn.model_selection import train_test_split
 
-from attacks import metrics
-from attacks import report
+from attacks import metrics, report
 from attacks.attack import Attack
 from attacks.dataset import Data
 
@@ -25,24 +22,28 @@
 
 P_THRESH = 0.05
 
+
 class WorstCaseAttackArgs:
     """Arguments for worst case"""
+
     def __init__(self, **kwargs):
-        self.__dict__['n_reps'] = 10
-        self.__dict__['p_thresh'] = 0.05
-        self.__dict__['n_dummy_reps'] = 1
-        self.__dict__['train_beta'] = 2
-        self.__dict__['test_beta'] = 2
-        self.__dict__['test_prop'] = 0.3
-        self.__dict__['n_rows_in'] = 1000
-        self.__dict__['n_rows_out'] = 1000
-        self.__dict__['in_sample_filename'] = None
-        self.__dict__['out_sample_filename'] = None
-        self.__dict__['report_name'] = None
+        self.__dict__["n_reps"] = 10
+        self.__dict__["p_thresh"] = 0.05
+        self.__dict__["n_dummy_reps"] = 1
+        self.__dict__["train_beta"] = 2
+        self.__dict__["test_beta"] = 2
+        self.__dict__["test_prop"] = 0.3
+        self.__dict__["n_rows_in"] = 1000
+        self.__dict__["n_rows_out"] = 1000
+        self.__dict__["in_sample_filename"] = None
+        self.__dict__["out_sample_filename"] = None
+        self.__dict__["report_name"] = None
         self.__dict__.update(kwargs)
 
     def __str__(self):
-        return ",".join([f"{str(key)}: {str(value)}" for key, value in self.__dict__.items()])
+        return ",".join(
+            [f"{str(key)}: {str(value)}" for key, value in self.__dict__.items()]
+        )
 
     def set_param(self, key: Hashable, value: Any) -> None:
         """Set a parameter"""
@@ -52,9 +53,11 @@ def get_args(self) -> dict:
         """Return arguments"""
         return self.__dict__
 
+
 class WorstCaseAttack(Attack):
     """Class to wrap the worst case attack code"""
-    def __init__(self, args: WorstCaseAttackArgs=WorstCaseAttackArgs()):
+
+    def __init__(self, args: WorstCaseAttackArgs = WorstCaseAttackArgs()):
         self.attack_metrics = None
         self.dummy_attack_metrics = None
         self.metadata = None
@@ -63,10 +66,7 @@ def __init__(self, args: WorstCaseAttackArgs=WorstCaseAttackArgs()):
     def __str__(self):
         return "WorstCase attack"
 
-    def attack(
-        self,
-        dataset: Data,
-        target_model: sklearn.base.BaseEstimator) -> None:
+    def attack(self, dataset: Data, target_model: sklearn.base.BaseEstimator) -> None:
         """Programmatic attack entry point
 
         To be used when code has access to data class and trained target model
@@ -94,10 +94,9 @@ def attack_from_prediction_files(self):
         test_preds = np.loadtxt(self.args.out_sample_filename, delimiter=",")
         self.attack_from_preds(train_preds, test_preds)
 
-    def attack_from_preds( # pylint: disable=too-many-locals
-        self,
-        train_preds: np.ndarray,
-        test_preds: np.ndarray) -> None:
+    def attack_from_preds(  # pylint: disable=too-many-locals
+        self, train_preds: np.ndarray, test_preds: np.ndarray
+    ) -> None:
         """
         Runs the attack based upon the predictions in train_preds and test_preds, and the params
         stored in self.args
@@ -121,16 +120,12 @@ def attack_from_preds( # pylint: disable=too-many-locals
             n_test_rows = len(test_preds)
             for _ in range(self.args.n_dummy_reps):
                 d_train_preds, d_test_preds = self.generate_arrays(
-                    n_train_rows,
-                    n_test_rows,
-                    self.args.train_beta,
-                    self.args.test_beta
+                    n_train_rows, n_test_rows, self.args.train_beta, self.args.test_beta
                 )
                 temp_metrics = self.run_attack_reps(d_train_preds, d_test_preds)
                 self.dummy_attack_metrics += temp_metrics
         logger.info("Finished running attacks")
 
-
     def run_attack_reps(self, train_preds: np.ndarray, test_preds: np.ndarray) -> list:
         """
         Run actual attack reps from train and test predictions
@@ -147,8 +142,8 @@ def run_attack_reps(self, train_preds: np.ndarray, test_preds: np.ndarray) -> li
         mia_metrics: List
             List of attack metrics dictionaries, one for each repetition
         """
-        self.args.set_param('n_rows_in', len(train_preds))
-        self.args.set_param('n_rows_out', len(test_preds))
+        self.args.set_param("n_rows_in", len(train_preds))
+        self.args.set_param("n_rows_out", len(test_preds))
         logger = logging.getLogger("attack-reps")
         logger.info("Sorting probabilities to leave highest value in first column")
         train_preds = -np.sort(-train_preds, axis=1)
@@ -156,12 +151,7 @@ def run_attack_reps(self, train_preds: np.ndarray, test_preds: np.ndarray) -> li
 
         logger.info("Creating MIA data")
         mi_x = np.vstack((train_preds, test_preds))
-        mi_y = np.hstack(
-            (
-                np.ones(len(train_preds)),
-                np.zeros(len(test_preds))
-            )
-        )
+        mi_y = np.hstack((np.ones(len(train_preds)), np.zeros(len(test_preds))))
 
         mia_metrics = []
         for rep in range(self.args.n_reps):
@@ -173,19 +163,13 @@ def run_attack_reps(self, train_preds: np.ndarray, test_preds: np.ndarray) -> li
             attack_classifier.fit(mi_train_x, mi_train_y)
 
             mia_metrics.append(
-                metrics.get_metrics(
-                    attack_classifier,
-                    mi_test_x,
-                    mi_test_y
-                )
+                metrics.get_metrics(attack_classifier, mi_test_x, mi_test_y)
             )
 
         logger.info("Finished simulating attacks")
 
         return mia_metrics
 
-
-
     def _get_global_metrics(self, attack_metrics: list) -> dict:
         """Summarise metrics from a metric list
 
@@ -203,51 +187,48 @@ def _get_global_metrics(self, attack_metrics: list) -> dict:
         global_metrics = {}
         auc_p_vals = [
             metrics.auc_p_val(
-                m['AUC'],
-                m['n_pos_test_examples'],
-                m['n_neg_test_examples']
-            )[0] for m in attack_metrics
+                m["AUC"], m["n_pos_test_examples"], m["n_neg_test_examples"]
+            )[0]
+            for m in attack_metrics
         ]
 
         m = attack_metrics[0]
         _, auc_std = metrics.auc_p_val(
-            0.5,
-            m['n_pos_test_examples'],
-            m['n_neg_test_examples']
+            0.5, m["n_pos_test_examples"], m["n_neg_test_examples"]
         )
 
-        global_metrics['null_auc_3sd_range'] = \
-            f"{0.5 - 3*auc_std:.4f} -> {0.5 + 3*auc_std:.4f}"
-        global_metrics['n_sig_auc_p_vals'] = self._get_n_significant(auc_p_vals, self.args.p_thresh)
-        global_metrics['n_sig_auc_p_vals_corrected'] = self._get_n_significant(
-            auc_p_vals,
-            self.args.p_thresh,
-            bh_fdr_correction=True
+        global_metrics[
+            "null_auc_3sd_range"
+        ] = f"{0.5 - 3*auc_std:.4f} -> {0.5 + 3*auc_std:.4f}"
+        global_metrics["n_sig_auc_p_vals"] = self._get_n_significant(
+            auc_p_vals, self.args.p_thresh
+        )
+        global_metrics["n_sig_auc_p_vals_corrected"] = self._get_n_significant(
+            auc_p_vals, self.args.p_thresh, bh_fdr_correction=True
         )
 
-        pdif_vals = [np.exp(-m['PDIF01']) for m in attack_metrics]
-        global_metrics['n_sig_pdif_vals'] = self._get_n_significant(pdif_vals, self.args.p_thresh)
-        global_metrics['n_sig_pdif_vals_corrected'] = self._get_n_significant(
-            pdif_vals,
-            self.args.p_thresh,
-            bh_fdr_correction=True
+        pdif_vals = [np.exp(-m["PDIF01"]) for m in attack_metrics]
+        global_metrics["n_sig_pdif_vals"] = self._get_n_significant(
+            pdif_vals, self.args.p_thresh
+        )
+        global_metrics["n_sig_pdif_vals_corrected"] = self._get_n_significant(
+            pdif_vals, self.args.p_thresh, bh_fdr_correction=True
         )
 
         return global_metrics
 
-
     def _get_n_significant(self, p_val_list, p_thresh, bh_fdr_correction=False):
-        '''
+        """
         Helper method to determine if values within a list of p-values are significant at
         p_thresh. Can perform multiple testing correction.
-        '''
+        """
         if not bh_fdr_correction:
-            return sum(1 for p in p_val_list if p <= p_thresh) # pylint: disable = consider-using-generator
+            return sum(
+                1 for p in p_val_list if p <= p_thresh
+            )  # pylint: disable = consider-using-generator
         p_val_list = np.asarray(sorted(p_val_list))
         n_vals = len(p_val_list)
-        hoch_vals = np.array(
-            [(k / n_vals) * P_THRESH for k in range(1, n_vals + 1)]
-        )
+        hoch_vals = np.array([(k / n_vals) * P_THRESH for k in range(1, n_vals + 1)])
         bh_sig_list = p_val_list <= hoch_vals
         if any(bh_sig_list):
             n_sig_bh = (np.where(bh_sig_list)[0]).max() + 1
@@ -255,7 +236,6 @@ def _get_n_significant(self, p_val_list, p_thresh, bh_fdr_correction=False):
             n_sig_bh = 0
         return n_sig_bh
 
-
     def _generate_array(self, n_rows: int, beta: float) -> np.ndarray:
         """Generate a single array of predictions, used when doing baseline experiments
 
@@ -291,8 +271,9 @@ def generate_arrays(
         self,
         n_rows_in: int,
         n_rows_out: int,
-        train_beta: float=2,
-        test_beta: float=2) -> tuple[np.ndarray, np.ndarray]:
+        train_beta: float = 2,
+        test_beta: float = 2,
+    ) -> tuple[np.ndarray, np.ndarray]:
         """Generate train and test prediction arrays, used when computing baseline
 
         Parameters
@@ -337,55 +318,54 @@ def make_dummy_data(self) -> None:
         logger.info(
             "Making dummy data with %d rows in and %d out",
             self.args.n_rows_in,
-            self.args.n_rows_out
+            self.args.n_rows_out,
         )
         logger.info("Generating rows")
         train_preds, test_preds = self.generate_arrays(
             self.args.n_rows_in,
             self.args.n_rows_out,
             train_beta=self.args.train_beta,
-            test_beta=self.args.test_beta
+            test_beta=self.args.test_beta,
         )
         logger.info("Saving files")
         np.savetxt(self.args.in_sample_filename, train_preds, delimiter=",")
         np.savetxt(self.args.out_sample_filename, test_preds, delimiter=",")
 
-
     def _construct_metadata(self):
         """Constructs the metadata object, after attacks"""
         self.metadata = {}
         # Store all args
-        self.metadata['experiment_details'] = {}
-        self.metadata['experiment_details'].update(self.args.__dict__)
-        if 'func' in self.metadata['experiment_details']:
-            del self.metadata['experiment_details']['func']
+        self.metadata["experiment_details"] = {}
+        self.metadata["experiment_details"].update(self.args.__dict__)
+        if "func" in self.metadata["experiment_details"]:
+            del self.metadata["experiment_details"]["func"]
 
-        self.metadata['attack'] = str(self)
+        self.metadata["attack"] = str(self)
 
         # Global metrics
-        self.metadata['global_metrics'] = self._get_global_metrics(self.attack_metrics)
-        self.metadata['baseline_global_metrics'] = self._get_global_metrics(
+        self.metadata["global_metrics"] = self._get_global_metrics(self.attack_metrics)
+        self.metadata["baseline_global_metrics"] = self._get_global_metrics(
             self.dummy_attack_metrics
         )
 
-
     def make_report(self) -> dict:
         """Creates output dictionary structure"""
         output = {}
-        output['attack_metrics'] = self.attack_metrics
-        output['dummy_attack_metrics'] = self.dummy_attack_metrics
+        output["attack_metrics"] = self.attack_metrics
+        output["dummy_attack_metrics"] = self.dummy_attack_metrics
         self._construct_metadata()
-        output['metadata'] = self.metadata
+        output["metadata"] = self.metadata
         if self.args.report_name is not None:
             json_report = report.create_json_report(output)
-            with open(f"{self.args.report_name}.json", 'w', encoding='utf-8') as f:
+            with open(f"{self.args.report_name}.json", "w", encoding="utf-8") as f:
                 f.write(json_report)
 
             pdf = report.create_mia_report(output)
-            pdf.output(f"{self.args.report_name}.pdf", 'F')
+            pdf.output(f"{self.args.report_name}.pdf", "F")
 
         return output
 
+
 def _make_dummy_data(args):
     """Initialise class and run dummy data creation"""
     wc_args = WorstCaseAttackArgs(**args.__dict__)
@@ -394,6 +374,7 @@ def _make_dummy_data(args):
     attack_obj = WorstCaseAttack(wc_args)
     attack_obj.make_dummy_data()
 
+
 def _run_attack(args):
     """Initialise class and run attack from prediction files"""
     wc_args = WorstCaseAttackArgs(**args.__dict__)
@@ -403,152 +384,161 @@ def _run_attack(args):
 
 
 def main():
-    '''main method to parse arguments and invoke relevant method'''
+    """main method to parse arguments and invoke relevant method"""
     logger = logging.getLogger("main")
-    parser = argparse.ArgumentParser(description=(
-        'Perform a worst case attack from saved model predictions'
-        )
+    parser = argparse.ArgumentParser(
+        description=("Perform a worst case attack from saved model predictions")
     )
 
     subparsers = parser.add_subparsers()
-    dummy_parser = subparsers.add_parser('make-dummy-data')
-    dummy_parser.add_argument('--num-rows-in',
-        action='store',
-        dest='n_rows_in',
+    dummy_parser = subparsers.add_parser("make-dummy-data")
+    dummy_parser.add_argument(
+        "--num-rows-in",
+        action="store",
+        dest="n_rows_in",
         type=int,
         required=False,
         default=1000,
-        help=(
-            'How many rows to generate in the in-sample file. Default = %(default)d'
-        )
+        help=("How many rows to generate in the in-sample file. Default = %(default)d"),
     )
 
-    dummy_parser.add_argument('--num-rows-out',
-        action='store',
-        dest='n_rows_out',
+    dummy_parser.add_argument(
+        "--num-rows-out",
+        action="store",
+        dest="n_rows_out",
         type=int,
         required=False,
         default=1000,
         help=(
-            'How many rows to generate in the out-of-sample file. Default = %(default)d'
-        )
+            "How many rows to generate in the out-of-sample file. Default = %(default)d"
+        ),
     )
 
-    dummy_parser.add_argument('--train-beta',
-        action='store',
+    dummy_parser.add_argument(
+        "--train-beta",
+        action="store",
         type=float,
         required=False,
         default=5,
         dest="train_beta",
         help=(
-            'Value of b parameter for beta distribution used to sample the in-sample '
-            'probabilities. '
-            'High values will give more extreme probabilities. Set this value higher than '
-            '--test-beta to see successful attacks. Default = %(default)f'
-        )
+            "Value of b parameter for beta distribution used to sample the in-sample "
+            "probabilities. "
+            "High values will give more extreme probabilities. Set this value higher than "
+            "--test-beta to see successful attacks. Default = %(default)f"
+        ),
     )
 
-    dummy_parser.add_argument('--test-beta',
-        action='store',
+    dummy_parser.add_argument(
+        "--test-beta",
+        action="store",
         type=float,
         required=False,
         default=2,
         dest="test_beta",
         help=(
-            'Value of b parameter for beta distribution used to sample the out-of-sample '
-            'probabilities. High values will give more extreme probabilities. Set this value '
-            'lower than --train-beta to see successful attacks. Default = %(default)f'
-        )
+            "Value of b parameter for beta distribution used to sample the out-of-sample "
+            "probabilities. High values will give more extreme probabilities. Set this value "
+            "lower than --train-beta to see successful attacks. Default = %(default)f"
+        ),
     )
 
     dummy_parser.set_defaults(func=_make_dummy_data)
 
-    attack_parser = subparsers.add_parser('run-attack')
-    attack_parser.add_argument('-i', '--in-sample-preds',
-        action='store',
-        dest='in_sample_filename',
+    attack_parser = subparsers.add_parser("run-attack")
+    attack_parser.add_argument(
+        "-i",
+        "--in-sample-preds",
+        action="store",
+        dest="in_sample_filename",
         required=False,
         type=str,
         default="train_preds.csv",
         help=(
-            'csv file containing the predictive probabilities (one column per class) for the '
-            'training data (one row per training example). Default = %(default)s'
-        )
+            "csv file containing the predictive probabilities (one column per class) for the "
+            "training data (one row per training example). Default = %(default)s"
+        ),
     )
 
-    attack_parser.add_argument('-o', '--out-of-sample-preds',
-        action='store',
-        dest='out_sample_filename',
+    attack_parser.add_argument(
+        "-o",
+        "--out-of-sample-preds",
+        action="store",
+        dest="out_sample_filename",
         required=False,
         type=str,
         default="test_preds.csv",
         help=(
-            'csv file containing the predictive probabilities (one column per class) for the '
-            'non-training data (one row per training example). Default = %(default)s'
-        )
+            "csv file containing the predictive probabilities (one column per class) for the "
+            "non-training data (one row per training example). Default = %(default)s"
+        ),
     )
 
-    attack_parser.add_argument('-r', '--n-reps',
+    attack_parser.add_argument(
+        "-r",
+        "--n-reps",
         type=int,
         required=False,
         default=5,
         action="store",
         dest="n_reps",
         help=(
-            'Number of repetitions (splitting data into attack model training and testing '
-            'paritions to perform. Default = %(default)d'
-        )
+            "Number of repetitions (splitting data into attack model training and testing "
+            "partitions to perform. Default = %(default)d"
+        ),
     )
 
-    attack_parser.add_argument('-t', '--test-prop',
+    attack_parser.add_argument(
+        "-t",
+        "--test-prop",
         type=float,
         required=False,
         default=0.3,
         action="store",
         dest="test_prop",
         help=(
-            'Proportion of examples to be used for testing when fiting the attack model. '
-            'Default = %(default)f'
-        )
+            "Proportion of examples to be used for testing when fiting the attack model. "
+            "Default = %(default)f"
+        ),
     )
 
-    attack_parser.add_argument('--report-name',
+    attack_parser.add_argument(
+        "--report-name",
         type=str,
-        action='store',
-        dest='report_name',
-        default='worstcase_report',
+        action="store",
+        dest="report_name",
+        default="worstcase_report",
         required=False,
         help=(
-            'Filename for the report output. Default = %(default)s. Code will append .pdf and .json'
-        )
+            "Filename for the report output. Default = %(default)s. Code will append .pdf and .json"
+        ),
     )
 
-    attack_parser.add_argument('--n-dummy-reps',
+    attack_parser.add_argument(
+        "--n-dummy-reps",
         type=int,
-        action='store',
-        dest='n_dummy_reps',
+        action="store",
+        dest="n_dummy_reps",
         default=1,
         required=False,
         help=(
-            'Number of dummy datasets to sample. Each will be assessed with --n-reps train and '
-            'test splits. Set to 0 to do no baseline calculations. Default = %(default)d'
-        )
+            "Number of dummy datasets to sample. Each will be assessed with --n-reps train and "
+            "test splits. Set to 0 to do no baseline calculations. Default = %(default)d"
+        ),
     )
 
-    attack_parser.add_argument('--p-thresh',
-        action='store',
+    attack_parser.add_argument(
+        "--p-thresh",
+        action="store",
         type=float,
         default=P_THRESH,
         required=False,
-        dest='p_thresh',
-        help=(
-            'P-value threshold for significance testing. Default = %(default)f'
-        )
+        dest="p_thresh",
+        help=("P-value threshold for significance testing. Default = %(default)f"),
     )
 
     attack_parser.set_defaults(func=_run_attack)
 
-
     args = parser.parse_args()
 
     try:
@@ -557,5 +547,6 @@ def main():
         logger.error("Invalid command. Try --help to get more details")
         logger.error(e)
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     main()
diff --git a/example_notebooks/example-notebook-SVC.ipynb b/example_notebooks/example-notebook-SVC.ipynb
index 9329a297..0daaa64b 100644
--- a/example_notebooks/example-notebook-SVC.ipynb
+++ b/example_notebooks/example-notebook-SVC.ipynb
@@ -40,7 +40,7 @@
     "ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(\"\")))\n",
     "sys.path.append(ROOT_DIR)\n",
     "home = expanduser(\"~\")\n",
-    "sys.path.append(os.path.abspath(home+'/AI-SDC'))\n",
+    "sys.path.append(os.path.abspath(home + \"/AI-SDC\"))\n",
     "logging.basicConfig()\n",
     "logger = logging.getLogger(\"wrapper_svm\")\n",
     "logger.setLevel(logging.INFO)\n",
@@ -66,7 +66,7 @@
    "source": [
     "cancer = datasets.load_breast_cancer()\n",
     "x = np.asarray(cancer.data, dtype=np.float64)\n",
-    "y = np.asarray(cancer.target, dtype=np.float64)\n"
+    "y = np.asarray(cancer.target, dtype=np.float64)"
    ]
   },
   {
diff --git a/example_notebooks/example-notebook-decisiontree.ipynb b/example_notebooks/example-notebook-decisiontree.ipynb
index 5e5cb053..7c751dd4 100644
--- a/example_notebooks/example-notebook-decisiontree.ipynb
+++ b/example_notebooks/example-notebook-decisiontree.ipynb
@@ -24,7 +24,7 @@
     "ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(\"\")))\n",
     "sys.path.append(ROOT_DIR)\n",
     "home = expanduser(\"~\")\n",
-    "sys.path.append(os.path.abspath(home+'/AI-SDC'))\n"
+    "sys.path.append(os.path.abspath(home + \"/AI-SDC\"))"
    ]
   },
   {
@@ -391,14 +391,12 @@
     }
    ],
    "source": [
-    "\n",
     "safeDTModel.min_samples_leaf = 9\n",
     "safeDTModel.min_samples_split = 1\n",
     "msg, disclosive = safeDTModel.posthoc_check()\n",
     "print(f\"disclosive currently {disclosive}\")\n",
     "if disclosive:\n",
-    "    print(\"Because \" + msg)\n",
-    "\n"
+    "    print(\"Because \" + msg)"
    ]
   },
   {
@@ -599,8 +597,7 @@
     "\n",
     "safeDTModel.save(name=\"testSave.pkl\")\n",
     "safeDTModel.preliminary_check()\n",
-    "safeDTModel.request_release(\"testSave.pkl\")\n",
-    "\n"
+    "safeDTModel.request_release(\"testSave.pkl\")"
    ]
   },
   {
diff --git a/example_notebooks/example-notebook-keras.ipynb b/example_notebooks/example-notebook-keras.ipynb
index 0bf467dc..d42f1f18 100644
--- a/example_notebooks/example-notebook-keras.ipynb
+++ b/example_notebooks/example-notebook-keras.ipynb
@@ -24,7 +24,7 @@
     "ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(\"\")))\n",
     "sys.path.append(ROOT_DIR)\n",
     "home = expanduser(\"~\")\n",
-    "sys.path.append(os.path.abspath(home+'/AI-SDC'))\n"
+    "sys.path.append(os.path.abspath(home + \"/AI-SDC\"))"
    ]
   },
   {
@@ -438,7 +438,9 @@
     "batch_size = safeModel.batch_size\n",
     "epochs = 20\n",
     "\n",
-    "dp_met, privacy = safeModel.dp_epsilon_met(num_examples=num_samples, batch_size=batch_size, epochs=epochs)\n",
+    "dp_met, privacy = safeModel.dp_epsilon_met(\n",
+    "    num_examples=num_samples, batch_size=batch_size, epochs=epochs\n",
+    ")\n",
     "\n",
     "print(f\"with these settings privacy = {privacy}\")"
    ]
@@ -464,9 +466,11 @@
     }
    ],
    "source": [
-    "dp_met, msg = safeModel.check_epsilon(X.shape[0], safeModel.batch_size, safeModel.epochs)\n",
-    "print(f'Satisfies DP: {dp_met}')\n",
-    "print(f'{msg}')"
+    "dp_met, msg = safeModel.check_epsilon(\n",
+    "    X.shape[0], safeModel.batch_size, safeModel.epochs\n",
+    ")\n",
+    "print(f\"Satisfies DP: {dp_met}\")\n",
+    "print(f\"{msg}\")"
    ]
   },
   {
diff --git a/example_notebooks/example-notebook-randomforest.ipynb b/example_notebooks/example-notebook-randomforest.ipynb
index d9cb9d47..7eded277 100644
--- a/example_notebooks/example-notebook-randomforest.ipynb
+++ b/example_notebooks/example-notebook-randomforest.ipynb
@@ -24,7 +24,7 @@
     "ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(\"\")))\n",
     "sys.path.append(ROOT_DIR)\n",
     "home = expanduser(\"~\")\n",
-    "sys.path.append(os.path.abspath(home+'/AI-SDC'))\n"
+    "sys.path.append(os.path.abspath(home + \"/AI-SDC\"))"
    ]
   },
   {
diff --git a/examples/attribute_inference_example.py b/examples/attribute_inference_example.py
index 5f44a047..03adff20 100644
--- a/examples/attribute_inference_example.py
+++ b/examples/attribute_inference_example.py
@@ -82,7 +82,9 @@
     print(f"Base model test accuracy: {acc_test}")
 
     # [TRE] Define some attack parameters
-    attack_args = attribute_attack.AttributeAttackArgs(n_cpu=7, report_name="aia_report")
+    attack_args = attribute_attack.AttributeAttackArgs(
+        n_cpu=7, report_name="aia_report"
+    )
 
     # [TRE] Create the attack object
     attack_obj = attribute_attack.AttributeAttack(attack_args)
@@ -91,7 +93,7 @@
     attack_obj.attack(data, model)
 
     # [TRE] Grab the output
-    output = attack_obj.make_report() # also makes .pdf and .json files
+    output = attack_obj.make_report()  # also makes .pdf and .json files
     output = output["attack_metrics"]
 
     # [TRE] explore the metrics
diff --git a/examples/lira_attack_example.py b/examples/lira_attack_example.py
index a04a847e..f4a93bcc 100644
--- a/examples/lira_attack_example.py
+++ b/examples/lira_attack_example.py
@@ -32,16 +32,19 @@
 """
 # pylint: disable = duplicate-code
 
-import os
 import json
+import os
 
 import numpy as np
-from sklearn.model_selection import train_test_split
-from sklearn.ensemble import RandomForestClassifier
 from sklearn.datasets import load_breast_cancer
+from sklearn.ensemble import RandomForestClassifier
+from sklearn.model_selection import train_test_split
 
-from attacks.likelihood_attack import LIRAAttackArgs, LIRAAttack # pylint: disable = import-error
-from attacks.dataset import Data # pylint: disable = import-error
+from attacks.dataset import Data  # pylint: disable = import-error
+from attacks.likelihood_attack import (  # pylint: disable = import-error
+    LIRAAttack,
+    LIRAAttackArgs,
+)
 
 # [Researcher] Access a dataset
 X, y = load_breast_cancer(return_X_y=True, as_frame=False)
@@ -66,11 +69,11 @@
 attack_obj.attack(dataset, target_model)
 
 # [TRE] Get the output
-output = attack_obj.make_report() # also makes .pdf and .json files
+output = attack_obj.make_report()  # also makes .pdf and .json files
 
 # [TRE] Accesses attack metrics and metadata
-attack_metrics = output['attack_metrics'][0]
-metadata = output['metadata']
+attack_metrics = output["attack_metrics"][0]
+metadata = output["metadata"]
 
 # [TRE] Looks at the metric values
 print("Attack metrics:")
@@ -81,7 +84,7 @@
         print(f"Cannot print {key}")
 
 print("Global metrics")
-for key, value in metadata['global_metrics'].items():
+for key, value in metadata["global_metrics"].items():
     print(key, value)
 
 
@@ -100,16 +103,8 @@
 np.savetxt("test_preds.csv", target_model.predict_proba(test_X), delimiter=",")
 
 # [Researcher] Dump the training and test data to a .csv file
-np.savetxt(
-    "train_data.csv",
-    np.hstack((train_X, train_y[:, None])),
-    delimiter=","
-)
-np.savetxt(
-    "test_data.csv",
-    np.hstack((test_X, test_y[:, None])),
-    delimiter=","
-)
+np.savetxt("train_data.csv", np.hstack((train_X, train_y[:, None])), delimiter=",")
+np.savetxt("test_data.csv", np.hstack((test_X, test_y[:, None])), delimiter=",")
 
 # [TRE] Creates a config file for the likelihood attack
 config = {
@@ -118,13 +113,10 @@
     "training_preds_file": "train_preds.csv",
     "testing_preds_file": "test_preds.csv",
     "target_model": ["sklearn.ensemble", "RandomForestClassifier"],
-    "target_hyppars": {
-        "min_samples_split": 2,
-        "min_samples_leaf": 1
-    }
+    "target_hyppars": {"min_samples_split": 2, "min_samples_leaf": 1},
 }
 
-with open('config.json', 'w', encoding='utf-8') as f:
+with open("config.json", "w", encoding="utf-8") as f:
     f.write(json.dumps(config))
 
 
@@ -134,12 +126,10 @@
 
 # [TRE] Then they run the attack
 os.system(
-    (
-        "python -m attacks.likelihood_attack run-attack "
-        "--json-file config.json "
-        "--report-name example_lira_report "
-        "--n-shadow-models 100"
-    )
+    "python -m attacks.likelihood_attack run-attack "
+    "--json-file config.json "
+    "--report-name example_lira_report "
+    "--n-shadow-models 100"
 )
 
 # [TRE] The code produces a .pdf report (example_lira_report.pdf)
diff --git a/examples/safemodel_attack_integration_bothcalls.py b/examples/safemodel_attack_integration_bothcalls.py
index b2c9f278..bc9ea45e 100644
--- a/examples/safemodel_attack_integration_bothcalls.py
+++ b/examples/safemodel_attack_integration_bothcalls.py
@@ -6,14 +6,12 @@
 import logging
 
 import numpy as np
-
 from sklearn.datasets import fetch_openml
 from sklearn.model_selection import train_test_split
 from sklearn.preprocessing import LabelEncoder, OneHotEncoder
 
-from safemodel.classifiers import SafeDecisionTreeClassifier
 from attacks.dataset import Data
-
+from safemodel.classifiers import SafeDecisionTreeClassifier
 
 if __name__ == "__main__":
 
@@ -61,34 +59,33 @@
     for i in range(n_features):
         the_data.add_feature(nursery_data.feature_names[i], indices[i], "onehot")
 
-    logging.info('Dataset: %s',the_data.name)
-    logging.info('Features: %s',the_data.features)
-    logging.info('x_train shape = %s',np.shape(the_data.x_train))
-    logging.info('y_train shape = %s',np.shape(the_data.y_train))
-    logging.info('x_test shape = %s',np.shape(the_data.x_test))
-    logging.info('y_test shape = %s',np.shape(the_data.y_test))
-
+    logging.info("Dataset: %s", the_data.name)
+    logging.info("Features: %s", the_data.features)
+    logging.info("x_train shape = %s", np.shape(the_data.x_train))
+    logging.info("y_train shape = %s", np.shape(the_data.y_train))
+    logging.info("x_test shape = %s", np.shape(the_data.x_test))
+    logging.info("y_test shape = %s", np.shape(the_data.y_test))
 
-    #build a model
+    # build a model
     model = SafeDecisionTreeClassifier(random_state=1)
     model.fit(x_train, y_train)
     msg, disclosive = model.preliminary_check()
 
     ##check direct method
-    print('==========> first running attacks explicitly via run_attack()')
-    for attack_name in ['worst_case','attribute','lira']:
-        print(f'===> running {attack_name} attack directly')
-        fname=f"modelDOTrun_attack_output_{attack_name}"
-        metadata= model.run_attack(the_data,attack_name,fname)
-        logging.info('metadata is:')
-        for key,val in metadata.items():
-            if  isinstance(val,dict):
-                logging.info(' %s ',key)
-                for key1,val2 in val.items():
-                    logging.info('  %s : %s', key1 , val2)
+    print("==========> first running attacks explicitly via run_attack()")
+    for attack_name in ["worst_case", "attribute", "lira"]:
+        print(f"===> running {attack_name} attack directly")
+        fname = f"modelDOTrun_attack_output_{attack_name}"
+        metadata = model.run_attack(the_data, attack_name, fname)
+        logging.info("metadata is:")
+        for key, val in metadata.items():
+            if isinstance(val, dict):
+                logging.info(" %s ", key)
+                for key1, val2 in val.items():
+                    logging.info("  %s : %s", key1, val2)
             else:
-                logging.info(' %s : %s',key ,val)
+                logging.info(" %s : %s", key, val)
 
     ## now via request_release()
-    print('===>now running attacks implicitly via request_release()')
-    model.request_release("test.sav",the_data)
+    print("===>now running attacks implicitly via request_release()")
+    model.request_release("test.sav", the_data)
diff --git a/examples/worst_case_attack_example.py b/examples/worst_case_attack_example.py
index 95aa1f72..43cdb617 100644
--- a/examples/worst_case_attack_example.py
+++ b/examples/worst_case_attack_example.py
@@ -23,11 +23,11 @@
 import os
 
 import numpy as np
+from sklearn.datasets import load_breast_cancer
 from sklearn.model_selection import train_test_split
 from sklearn.svm import SVC
-from sklearn.datasets import load_breast_cancer
 
-from attacks import worst_case_attack, dataset # pylint: disable = import-error
+from attacks import dataset, worst_case_attack  # pylint: disable = import-error
 
 # [Researcher] Access a dataset
 X, y = load_breast_cancer(return_X_y=True, as_frame=False)
@@ -62,7 +62,7 @@
     # Proportion of data to use as a test set for the attack model;
     test_prop=0.5,
     # Report name is None - don't make json or pdf files
-    report_name=None
+    report_name=None,
 )
 
 # [TRE / Researcher] Wrap the data in a dataset object
@@ -77,7 +77,7 @@
 
 # [TRE] Grab the output
 output = attack_obj.make_report()
-metadata = output['metadata']
+metadata = output["metadata"]
 # [TRE] explore the metrics
 # For how many of the reps is the AUC p-value significant, with and without FDR correction. A
 # significant P-value means that the attack was statistically successful at predicting rows at
@@ -85,23 +85,23 @@
 
 print(
     "Number of significant AUC values (raw):",
-    f"{metadata['global_metrics']['n_sig_auc_p_vals']}/{args.n_reps}"
+    f"{metadata['global_metrics']['n_sig_auc_p_vals']}/{args.n_reps}",
 )
 
 print(
     "Number of significant AUC values (FDR corrected):",
-    f"{metadata['global_metrics']['n_sig_auc_p_vals_corrected']}/{args.n_reps}"
+    f"{metadata['global_metrics']['n_sig_auc_p_vals_corrected']}/{args.n_reps}",
 )
 
 # Or the number of repetitions in which the PDIF (0.1) was significant
 print(
     "Number of significant PDIF values (proportion of 0.1), raw:",
-    f"{metadata['global_metrics']['n_sig_pdif_vals']}/{args.n_reps}"
+    f"{metadata['global_metrics']['n_sig_pdif_vals']}/{args.n_reps}",
 )
 
 print(
     "Number of significant PDIF values (proportion of 0.1), FDR corrected:",
-    f"{metadata['global_metrics']['n_sig_pdif_vals_corrected']}/{args.n_reps}"
+    f"{metadata['global_metrics']['n_sig_pdif_vals_corrected']}/{args.n_reps}",
 )
 
 # [TRE] to compare the results obtained with those expected by chance, the attack runs some
@@ -110,23 +110,23 @@
 # [TRE] looks at the metric values to compare with those for the model
 print(
     "(dummy) Number of significant AUC values (raw):",
-    f"{metadata['baseline_global_metrics']['n_sig_auc_p_vals']}/{args.n_reps}"
+    f"{metadata['baseline_global_metrics']['n_sig_auc_p_vals']}/{args.n_reps}",
 )
 
 print(
     "(dummy) Number of significant AUC values (FDR corrected):",
-    f"{metadata['baseline_global_metrics']['n_sig_auc_p_vals_corrected']}/{args.n_reps}"
+    f"{metadata['baseline_global_metrics']['n_sig_auc_p_vals_corrected']}/{args.n_reps}",
 )
 
 # Or the number of repetitions in which the PDIF (0.1) was significant
 print(
     "(dummy) Number of significant PDIF values (proportion of 0.1), raw:",
-    f"{metadata['baseline_global_metrics']['n_sig_pdif_vals']}/{args.n_reps}"
+    f"{metadata['baseline_global_metrics']['n_sig_pdif_vals']}/{args.n_reps}",
 )
 
 print(
     "(dummy) Number of significant PDIF values (proportion of 0.1), FDR corrected:",
-    f"{metadata['baseline_global_metrics']['n_sig_pdif_vals_corrected']}/{args.n_reps}"
+    f"{metadata['baseline_global_metrics']['n_sig_pdif_vals_corrected']}/{args.n_reps}",
 )
 
 print("Programmatic example finished")
@@ -149,16 +149,14 @@
 
 # [TRE] Then they run the attack
 os.system(
-    (
-        "python -m attacks.worst_case_attack run-attack "
-        "--in-sample-preds train_preds.csv "
-        "--out-of-sample-preds test_preds.csv "
-        "--n-reps 10 "
-        "--report-name example_report_risky "
-        "--n-dummy-reps 1 "
-        "--test-prop 0.1"
-        "--report-name example_report"
-    )
+    "python -m attacks.worst_case_attack run-attack "
+    "--in-sample-preds train_preds.csv "
+    "--out-of-sample-preds test_preds.csv "
+    "--n-reps 10 "
+    "--report-name example_report_risky "
+    "--n-dummy-reps 1 "
+    "--test-prop 0.1"
+    "--report-name example_report"
 )
 
 # [TRE] The code produces a .pdf report (example_report.pdf) and a .json file (example_report.json)
diff --git a/preprocessing/loaders.py b/preprocessing/loaders.py
index 0b7a163d..763439ed 100644
--- a/preprocessing/loaders.py
+++ b/preprocessing/loaders.py
@@ -1,31 +1,28 @@
-'''
+"""
 loaders.py
 A set of useful handlers to pull in datasets common to the project and perform the appropriate
 pre-processing
-'''
+"""
 
 # pylint: disable=import-error, invalid-name, consider-using-with, too-many-return-statements
 
-import os
 import logging
-from typing import Tuple, List
-from zipfile import ZipFile, BadZipFile
+import os
 from collections import Counter
-import pandas as pd
+from typing import List, Tuple
+from zipfile import BadZipFile, ZipFile
+
 import numpy as np
+import pandas as pd
 import pylab as plt
-from sklearn.preprocessing import OneHotEncoder, LabelEncoder
-from sklearn.datasets import fetch_openml
-from sklearn.datasets import load_iris
-
+from sklearn.datasets import fetch_openml, load_iris
+from sklearn.preprocessing import LabelEncoder, OneHotEncoder
 
 # Following is to stop pylint always sqwarking at pandas things
 # pylint: disable = no-member, unsubscriptable-object
 
 
-logging.basicConfig(
-    level = "DEBUG"
-)
+logging.basicConfig(level="DEBUG")
 
 logger = logging.getLogger(__file__)
 
@@ -35,18 +32,18 @@
 
 
 class UnknownDataset(Exception):
-    '''Exception raised if the user passes a name that we don't recognise
-    '''
+    """Exception raised if the user passes a name that we don't recognise"""
+
 
 class DataNotAvailable(Exception):
-    '''Exception raised if the user asks for a dataset that they do not have the data for. I.e.
+    """Exception raised if the user asks for a dataset that they do not have the data for. I.e.
     some datasets require a .csv file to have been downloaded.
-    '''
+    """
 
-def get_data_sklearn( # pylint: disable = too-many-branches
-    dataset_name: str,
-    data_folder: str = os.path.join(PROJECT_ROOT_FOLDER, "data")
-    ) -> Tuple[pd.DataFrame, pd.DataFrame]:
+
+def get_data_sklearn(  # pylint: disable = too-many-branches
+    dataset_name: str, data_folder: str = os.path.join(PROJECT_ROOT_FOLDER, "data")
+) -> Tuple[pd.DataFrame, pd.DataFrame]:
     """Main entry method to return data in format sensible for sklearn. User passes a name and that
     dataset is returned as a tuple of pandas DataFrames (data, labels).
 
@@ -121,69 +118,69 @@ def get_data_sklearn( # pylint: disable = too-many-branches
         sub_name = dataset_name.split("round")[1].strip()
         logger.debug(sub_name)
         feature_df, target_df = get_data_sklearn(sub_name)
-        column_dtype = feature_df.dtypes # pylint: disable = no-member
+        column_dtype = feature_df.dtypes  # pylint: disable = no-member
 
         for i, column in enumerate(feature_df.columns):
             if column_dtype[i] == "float64":
                 feature_df[column] = feature_df[column].round(decimals=3)
         return feature_df, target_df
 
-
-
-    if dataset_name == 'mimic2-iaccd':
+    if dataset_name == "mimic2-iaccd":
         return _mimic_iaccd(data_folder)
-    if dataset_name == 'in-hospital-mortality':
+    if dataset_name == "in-hospital-mortality":
         return _in_hospital_mortality(data_folder)
-    if dataset_name == 'medical-mnist-ab-v-br-100':
-        return _medical_mnist_loader(data_folder, 100, ['AbdomenCT', 'BreastMRI'])
-    if dataset_name == 'medical-mnist-ab-v-br-500':
-        return _medical_mnist_loader(data_folder, 500, ['AbdomenCT', 'BreastMRI'])
-    if dataset_name == 'medical-mnist-all-100':
+    if dataset_name == "medical-mnist-ab-v-br-100":
+        return _medical_mnist_loader(data_folder, 100, ["AbdomenCT", "BreastMRI"])
+    if dataset_name == "medical-mnist-ab-v-br-500":
+        return _medical_mnist_loader(data_folder, 500, ["AbdomenCT", "BreastMRI"])
+    if dataset_name == "medical-mnist-all-100":
         return _medical_mnist_loader(
             data_folder,
             100,
-            ['AbdomenCT', 'BreastMRI', 'CXR', 'ChestCT', 'Hand', 'HeadCT']
+            ["AbdomenCT", "BreastMRI", "CXR", "ChestCT", "Hand", "HeadCT"],
         )
-    if dataset_name == 'indian liver':
+    if dataset_name == "indian liver":
         return _indian_liver(data_folder)
-    if dataset_name == 'texas hospitals 10':
+    if dataset_name == "texas hospitals 10":
         return _texas_hospitals(data_folder)
-    if dataset_name == 'synth-ae':
+    if dataset_name == "synth-ae":
         return _synth_ae(data_folder)
-    if dataset_name == 'synth-ae-small':
+    if dataset_name == "synth-ae-small":
         x, y = _synth_ae(data_folder, 200)
         return x, y
-    if dataset_name == 'nursery':
+    if dataset_name == "nursery":
         return _nursery()
-    if dataset_name == 'iris':
+    if dataset_name == "iris":
         return _iris()
     raise UnknownDataset(dataset_name)
 
 
 def _iris() -> Tuple[pd.DataFrame, pd.DataFrame]:
-    '''
+    """
     sklearn iris data - just first two classes
-    '''
+    """
     X, y = load_iris(return_X_y=True, as_frame=True)
     X = X[y < 2]
     y = y[y < 2]
     return X, pd.DataFrame(y)
 
+
 def _nursery() -> Tuple[pd.DataFrame, pd.DataFrame]:
-    '''
+    """
     The sklearn nursery dataset
-    '''
+    """
 
     data = fetch_openml(data_id=26, as_frame=True)
 
     target_encoder = LabelEncoder()
-    target_vals = target_encoder.fit_transform(data['target'].values)
-    target_dataframe = pd.DataFrame({'target': target_vals})
+    target_vals = target_encoder.fit_transform(data["target"].values)
+    target_dataframe = pd.DataFrame({"target": target_vals})
 
     feature_encoder = OneHotEncoder()
-    x_encoded = feature_encoder.fit_transform(data['data']).toarray()
-    feature_dataframe = pd.DataFrame(x_encoded, columns=feature_encoder.get_feature_names_out())
-
+    x_encoded = feature_encoder.fit_transform(data["data"]).toarray()
+    feature_dataframe = pd.DataFrame(
+        x_encoded, columns=feature_encoder.get_feature_names_out()
+    )
 
     return feature_dataframe, target_dataframe
 
@@ -191,44 +188,40 @@ def _nursery() -> Tuple[pd.DataFrame, pd.DataFrame]:
 # Patched to support non-flattened images. Same behaviour as before except if called with
 # flatten=False explicitly.
 def _images_to_ndarray(
-    images_dir: str,
-    number_to_load: int,
-    label: int,
-    flatten: bool = True) -> Tuple[np.array, np.array]:
-    '''
+    images_dir: str, number_to_load: int, label: int, flatten: bool = True
+) -> Tuple[np.array, np.array]:
+    """
     Grab number_to_load images from the images_dir and create a np array and label array
-    '''
+    """
     folder_path = images_dir + os.sep
     images_names = sorted(os.listdir(folder_path))
     images_names = images_names[:number_to_load]
     if flatten:
-        np_images = np.array([plt.imread(folder_path + img).flatten() for img in images_names])
+        np_images = np.array(
+            [plt.imread(folder_path + img).flatten() for img in images_names]
+        )
     else:
         np_images = np.array([plt.imread(folder_path + img) for img in images_names])
     labels = np.ones((len(np_images), 1), int) * label
-    return(np_images, labels)
+    return (np_images, labels)
 
-def _medical_mnist_loader( #pylint: disable = too-many-locals
-    data_folder: str,
-    n_per_class: int,
-    classes: List[str]) -> Tuple[pd.DataFrame, pd.DataFrame]:
-    '''
+
+def _medical_mnist_loader(  # pylint: disable = too-many-locals
+    data_folder: str, n_per_class: int, classes: List[str]
+) -> Tuple[pd.DataFrame, pd.DataFrame]:
+    """
     Load Medical MNIST into pandas format
     borrows heavily from: https://www.kaggle.com/harelshattenstein/medical-mnist-knn
     Creates a binary classification
-    '''
+    """
 
     base_folder = os.path.join(
         data_folder,
-        'kaggle-medical-mnist',
-        'archive',
+        "kaggle-medical-mnist",
+        "archive",
     )
 
-    zip_file = os.path.join(
-        data_folder,
-        'kaggle-medical-mnist',
-        "archive.zip"
-    )
+    zip_file = os.path.join(data_folder, "kaggle-medical-mnist", "archive.zip")
 
     logger.debug(base_folder, data_folder)
     if not any([os.path.exists(base_folder), os.path.exists(zip_file)]):
@@ -251,12 +244,12 @@ def _medical_mnist_loader( #pylint: disable = too-many-locals
             raise
 
     labels_dict = {
-        0 : 'AbdomenCT',
-        1 : 'BreastMRI',
-        2 : 'CXR',
-        3 : 'ChestCT',
-        4 : 'Hand',
-        5 : 'HeadCT'
+        0: "AbdomenCT",
+        1: "BreastMRI",
+        2: "CXR",
+        3: "ChestCT",
+        4: "Hand",
+        5: "HeadCT",
     }
 
     reverse_labels_dict = {v: k for k, v in labels_dict.items()}
@@ -264,9 +257,7 @@ def _medical_mnist_loader( #pylint: disable = too-many-locals
     for i, class_name in enumerate(classes):
         label = reverse_labels_dict[class_name]
         x_images, y_images = _images_to_ndarray(
-            os.path.join(base_folder, class_name),
-            n_per_class,
-            label
+            os.path.join(base_folder, class_name), n_per_class, label
         )
 
         if i == 0:
@@ -279,15 +270,16 @@ def _medical_mnist_loader( #pylint: disable = too-many-locals
     return (pd.DataFrame(all_x), pd.DataFrame(all_y))
 
 
-def _synth_ae(data_folder: str, n_rows: int=5000) -> Tuple[pd.DataFrame, pd.DataFrame]:
-    '''
+def _synth_ae(
+    data_folder: str, n_rows: int = 5000
+) -> Tuple[pd.DataFrame, pd.DataFrame]:
+    """
     First norws (default 5000) rows from the Synthetic A&E data from NHS England
     https://data.england.nhs.uk/dataset/a-e-synthetic-data/resource/81b068e5-6501-4840-a880-a8e7aa56890e # pylint: disable=line-too-long
-    '''
+    """
 
     file_path = os.path.join(
-        data_folder,
-        'AE_England_synthetic.csv' #'A&E Synthetic Data.csv'
+        data_folder, "AE_England_synthetic.csv"  #'A&E Synthetic Data.csv'
     )
 
     if not os.path.exists(file_path):
@@ -298,11 +290,15 @@ def _synth_ae(data_folder: str, n_rows: int=5000) -> Tuple[pd.DataFrame, pd.Data
     """
         raise DataNotAvailable(help_message)
 
-    input_data = pd.read_csv(file_path, nrows = n_rows)
+    input_data = pd.read_csv(file_path, nrows=n_rows)
     columns_to_drop = [
-        'AE_Arrive_Date', 'AE_Arrive_HourOfDay', 'Admission_Method',
-        'ICD10_Chapter_Code', 'Treatment_Function_Code', 'Length_Of_Stay_Days',
-        'ProvID'
+        "AE_Arrive_Date",
+        "AE_Arrive_HourOfDay",
+        "Admission_Method",
+        "ICD10_Chapter_Code",
+        "Treatment_Function_Code",
+        "Length_Of_Stay_Days",
+        "ProvID",
     ]
     input_data.drop(columns_to_drop, axis=1, inplace=True)
 
@@ -310,7 +306,7 @@ def _synth_ae(data_folder: str, n_rows: int=5000) -> Tuple[pd.DataFrame, pd.Data
     input_data.dropna(axis=0, inplace=True)
 
     # One-hot encode some columns
-    encode_columns = ['Age_Band', 'AE_HRG']
+    encode_columns = ["Age_Band", "AE_HRG"]
     encode_data = input_data[encode_columns]
     input_data.drop(encode_columns, axis=1, inplace=True)
 
@@ -319,27 +315,24 @@ def _synth_ae(data_folder: str, n_rows: int=5000) -> Tuple[pd.DataFrame, pd.Data
     onehot_df = pd.DataFrame(
         oh.transform(encode_data).toarray(),
         columns=oh.get_feature_names_out(),
-        index=input_data.index
+        index=input_data.index,
     )
 
     input_data = pd.concat([input_data, onehot_df], axis=1)
 
-    X = input_data.drop(['Admitted_Flag'], axis=1)
-    y = input_data[['Admitted_Flag']]
+    X = input_data.drop(["Admitted_Flag"], axis=1)
+    y = input_data[["Admitted_Flag"]]
 
     return (X, y)
 
 
 def _indian_liver(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
-    '''
+    """
     Indian Liver Patient Dataset
     https://archive.ics.uci.edu/ml/machine-learning-databases/00225/Indian%20Liver%20Patient%20Dataset%20(ILPD).csv # pylint: disable=line-too-long
-    '''
-    #(https://archive.ics.uci.edu/ml/datasets/ILPD+(Indian+Liver+Patient+Dataset)
-    file_path = os.path.join(
-        data_folder,
-        "Indian Liver Patient Dataset (ILPD).csv"
-    )
+    """
+    # (https://archive.ics.uci.edu/ml/datasets/ILPD+(Indian+Liver+Patient+Dataset)
+    file_path = os.path.join(data_folder, "Indian Liver Patient Dataset (ILPD).csv")
     if not os.path.exists(file_path):
         help_message = f"""
 Data file {file_path} does not exist. Please download fhe file from:
@@ -348,38 +341,49 @@ def _indian_liver(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
         """
         raise DataNotAvailable(help_message)
 
-    column_names= [
-        'age', 'gender', 'total Bilirubin', 'direct Bilirubin', 'Alkphos',
-        'SGPT', 'SGOT', 'total proteins', 'albumin', 'A/G ratio',  'class'
+    column_names = [
+        "age",
+        "gender",
+        "total Bilirubin",
+        "direct Bilirubin",
+        "Alkphos",
+        "SGPT",
+        "SGOT",
+        "total proteins",
+        "albumin",
+        "A/G ratio",
+        "class",
     ]
 
     liver_data = pd.read_csv(file_path, names=column_names, index_col=False)
 
-    liver_data.gender.replace('Male', 0, inplace=True)
-    liver_data.gender.replace('Female', 1, inplace=True)
+    liver_data.gender.replace("Male", 0, inplace=True)
+    liver_data.gender.replace("Female", 1, inplace=True)
 
     liver_data.dropna(axis=0, inplace=True)
 
-    liver_labels = liver_data['class']
-    liver_data.drop(['class'], axis=1, inplace=True)
+    liver_labels = liver_data["class"]
+    liver_data.drop(["class"], axis=1, inplace=True)
 
     label_encoder = LabelEncoder()
     encoded_labels = label_encoder.fit_transform(liver_labels.values)
-    liver_labels = pd.DataFrame({'class': encoded_labels})
+    liver_labels = pd.DataFrame({"class": encoded_labels})
     return (liver_data, liver_labels)
 
 
 def _in_hospital_mortality(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
-    '''
+    """
     In-hospital mortality data from this study:
         https://datadryad.org/stash/dataset/doi:10.5061/dryad.0p2ngf1zd
-    '''
+    """
     # Check the data has been downloaded. If not throw an exception with instructions on how to
     # download, and where to store
     files = ["data01.csv", "doi_10.5061_dryad.0p2ngf1zd__v5.zip"]
     file_path = [os.path.join(data_folder, f) for f in files]
 
-    if not any([os.path.exists(fp) for fp in file_path]): # pylint: disable=use-a-generator
+    if not any(
+        [os.path.exists(fp) for fp in file_path]
+    ):  # pylint: disable=use-a-generator
         help_message = f"""
 Data file {file_path[0]} or {file_path[1]} does not exist. Please download the file from:
 https://datadryad.org/stash/dataset/doi:10.5061/dryad.0p2ngf1zd
@@ -391,33 +395,26 @@ def _in_hospital_mortality(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame
         input_data = pd.read_csv(ZipFile(file_path[1]).open("data01.csv"))
     else:
         input_data = pd.read_csv(file_path[0])
-    clean_data = input_data.dropna(axis=0, how='any').drop(columns=["group", "ID"])
-    target = 'outcome'
+    clean_data = input_data.dropna(axis=0, how="any").drop(columns=["group", "ID"])
+    target = "outcome"
     labels = clean_data[target]
     features = clean_data.drop([target], axis=1)
 
     label_encoder = LabelEncoder()
     encoded_labels = label_encoder.fit_transform(labels.values)
-    labels = pd.DataFrame({'outcome': encoded_labels})
+    labels = pd.DataFrame({"outcome": encoded_labels})
 
     return (features, labels)
 
 
-
-
 def _mimic_iaccd(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
-    '''
+    """
     Loads the mimic_iaccd data and performs Alba's pre-processing
-    '''
+    """
 
     # Check the data has been downloaded. If not throw an exception with instructions on how to
     # download, and where to store
-    file_path = os.path.join(
-        data_folder,
-        "mimic2-iaccd",
-        "1.0",
-        "full_cohort_data.csv"
-    )
+    file_path = os.path.join(data_folder, "mimic2-iaccd", "1.0", "full_cohort_data.csv")
 
     if not os.path.exists(file_path):
         help_message = f"""
@@ -433,52 +430,63 @@ def _mimic_iaccd(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
 
     logger.info("Preprocessing")
     # remove columns non-numerical and repetitive or uninformative data for the analysis
-    col = ['service_unit', 'day_icu_intime', 'hosp_exp_flg','icu_exp_flg', 'day_28_flg']
+    col = [
+        "service_unit",
+        "day_icu_intime",
+        "hosp_exp_flg",
+        "icu_exp_flg",
+        "day_28_flg",
+    ]
     # service_num is the numerical version of service_unit
     # day_icu_intime_num is the numerical version of day_icu_intime
     # the other columns are to do with death and are somewhat repetitive with censor_flg
-    input_data.drop(col, axis = 1, inplace=True)
+    input_data.drop(col, axis=1, inplace=True)
     # drop columns with only 1 value
-    input_data.drop('sepsis_flg', axis=1, inplace=True)
+    input_data.drop("sepsis_flg", axis=1, inplace=True)
     # drop NA by row
     input_data.dropna(axis=0, inplace=True)
 
     # extract target
-    target = 'censor_flg'
+    target = "censor_flg"
     y = input_data[target]
-    X = input_data.drop([target, 'mort_day_censored'], axis=1)
+    X = input_data.drop([target, "mort_day_censored"], axis=1)
 
     label_encoder = LabelEncoder()
     encoded_labels = label_encoder.fit_transform(y.values)
-    y = pd.DataFrame({'censor_flag': encoded_labels})
-
+    y = pd.DataFrame({"censor_flag": encoded_labels})
 
     return (X, y)
 
-def _texas_hospitals(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame]: # pylint: disable=too-many-statements, too-many-locals
-    '''
+
+def _texas_hospitals(
+    data_folder: str,
+) -> Tuple[
+    pd.DataFrame, pd.DataFrame
+]:  # pylint: disable=too-many-statements, too-many-locals
+    """
     Texas Hospitals Dataset
     (https://www.dshs.texas.gov/THCIC/Hospitals/Download.shtm)
-    '''
-    file_list = ["PUDF-1Q2006-tab-delimited.zip",
-                 "PUDF-1Q2007-tab-delimited.zip",
-                 "PUDF-1Q2008-tab-delimited.zip",
-                 "PUDF-1Q2009-tab-delimited.zip",
-                 "PUDF-2Q2006-tab-delimited.zip",
-                 "PUDF-2Q2007-tab-delimited.zip",
-                 "PUDF-2Q2008-tab-delimited.zip",
-                 "PUDF-2Q2009-tab-delimited.zip",
-                 "PUDF-3Q2006-tab-delimited.zip",
-                 "PUDF-3Q2007-tab-delimited.zip",
-                 "PUDF-3Q2008-tab-delimited.zip",
-                 "PUDF-3Q2009-tab-delimited.zip",
-                 "PUDF-4Q2006-tab-delimited.zip",
-                 "PUDF-4Q2007-tab-delimited.zip",
-                 "PUDF-4Q2008-tab-delimited.zip",
-                 "PUDF-4Q2009-tab-delimited.zip"]
-
-    files_path = [os.path.join(
-        data_folder,"TexasHospitals",f) for f in file_list]
+    """
+    file_list = [
+        "PUDF-1Q2006-tab-delimited.zip",
+        "PUDF-1Q2007-tab-delimited.zip",
+        "PUDF-1Q2008-tab-delimited.zip",
+        "PUDF-1Q2009-tab-delimited.zip",
+        "PUDF-2Q2006-tab-delimited.zip",
+        "PUDF-2Q2007-tab-delimited.zip",
+        "PUDF-2Q2008-tab-delimited.zip",
+        "PUDF-2Q2009-tab-delimited.zip",
+        "PUDF-3Q2006-tab-delimited.zip",
+        "PUDF-3Q2007-tab-delimited.zip",
+        "PUDF-3Q2008-tab-delimited.zip",
+        "PUDF-3Q2009-tab-delimited.zip",
+        "PUDF-4Q2006-tab-delimited.zip",
+        "PUDF-4Q2007-tab-delimited.zip",
+        "PUDF-4Q2008-tab-delimited.zip",
+        "PUDF-4Q2009-tab-delimited.zip",
+    ]
+
+    files_path = [os.path.join(data_folder, "TexasHospitals", f) for f in file_list]
 
     found = [os.path.exists(file_path) for file_path in files_path]
     not_found = [file_path for file_path in files_path if not os.path.exists(file_path)]
@@ -496,114 +504,134 @@ def _texas_hospitals(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame]: # p
         """
         raise DataNotAvailable(help_message)
 
-    if not os.path.exists(os.path.join(data_folder,"TexasHospitals", processed_data_file)):
+    if not os.path.exists(
+        os.path.join(data_folder, "TexasHospitals", processed_data_file)
+    ):
 
         logger.info("Processing Texas Hospitals data (2006-2009)")
 
-        #Load data
+        # Load data
         columns_names = [
-            'THCIC_ID',# Provider ID. Unique identifier assigned to the provider by DSHS.
+            "THCIC_ID",  # Provider ID. Unique identifier assigned to the provider by DSHS.
             # Hospitals with fewer than 50 discharges have been aggregated into the
             # Provider ID '999999'
-            'DISCHARGE_QTR', #yyyyQm
-            'TYPE_OF_ADMISSION',
-            'SOURCE_OF_ADMISSION',
-            'PAT_ZIP',#Patient’s five-digit ZIP code
-            'PUBLIC_HEALTH_REGION', #Public Health Region of patient’s address
-            'PAT_STATUS', #Code indicating patient status as of the ending date of service for
+            "DISCHARGE_QTR",  # yyyyQm
+            "TYPE_OF_ADMISSION",
+            "SOURCE_OF_ADMISSION",
+            "PAT_ZIP",  # Patient’s five-digit ZIP code
+            "PUBLIC_HEALTH_REGION",  # Public Health Region of patient’s address
+            "PAT_STATUS",  # Code indicating patient status as of the ending date of service for
             # the period of care reported
-            'SEX_CODE',
-            'RACE',
-            'ETHNICITY',
-            'LENGTH_OF_STAY',
-            'PAT_AGE', #Code indicating age of patient in days or years on date of discharge.
-            'PRINC_DIAG_CODE', #diagnosis code for the principal diagnosis
-            'E_CODE_1', #external cause of injury
-            'PRINC_SURG_PROC_CODE', #Code for the principal surgical or other procedure performed
+            "SEX_CODE",
+            "RACE",
+            "ETHNICITY",
+            "LENGTH_OF_STAY",
+            "PAT_AGE",  # Code indicating age of patient in days or years on date of discharge.
+            "PRINC_DIAG_CODE",  # diagnosis code for the principal diagnosis
+            "E_CODE_1",  # external cause of injury
+            "PRINC_SURG_PROC_CODE",  # Code for the principal surgical or other procedure performed
             # during the period covered by the bill
-            'RISK_MORTALITY', #Assignment of a risk of mortality score from the All Patient
+            "RISK_MORTALITY",  # Assignment of a risk of mortality score from the All Patient
             # Refined (APR) Diagnosis Related Group (DRG)
-            'ILLNESS_SEVERITY',#Assignment of a severity of illness score from the All Patient
+            "ILLNESS_SEVERITY",  # Assignment of a severity of illness score from the All Patient
             # Refined (APR) Diagnosis RelatedGroup (DRG
-            'RECORD_ID'
+            "RECORD_ID",
         ]
         # obtain the 100 most frequent procedures
         tmp = []
         for f in files_path:
             df = [
-                pd.read_csv(ZipFile(f).open(i), sep="\t", usecols=["PRINC_SURG_PROC_CODE"])\
-                     for i in ZipFile(f).namelist() if 'base' in i
+                pd.read_csv(
+                    ZipFile(f).open(i), sep="\t", usecols=["PRINC_SURG_PROC_CODE"]
+                )
+                for i in ZipFile(f).namelist()
+                if "base" in i
             ][0]
             df.dropna(inplace=True)
             tmp.extend(list(df.PRINC_SURG_PROC_CODE))
-        princ_surg_proc_keep = [k for k,v in Counter(tmp).most_common(10)]
-        #remove unnecessary variables
+        princ_surg_proc_keep = [k for k, v in Counter(tmp).most_common(10)]
+        # remove unnecessary variables
         del tmp
 
         # Load the data
         tx_data = pd.DataFrame()
         for f in files_path:
-            df = [pd.read_csv(ZipFile(f).open(i), sep="\t", usecols=columns_names)
-                  for i in ZipFile(f).namelist() if 'base' in i][0]
-            #keep only those rows with one of the 10 most common principal surgical procedure
+            df = [
+                pd.read_csv(ZipFile(f).open(i), sep="\t", usecols=columns_names)
+                for i in ZipFile(f).namelist()
+                if "base" in i
+            ][0]
+            # keep only those rows with one of the 10 most common principal surgical procedure
             df = df[df["PRINC_SURG_PROC_CODE"].isin(princ_surg_proc_keep)]
-            #clean up data
+            # clean up data
             df.dropna(inplace=True)
-            df.replace('`', pd.NA, inplace=True)
-            df.replace('*', pd.NA, inplace=True)
-            #replace sex to numeric
-            df.SEX_CODE.replace('M',0,inplace=True)
-            df.SEX_CODE.replace('F',1,inplace=True)
-            df.SEX_CODE.replace('U',2,inplace=True)
-            #set to numerical variable
+            df.replace("`", pd.NA, inplace=True)
+            df.replace("*", pd.NA, inplace=True)
+            # replace sex to numeric
+            df.SEX_CODE.replace("M", 0, inplace=True)
+            df.SEX_CODE.replace("F", 1, inplace=True)
+            df.SEX_CODE.replace("U", 2, inplace=True)
+            # set to numerical variable
             for d_code in set(list(df.DISCHARGE_QTR)):
-                df.DISCHARGE_QTR.replace(d_code, ''.join(d_code.split('Q')), inplace=True)
+                df.DISCHARGE_QTR.replace(
+                    d_code, "".join(d_code.split("Q")), inplace=True
+                )
             df.dropna(inplace=True)
-            #merge data
+            # merge data
             tx_data = pd.concat([tx_data, df])
-        #remove uncessary variables
+        # remove uncessary variables
         del df
 
-        #Risk moratality, make it binary
-        #1 Minor
-        #2 Moderate
-        #3 Major
-        #4 Extreme
+        # Risk moratality, make it binary
+        # 1 Minor
+        # 2 Moderate
+        # 3 Major
+        # 4 Extreme
         tx_data.RISK_MORTALITY.astype(int)
-        tx_data.RISK_MORTALITY.replace(1,0,inplace=True)
-        tx_data.RISK_MORTALITY.replace(2,0,inplace=True)
-        tx_data.RISK_MORTALITY.replace(3,1,inplace=True)
-        tx_data.RISK_MORTALITY.replace(4,1,inplace=True)
+        tx_data.RISK_MORTALITY.replace(1, 0, inplace=True)
+        tx_data.RISK_MORTALITY.replace(2, 0, inplace=True)
+        tx_data.RISK_MORTALITY.replace(3, 1, inplace=True)
+        tx_data.RISK_MORTALITY.replace(4, 1, inplace=True)
 
-        #renumber non-numerical codes for cols
-        cols=['PRINC_DIAG_CODE', 'SOURCE_OF_ADMISSION', 'E_CODE_1']
+        # renumber non-numerical codes for cols
+        cols = ["PRINC_DIAG_CODE", "SOURCE_OF_ADMISSION", "E_CODE_1"]
         for col in cols:
             tmp = list(
-                set([x for x in tx_data[col] if not str(x).isdigit() and not isinstance(x, float)]) # pylint: disable=consider-using-set-comprehension
+                {
+                    x
+                    for x in tx_data[col]
+                    if not str(x).isdigit() and not isinstance(x, float)
+                }  # pylint: disable=consider-using-set-comprehension
             )
             n = max(
                 list(
-                    set([int(x) for x in tx_data[col] if str(x).isdigit() or isinstance(x, float)]) # pylint: disable=consider-using-set-comprehension
+                    {
+                        int(x)
+                        for x in tx_data[col]
+                        if str(x).isdigit() or isinstance(x, float)
+                    }  # pylint: disable=consider-using-set-comprehension
                 )
             )
             for i, x in enumerate(tmp):
                 tx_data[col].replace(x, n + i, inplace=True)
         del tmp, n
-        #set index
-        tx_data.set_index('RECORD_ID', inplace=True)
-        #final check and drop of NAs
+        # set index
+        tx_data.set_index("RECORD_ID", inplace=True)
+        # final check and drop of NAs
         tx_data.dropna(inplace=True)
-        #convert all data to numerical
+        # convert all data to numerical
         tx_data = tx_data.astype(int)
-        #save csv file
+        # save csv file
         tx_data.to_csv(os.path.join(data_folder, "TexasHospitals", processed_data_file))
     else:
         logger.info("Loading processed Texas Hospitals data (2006-2009) csv file.")
-        #load texas data processed csv file
-        tx_data = pd.read_csv(os.path.join(data_folder, "TexasHospitals", processed_data_file))
+        # load texas data processed csv file
+        tx_data = pd.read_csv(
+            os.path.join(data_folder, "TexasHospitals", processed_data_file)
+        )
 
     # extract target
-    var = 'RISK_MORTALITY'
+    var = "RISK_MORTALITY"
     labels = tx_data[var]
     # Drop the column that contains the labels
     tx_data.drop([var], axis=1, inplace=True)
@@ -612,5 +640,4 @@ def _texas_hospitals(data_folder: str) -> Tuple[pd.DataFrame, pd.DataFrame]: # p
     encoded_labels = label_encoder.fit_transform(labels.values)
     labels = pd.DataFrame({var: encoded_labels})
 
-
-    return(tx_data, labels)
+    return (tx_data, labels)
diff --git a/risk_examples/python/attribute_inference_cancer.ipynb b/risk_examples/python/attribute_inference_cancer.ipynb
index 73244509..403d20b6 100644
--- a/risk_examples/python/attribute_inference_cancer.ipynb
+++ b/risk_examples/python/attribute_inference_cancer.ipynb
@@ -82,40 +82,54 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "#1 is cancer, 0 is no cancer, this is our label and what we want to predict.\n",
-    "cancer = [1]*99 + [0]*100\n",
+    "# 1 is cancer, 0 is no cancer, this is our label and what we want to predict.\n",
+    "cancer = [1] * 99 + [0] * 100\n",
     "\n",
     "df = pd.DataFrame()\n",
     "\n",
-    "#diabetes 0 no, 1 yes\n",
-    "df['diabetes'] = [[1, 0][random.random()>0.7] for n in range(99)] +[[1, 0][random.random()>0.2] for n in range(100)]\n",
-    "\n",
-    "#asthma 0 no, 1 yes\n",
-    "df['asthma'] = [[1, 0][random.random()>0.7] for n in range(99)] + [[1, 0][random.random()>0.5] for n in range(100)]\n",
-    "\n",
-    "#bmi group 1 under, 2 normal, 3 overweight, 4 obese\n",
-    "df['bmi_group'] = [random.choices([1, 2, 3, 4], weights = [0.5, 5, 7, 5], k = 1)[0] for n in range(99)] + [random.choices([1, 2, 3, 4], weights = [1, 7, 4, 1], k = 1)[0] for n in range(100)]\n",
-    "\n",
-    "#blood pressure 0 is low, 1 is normal, 5 is extremly high\n",
-    "df['blood_pressure'] = [random.choices([0, 1, 2, 3, 4, 5], weights = [0.5, 1, 5, 6, 1, 0.5], k = 1)[0] for n in range(99)] + [random.choices([0, 1, 2, 3, 4, 5], weights = [0.5, 5, 5, 1, 1, 0.5], k = 1)[0] for n in range(100)]\n",
-    "\n",
-    "#smoker 0 is non smoker, 1 is smoker\n",
-    "df['smoker'] = [[1, 0][random.random()>0.8] for n in range(99)] + [[1, 0][random.random()>0.2] for n in range(100)]\n",
-    "\n",
-    "#age\n",
-    "x = np.arange(20,90)\n",
+    "# diabetes 0 no, 1 yes\n",
+    "df[\"diabetes\"] = [[1, 0][random.random() > 0.7] for n in range(99)] + [\n",
+    "    [1, 0][random.random() > 0.2] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# asthma 0 no, 1 yes\n",
+    "df[\"asthma\"] = [[1, 0][random.random() > 0.7] for n in range(99)] + [\n",
+    "    [1, 0][random.random() > 0.5] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# bmi group 1 under, 2 normal, 3 overweight, 4 obese\n",
+    "df[\"bmi_group\"] = [\n",
+    "    random.choices([1, 2, 3, 4], weights=[0.5, 5, 7, 5], k=1)[0] for n in range(99)\n",
+    "] + [random.choices([1, 2, 3, 4], weights=[1, 7, 4, 1], k=1)[0] for n in range(100)]\n",
+    "\n",
+    "# blood pressure 0 is low, 1 is normal, 5 is extremly high\n",
+    "df[\"blood_pressure\"] = [\n",
+    "    random.choices([0, 1, 2, 3, 4, 5], weights=[0.5, 1, 5, 6, 1, 0.5], k=1)[0]\n",
+    "    for n in range(99)\n",
+    "] + [\n",
+    "    random.choices([0, 1, 2, 3, 4, 5], weights=[0.5, 5, 5, 1, 1, 0.5], k=1)[0]\n",
+    "    for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# smoker 0 is non smoker, 1 is smoker\n",
+    "df[\"smoker\"] = [[1, 0][random.random() > 0.8] for n in range(99)] + [\n",
+    "    [1, 0][random.random() > 0.2] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# age\n",
+    "x = np.arange(20, 90)\n",
     "pmf = poisson.pmf(x, 72)\n",
-    "age = [random.choices(x, weights = pmf, k = 1)[0] for n in range(99)]\n",
-    "x = np.arange(20,90)\n",
+    "age = [random.choices(x, weights=pmf, k=1)[0] for n in range(99)]\n",
+    "x = np.arange(20, 90)\n",
     "pmf = poisson.pmf(x, 55)\n",
-    "age2 = [random.choices(x, weights = pmf, k = 1)[0] for n in range(100)]\n",
-    "df['age'] = age + age2\n",
+    "age2 = [random.choices(x, weights=pmf, k=1)[0] for n in range(100)]\n",
+    "df[\"age\"] = age + age2\n",
     "\n",
-    "#Add the data of your MP\n",
+    "# Add the data of your MP\n",
     "cancer = cancer + [1]\n",
     "\n",
-    "#add new row to end of DataFrame\n",
-    "#the order of the list indicates in order diabetes, asthma, bmi_group, blood_pressure, smoker, age\n",
+    "# add new row to end of DataFrame\n",
+    "# the order of the list indicates in order diabetes, asthma, bmi_group, blood_pressure, smoker, age\n",
     "df.loc[len(df.index)] = [1, 1, 3, 2, 1, 62]"
    ]
   },
@@ -179,7 +193,7 @@
     }
    ],
    "source": [
-    "print(df.iloc[199,:])"
+    "print(df.iloc[199, :])"
    ]
   },
   {
@@ -216,7 +230,7 @@
     "# Train a model\n",
     "prng = np.random.RandomState(12)\n",
     "svc = SVC(C=1, gamma=3, probability=True, random_state=prng)\n",
-    "svc.fit(df, cancer)\n"
+    "svc.fit(df, cancer)"
    ]
   },
   {
@@ -247,17 +261,18 @@
    "source": [
     "test_example = pd.DataFrame(\n",
     "    {\n",
-    "        'diabetes': 1,\n",
-    "        'asthma': 1,\n",
-    "        'bmi_group': 1,\n",
-    "        'blood_pressure': 5,\n",
-    "        'smoker': 1,\n",
-    "        'age': 72\n",
-    "    }, index=[1]\n",
+    "        \"diabetes\": 1,\n",
+    "        \"asthma\": 1,\n",
+    "        \"bmi_group\": 1,\n",
+    "        \"blood_pressure\": 5,\n",
+    "        \"smoker\": 1,\n",
+    "        \"age\": 72,\n",
+    "    },\n",
+    "    index=[1],\n",
     ")\n",
     "predictions = svc.predict_proba(test_example)\n",
-    "print(f'non-cancer score = {predictions[0][0]:.2f}')\n",
-    "print(f'cancer score = {predictions[0][1]:.2f}')\n"
+    "print(f\"non-cancer score = {predictions[0][0]:.2f}\")\n",
+    "print(f\"cancer score = {predictions[0][1]:.2f}\")"
    ]
   },
   {
@@ -304,12 +319,12 @@
    ],
    "source": [
     "feature_vals = {\n",
-    "    'diabetes': [1],\n",
-    "    'asthma': [1],\n",
-    "    'bmi_group': [1, 2, 3, 4],\n",
-    "    'blood_pressure': [0, 1, 2, 3, 4, 5],\n",
-    "    'smoker': [1],\n",
-    "    'age': [62]\n",
+    "    \"diabetes\": [1],\n",
+    "    \"asthma\": [1],\n",
+    "    \"bmi_group\": [1, 2, 3, 4],\n",
+    "    \"blood_pressure\": [0, 1, 2, 3, 4, 5],\n",
+    "    \"smoker\": [1],\n",
+    "    \"age\": [62],\n",
     "}\n",
     "\n",
     "all_combinations = product(*feature_vals.values())\n",
@@ -324,8 +339,10 @@
     "\n",
     "# Add the prob cancer to the dataframe\n",
     "attack_values = attack_inputs.copy()\n",
-    "attack_values['confidence'] = probs[:, 1]\n",
-    "sorted_attack_values = attack_values.sort_values(by='confidence', ascending=False)[['bmi_group', 'blood_pressure', 'confidence']]"
+    "attack_values[\"confidence\"] = probs[:, 1]\n",
+    "sorted_attack_values = attack_values.sort_values(by=\"confidence\", ascending=False)[\n",
+    "    [\"bmi_group\", \"blood_pressure\", \"confidence\"]\n",
+    "]"
    ]
   },
   {
@@ -426,8 +443,10 @@
     "\n",
     "# Add the prob cancer to the dataframe\n",
     "attack_values = attack_inputs.copy()\n",
-    "attack_values['confidence'] = probs[:, 1]\n",
-    "sorted_attack_values = attack_values.sort_values(by='confidence', ascending=False)[['bmi_group', 'blood_pressure', 'confidence']]\n",
+    "attack_values[\"confidence\"] = probs[:, 1]\n",
+    "sorted_attack_values = attack_values.sort_values(by=\"confidence\", ascending=False)[\n",
+    "    [\"bmi_group\", \"blood_pressure\", \"confidence\"]\n",
+    "]\n",
     "print(sorted_attack_values.head(n=24))"
    ]
   },
diff --git a/risk_examples/python/instance_based_mimic.ipynb b/risk_examples/python/instance_based_mimic.ipynb
index 288887bb..0f4a8244 100644
--- a/risk_examples/python/instance_based_mimic.ipynb
+++ b/risk_examples/python/instance_based_mimic.ipynb
@@ -33,14 +33,15 @@
     "import logging\n",
     "import numpy as np\n",
     "import pylab as plt\n",
+    "\n",
     "%matplotlib inline\n",
     "\n",
-    "logging.getLogger('matplotlib.font_manager').disabled = True\n",
+    "logging.getLogger(\"matplotlib.font_manager\").disabled = True\n",
     "\n",
     "os.chdir(\"c:\\\\Users\\\\simonr04\\\\git\\\\GRAIMatter\")\n",
     "from data_preprocessing.data_interface import get_data_sklearn\n",
     "\n",
-    "logging.basicConfig(level = logging.DEBUG)\n"
+    "logging.basicConfig(level=logging.DEBUG)"
    ]
   },
   {
@@ -69,14 +70,12 @@
     "DATASET_NAME = \"mimic2-iaccd\"\n",
     "X, y = get_data_sklearn(DATASET_NAME)\n",
     "# Choose some features (we don't need all of them)\n",
-    "FEATURES = [\n",
-    "    \"age\", \"gender_num\", \"bmi\", \"day_icu_intime_num\", \"liver_flg\", \"copd_flg\"\n",
-    "]\n",
+    "FEATURES = [\"age\", \"gender_num\", \"bmi\", \"day_icu_intime_num\", \"liver_flg\", \"copd_flg\"]\n",
     "subX = X[FEATURES].copy()\n",
     "\n",
     "# Round bmi to an integer\n",
-    "subX['bmi'] = subX['bmi'].astype(int)\n",
-    "subX['age'] = subX['age'].astype(int)"
+    "subX[\"bmi\"] = subX[\"bmi\"].astype(int)\n",
+    "subX[\"age\"] = subX[\"age\"].astype(int)"
    ]
   },
   {
@@ -127,22 +126,23 @@
     "from sklearn.svm import SVC\n",
     "from sklearn.model_selection import train_test_split\n",
     "from sklearn.metrics import roc_curve\n",
-    "font = {'size': 14}\n",
-    "plt.rc('font', **font)\n",
+    "\n",
+    "font = {\"size\": 14}\n",
+    "plt.rc(\"font\", **font)\n",
     "train_X, test_X, train_y, test_y = train_test_split(subX.values, y.values.flatten())\n",
-    "svm = SVC(probability=True, gamma=.01)\n",
+    "svm = SVC(probability=True, gamma=0.01)\n",
     "svm.fit(train_X, train_y)\n",
     "train_probs = svm.predict_proba(train_X)\n",
     "test_probs = svm.predict_proba(test_X)\n",
     "plt.figure(figsize=(10, 10))\n",
     "fpr, tpr, _ = roc_curve(test_y, test_probs[:, 1])\n",
-    "plt.plot(fpr, tpr, label='test')\n",
+    "plt.plot(fpr, tpr, label=\"test\")\n",
     "fpr, tpr, _ = roc_curve(train_y, train_probs[:, 1])\n",
-    "plt.plot(fpr, tpr, label='train')\n",
+    "plt.plot(fpr, tpr, label=\"train\")\n",
     "plt.legend()\n",
-    "plt.xlabel('fpr')\n",
-    "plt.ylabel('tpr')\n",
-    "plt.plot([0, 1], [0, 1], 'k--')\n"
+    "plt.xlabel(\"fpr\")\n",
+    "plt.ylabel(\"tpr\")\n",
+    "plt.plot([0, 1], [0, 1], \"k--\")"
    ]
   },
   {
@@ -170,7 +170,9 @@
    "source": [
     "n_support_vectors = len(svm.support_vectors_)\n",
     "n_total = len(train_X)\n",
-    "print(f\"In this example, the SVM has stored exact copies of {n_support_vectors} of the original data rows (out of {n_total} total rows)\")"
+    "print(\n",
+    "    f\"In this example, the SVM has stored exact copies of {n_support_vectors} of the original data rows (out of {n_total} total rows)\"\n",
+    ")"
    ]
   },
   {
diff --git a/risk_examples/python/membership_inference_cancer.ipynb b/risk_examples/python/membership_inference_cancer.ipynb
index 6be14223..f578f186 100644
--- a/risk_examples/python/membership_inference_cancer.ipynb
+++ b/risk_examples/python/membership_inference_cancer.ipynb
@@ -83,40 +83,54 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "#1 is cancer, 0 is no cancer, this is our label and what we want to predict.\n",
-    "response = [1]*99 + [0]*100\n",
+    "# 1 is cancer, 0 is no cancer, this is our label and what we want to predict.\n",
+    "response = [1] * 99 + [0] * 100\n",
     "\n",
     "df = pd.DataFrame()\n",
     "\n",
-    "#diabetes 0 no, 1 yes\n",
-    "df['diabetes'] = [[1, 0][random.random()>0.7] for n in range(99)] +[[1, 0][random.random()>0.2] for n in range(100)]\n",
-    "\n",
-    "#asthma 0 no, 1 yes\n",
-    "df['asthma'] = [[1, 0][random.random()>0.7] for n in range(99)] + [[1, 0][random.random()>0.5] for n in range(100)]\n",
-    "\n",
-    "#bmi group 1 under, 2 normal, 3 overweight, 4 obese\n",
-    "df['bmi_group'] = [random.choices([1, 2, 3, 4], weights = [0.5, 5, 7, 5], k = 1)[0] for n in range(99)] + [random.choices([1, 2, 3, 4], weights = [1, 7, 4, 1], k = 1)[0] for n in range(100)]\n",
-    "\n",
-    "#blood pressure 0 is low, 1 is normal, 5 is extremly high\n",
-    "df['blood_pressure'] = [random.choices([0, 1, 2, 3, 4, 5], weights = [0.5, 1, 5, 6, 1, 0.5], k = 1)[0] for n in range(99)] + [random.choices([0, 1, 2, 3, 4, 5], weights = [0.5, 5, 5, 1, 1, 0.5], k = 1)[0] for n in range(100)]\n",
-    "\n",
-    "#smoker 0 is non smoker, 1 is smoker\n",
-    "df['smoker'] = [[1, 0][random.random()>0.8] for n in range(99)] + [[1, 0][random.random()>0.2] for n in range(100)]\n",
-    "\n",
-    "#age\n",
-    "x = np.arange(20,90)\n",
+    "# diabetes 0 no, 1 yes\n",
+    "df[\"diabetes\"] = [[1, 0][random.random() > 0.7] for n in range(99)] + [\n",
+    "    [1, 0][random.random() > 0.2] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# asthma 0 no, 1 yes\n",
+    "df[\"asthma\"] = [[1, 0][random.random() > 0.7] for n in range(99)] + [\n",
+    "    [1, 0][random.random() > 0.5] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# bmi group 1 under, 2 normal, 3 overweight, 4 obese\n",
+    "df[\"bmi_group\"] = [\n",
+    "    random.choices([1, 2, 3, 4], weights=[0.5, 5, 7, 5], k=1)[0] for n in range(99)\n",
+    "] + [random.choices([1, 2, 3, 4], weights=[1, 7, 4, 1], k=1)[0] for n in range(100)]\n",
+    "\n",
+    "# blood pressure 0 is low, 1 is normal, 5 is extremly high\n",
+    "df[\"blood_pressure\"] = [\n",
+    "    random.choices([0, 1, 2, 3, 4, 5], weights=[0.5, 1, 5, 6, 1, 0.5], k=1)[0]\n",
+    "    for n in range(99)\n",
+    "] + [\n",
+    "    random.choices([0, 1, 2, 3, 4, 5], weights=[0.5, 5, 5, 1, 1, 0.5], k=1)[0]\n",
+    "    for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# smoker 0 is non smoker, 1 is smoker\n",
+    "df[\"smoker\"] = [[1, 0][random.random() > 0.8] for n in range(99)] + [\n",
+    "    [1, 0][random.random() > 0.2] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# age\n",
+    "x = np.arange(20, 90)\n",
     "pmf = poisson.pmf(x, 72)\n",
-    "age = [random.choices(x, weights = pmf, k = 1)[0] for n in range(99)]\n",
-    "x = np.arange(20,90)\n",
+    "age = [random.choices(x, weights=pmf, k=1)[0] for n in range(99)]\n",
+    "x = np.arange(20, 90)\n",
     "pmf = poisson.pmf(x, 55)\n",
-    "age2 = [random.choices(x, weights = pmf, k = 1)[0] for n in range(100)]\n",
-    "df['age'] = age + age2\n",
+    "age2 = [random.choices(x, weights=pmf, k=1)[0] for n in range(100)]\n",
+    "df[\"age\"] = age + age2\n",
     "\n",
-    "#Add the data of your MP\n",
+    "# Add the data of your MP\n",
     "response = response + [1]\n",
     "\n",
-    "#add new row to end of DataFrame\n",
-    "#the order of the list indicates in order diabetes, asthma, bmi_group, blood_pressure, smoker, age\n",
+    "# add new row to end of DataFrame\n",
+    "# the order of the list indicates in order diabetes, asthma, bmi_group, blood_pressure, smoker, age\n",
     "df.loc[len(df.index)] = [1, 1, 3, 2, 1, 62]"
    ]
   },
@@ -180,7 +194,7 @@
     }
    ],
    "source": [
-    "print(df.iloc[199,:])"
+    "print(df.iloc[199, :])"
    ]
   },
   {
@@ -214,7 +228,6 @@
     }
    ],
    "source": [
-    "\n",
     "# Train a model\n",
     "prng = np.random.RandomState(12)\n",
     "svc = SVC(C=1, gamma=3, probability=True, random_state=prng)\n",
@@ -249,17 +262,18 @@
    "source": [
     "test_example = pd.DataFrame(\n",
     "    {\n",
-    "        'diabetes': 1,\n",
-    "        'asthma': 1,\n",
-    "        'bmi_group': 1,\n",
-    "        'blood_pressure': 5,\n",
-    "        'smoker': 1,\n",
-    "        'age': 72\n",
-    "    }, index=[1]\n",
+    "        \"diabetes\": 1,\n",
+    "        \"asthma\": 1,\n",
+    "        \"bmi_group\": 1,\n",
+    "        \"blood_pressure\": 5,\n",
+    "        \"smoker\": 1,\n",
+    "        \"age\": 72,\n",
+    "    },\n",
+    "    index=[1],\n",
     ")\n",
     "predictions = svc.predict_proba(test_example)\n",
-    "print(f'non-responders score = {predictions[0][0]:.2f}')\n",
-    "print(f'responders score = {predictions[0][1]:.2f}')\n"
+    "print(f\"non-responders score = {predictions[0][0]:.2f}\")\n",
+    "print(f\"responders score = {predictions[0][1]:.2f}\")"
    ]
   },
   {
@@ -297,14 +311,17 @@
     }
    ],
    "source": [
-    "mp_data = pd.DataFrame({\n",
-    "    'diabetes': 1,\n",
-    "    'asthma': 1,\n",
-    "    'bmi_group': 3,\n",
-    "    'blood_pressure': 2,\n",
-    "    'smoker': 1,\n",
-    "    'age': 62    \n",
-    "}, index=[1])\n",
+    "mp_data = pd.DataFrame(\n",
+    "    {\n",
+    "        \"diabetes\": 1,\n",
+    "        \"asthma\": 1,\n",
+    "        \"bmi_group\": 3,\n",
+    "        \"blood_pressure\": 2,\n",
+    "        \"smoker\": 1,\n",
+    "        \"age\": 62,\n",
+    "    },\n",
+    "    index=[1],\n",
+    ")\n",
     "mp_preds = svc.predict_proba(mp_data)\n",
     "print(mp_preds)"
    ]
@@ -332,34 +349,48 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "#1 is cancer, 0 is no response, this is our label and what we want to predict.\n",
-    "shadow_response = [1]*100 + [0]*100\n",
+    "# 1 is cancer, 0 is no response, this is our label and what we want to predict.\n",
+    "shadow_response = [1] * 100 + [0] * 100\n",
     "\n",
     "shadow_df = pd.DataFrame()\n",
     "\n",
-    "#diabetes 0 no, 1 yes\n",
-    "shadow_df['diabetes'] = [[1, 0][random.random()>0.7] for n in range(100)] +[[1, 0][random.random()>0.2] for n in range(100)]\n",
-    "\n",
-    "#asthma 0 no, 1 yes\n",
-    "shadow_df['asthma'] = [[1, 0][random.random()>0.7] for n in range(100)] + [[1, 0][random.random()>0.5] for n in range(100)]\n",
-    "\n",
-    "#bmi group 1 under, 2 normal, 3 overweight, 4 obese\n",
-    "shadow_df['bmi_group'] = [random.choices([1, 2, 3, 4], weights = [0.5, 5, 7, 5], k = 1)[0] for n in range(100)] + [random.choices([1, 2, 3, 4], weights = [1, 7, 4, 1], k = 1)[0] for n in range(100)]\n",
-    "\n",
-    "#blood pressure 0 is low, 1 is normal, 5 is extremly high\n",
-    "shadow_df['blood_pressure'] = [random.choices([0, 1, 2, 3, 4, 5], weights = [0.5, 1, 5, 6, 1, 0.5], k = 1)[0] for n in range(100)] + [random.choices([0, 1, 2, 3, 4, 5], weights = [0.5, 5, 5, 1, 1, 0.5], k = 1)[0] for n in range(100)]\n",
-    "\n",
-    "#smoker 0 is non smoker, 1 is smoker\n",
-    "shadow_df['smoker'] = [[1, 0][random.random()>0.8] for n in range(100)] + [[1, 0][random.random()>0.2] for n in range(100)]\n",
-    "\n",
-    "#age\n",
-    "x = np.arange(20,90)\n",
+    "# diabetes 0 no, 1 yes\n",
+    "shadow_df[\"diabetes\"] = [[1, 0][random.random() > 0.7] for n in range(100)] + [\n",
+    "    [1, 0][random.random() > 0.2] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# asthma 0 no, 1 yes\n",
+    "shadow_df[\"asthma\"] = [[1, 0][random.random() > 0.7] for n in range(100)] + [\n",
+    "    [1, 0][random.random() > 0.5] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# bmi group 1 under, 2 normal, 3 overweight, 4 obese\n",
+    "shadow_df[\"bmi_group\"] = [\n",
+    "    random.choices([1, 2, 3, 4], weights=[0.5, 5, 7, 5], k=1)[0] for n in range(100)\n",
+    "] + [random.choices([1, 2, 3, 4], weights=[1, 7, 4, 1], k=1)[0] for n in range(100)]\n",
+    "\n",
+    "# blood pressure 0 is low, 1 is normal, 5 is extremly high\n",
+    "shadow_df[\"blood_pressure\"] = [\n",
+    "    random.choices([0, 1, 2, 3, 4, 5], weights=[0.5, 1, 5, 6, 1, 0.5], k=1)[0]\n",
+    "    for n in range(100)\n",
+    "] + [\n",
+    "    random.choices([0, 1, 2, 3, 4, 5], weights=[0.5, 5, 5, 1, 1, 0.5], k=1)[0]\n",
+    "    for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# smoker 0 is non smoker, 1 is smoker\n",
+    "shadow_df[\"smoker\"] = [[1, 0][random.random() > 0.8] for n in range(100)] + [\n",
+    "    [1, 0][random.random() > 0.2] for n in range(100)\n",
+    "]\n",
+    "\n",
+    "# age\n",
+    "x = np.arange(20, 90)\n",
     "pmf = poisson.pmf(x, 72)\n",
-    "age = [random.choices(x, weights = pmf, k = 1)[0] for n in range(100)]\n",
-    "x = np.arange(20,90)\n",
+    "age = [random.choices(x, weights=pmf, k=1)[0] for n in range(100)]\n",
+    "x = np.arange(20, 90)\n",
     "pmf = poisson.pmf(x, 55)\n",
-    "age2 = [random.choices(x, weights = pmf, k = 1)[0] for n in range(100)]\n",
-    "shadow_df['age'] = age + age2"
+    "age2 = [random.choices(x, weights=pmf, k=1)[0] for n in range(100)]\n",
+    "shadow_df[\"age\"] = age + age2"
    ]
   },
   {
@@ -377,7 +408,9 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "shadow_train_x, shadow_test_x, shadow_train_y, shadow_test_y = train_test_split(shadow_df, shadow_response, test_size=0.5)"
+    "shadow_train_x, shadow_test_x, shadow_train_y, shadow_test_y = train_test_split(\n",
+    "    shadow_df, shadow_response, test_size=0.5\n",
+    ")"
    ]
   },
   {
@@ -439,8 +472,8 @@
    "source": [
     "train_preds = shadow_svc.predict_proba(shadow_train_x).max(axis=1)\n",
     "test_preds = shadow_svc.predict_proba(shadow_test_x).max(axis=1)\n",
-    "print(f'Mean of confidence for training examples = {train_preds.mean():.2f}')\n",
-    "print(f'Mean of confidence for non-training examples = {test_preds.mean():.2f}')\n"
+    "print(f\"Mean of confidence for training examples = {train_preds.mean():.2f}\")\n",
+    "print(f\"Mean of confidence for non-training examples = {test_preds.mean():.2f}\")"
    ]
   },
   {
@@ -466,7 +499,7 @@
     }
    ],
    "source": [
-    "print(f'Maximum confidence for MP in original model: {mp_preds.max():.2f}')"
+    "print(f\"Maximum confidence for MP in original model: {mp_preds.max():.2f}\")"
    ]
   },
   {
@@ -496,6 +529,7 @@
    ],
    "source": [
     "from sklearn.linear_model import LogisticRegression\n",
+    "\n",
     "lr = LogisticRegression()\n",
     "train_x = np.vstack((train_preds[:, None], test_preds[:, None]))\n",
     "train_y = np.hstack((np.ones(len(train_preds)), np.zeros(len(test_preds))))\n",
@@ -528,8 +562,8 @@
    "source": [
     "input_array = np.array([[mp_preds.max()]])\n",
     "prob_membership = lr.predict_proba(input_array)\n",
-    "print(f'Confidence of non-membership = {prob_membership[0][0]:.2f}')\n",
-    "print(f'Confidence of membership = {prob_membership[0][1]:.2f}')\n"
+    "print(f\"Confidence of non-membership = {prob_membership[0][0]:.2f}\")\n",
+    "print(f\"Confidence of membership = {prob_membership[0][1]:.2f}\")"
    ]
   },
   {
@@ -581,8 +615,8 @@
     "\n",
     "train_preds = shadow_svc.predict_proba(shadow_train_x).max(axis=1)\n",
     "test_preds = shadow_svc.predict_proba(shadow_test_x).max(axis=1)\n",
-    "print(f'Mean of confidence for training examples = {train_preds.mean():.2f}')\n",
-    "print(f'Mean of confidence for non-training examples = {test_preds.mean():.2f}')\n",
+    "print(f\"Mean of confidence for training examples = {train_preds.mean():.2f}\")\n",
+    "print(f\"Mean of confidence for non-training examples = {test_preds.mean():.2f}\")\n",
     "\n",
     "lr = LogisticRegression()\n",
     "train_x = np.vstack((train_preds[:, None], test_preds[:, None]))\n",
@@ -592,8 +626,8 @@
     "\n",
     "input_array = np.array([[mp_preds.max()]])\n",
     "prob_membership = lr.predict_proba(input_array)\n",
-    "print(f'Confidence of non-membership = {prob_membership[0][0]:.2f}')\n",
-    "print(f'Confidence of membership = {prob_membership[0][1]:.2f}')\n"
+    "print(f\"Confidence of non-membership = {prob_membership[0][0]:.2f}\")\n",
+    "print(f\"Confidence of membership = {prob_membership[0][1]:.2f}\")"
    ]
   },
   {
diff --git a/safemodel/__init__.py b/safemodel/__init__.py
index e69de29b..098baade 100644
--- a/safemodel/__init__.py
+++ b/safemodel/__init__.py
@@ -0,0 +1,2 @@
+""" empty file"""
+from .reporting import get_reporting_string
diff --git a/safemodel/classifiers/__init__.py b/safemodel/classifiers/__init__.py
index e5145af1..d3c1cc3f 100644
--- a/safemodel/classifiers/__init__.py
+++ b/safemodel/classifiers/__init__.py
@@ -1,3 +1,4 @@
+"""makes class for various models supported"""
 from .dp_svc import DPSVC
 from .safedecisiontreeclassifier import SafeDecisionTreeClassifier
 from .safekeras import SafeKerasModel
diff --git a/safemodel/classifiers/dp_svc.py b/safemodel/classifiers/dp_svc.py
index b83d928d..3b06cf7a 100644
--- a/safemodel/classifiers/dp_svc.py
+++ b/safemodel/classifiers/dp_svc.py
@@ -1,5 +1,7 @@
 """
 Differentially private SVC
+James Liley
+21/03/22
 """
 import logging
 from typing import Any
@@ -8,20 +10,16 @@
 from sklearn.linear_model import LogisticRegression
 from sklearn.svm import SVC
 
-
 local_logger = logging.getLogger(__file__)
 local_logger.setLevel(logging.WARNING)
 
 
 # pylint: disable = invalid-name
+# pylint: disable=R0902: too-many-instance-attributes
+# pylint:disable = fixme
 
 
-class DPSVC():
-    ## Wrapper for differentially private SVM
-    ##
-    ## James Liley
-    ## 21/03/22
-
+class DPSVC:
     """
     Wrapper for differentially private SVM, implemented according to the method in
 
@@ -69,6 +67,7 @@ def __init__(self, C=1.0, gamma="scale", dhat=1000, eps=10, **kwargs):
         self.classes_ = [0, 1]
         self.intercept = None
         self.noisy_weights = None
+        self.set_params(**kwargs)
 
     def phi_hat(self, input_vector):
         """Project a single feature"""
diff --git a/safemodel/classifiers/new_model_template.py b/safemodel/classifiers/new_model_template.py
index b16a6e32..bf36622c 100644
--- a/safemodel/classifiers/new_model_template.py
+++ b/safemodel/classifiers/new_model_template.py
@@ -1,4 +1,9 @@
-"""This is a template for implementing supplementary models"""
+"""This is a template for implementing supplementary models
+   Obviously we have invented an sklearn ensemble called ModelToMakeSafer
+   Replace this with details of the model you wish to create a wrapper for
+   and then remove the comment which disables the pylint warning"""
+
+# pylint: disable=duplicate-code
 
 from __future__ import annotations
 
@@ -7,28 +12,67 @@
 
 import numpy as np
 from dictdiffer import diff
-from sklearn.ensemble import ModelToMakeSafer
+from sklearn.ensemble import ModelToMakeSafer  # pylint: disable=E0611
+from sklearn.tree import DecisionTreeClassifier
 
 from ..safemodel import SafeModel
+from .safedecisiontreeclassifier import decision_trees_are_equal
+
+
+def check_present(
+    item: str, curr_separate: dict, saved_separate: dict
+) -> tuple[str, bool]:
+    """checks item is present in both dicts and reports suitably"""
+    disclosive = False
+    msg = ""
+    if curr_separate[item] == "Absent" and saved_separate[item] == "Absent":
+        disclosive = True
+        msg += "Error: model has not been fitted to data.\n"
+
+    elif curr_separate[item] == "Absent":
+        disclosive = True
+        msg += "Error: current version of model has had trees removed after fitting.\n"
 
-class SafeModelToMakeSafe(SafeModel, ModelToMakeSafe):
+    elif saved_separate[item] == "Absent":
+        disclosive = True
+        msg += "Error: current version of model has had trees manually edited.\n"
+    return msg, disclosive
+
+
+class SafeModelToMakeSafe(SafeModel, ModelToMakeSafer):
     """Privacy protected ModelToMakeSafer."""
 
     def __init__(self, **kwargs: Any) -> None:
         """Creates model and applies constraints to params"""
         SafeModel.__init__(self)
+        self.k_anonymity = 0
+        self.basemodel_paramnames = [
+            "edit",
+            "this",
+            "list",
+            "to",
+            "contain",
+            "just",
+            "the",
+            "valid",
+            "parameters",
+            "for",
+            "the",
+            "class",
+            "you ",
+            "are",
+            "creating",
+            "a",
+            "safe",
+            "wrapper",
+            "version",
+            "of",
+        ]
 
-        self.basemodel_paramnames=[
-            'edit','this','list','to',
-            'contain','just','the','valid','parameters',
-            'for','the','class',
-            'you ','are','creating','a'
-            'safe','wrapper','version','of']
-
-        the_kwds=dict()
-        for key,val in kwargs.items():
+        the_kwds = {}
+        for key, val in kwargs.items():
             if key in self.basemodel_paramnames:
-                the_kwds[key]=val
+                the_kwds[key] = val
         ModelToMakeSafer.__init__(self, **the_kwds)
         self.model_type: str = "ModelToMakeSafer"
         super().preliminary_check(apply_constraints=True, verbose=True)
@@ -39,14 +83,18 @@ def __init__(self, **kwargs: Any) -> None:
         ]
         self.examine_seperately_items = ["base_estimator", "estimators_"]
 
-    def additional_checks(
+    def additional_checks(  # pylint: disable=too-many-nested-blocks,too-many-branches
         self, curr_separate: dict, saved_separate: dict
     ) -> tuple[str, str]:
-        """ModelToMakeSafer specific checks"""
-        msg=""
-        disclosive=False
+        """ModelToMakeSafer specific checks
+        This example shows how to deal with instances of sklearn's tree class
+        as base estimators in a forest (line 99)
+        or as single estimators (lines 114-118)
+        """
+        msg = ""
+        disclosive = False
         ## call the super function to deal with any items that are lists
-        #msg, disclosive = super().additional_checks(curr_separate, saved_separate)
+        # msg, disclosive = super().additional_checks(curr_separate, saved_separate)
         # now the relevant ModelToMakeSafer specific things
         for item in self.examine_seperately_items:
             if item == "base_estimator":
@@ -60,19 +108,10 @@ def additional_checks(
                     disclosive = True
 
             elif item == "estimators_":
-
-                if curr_separate[item] == "Absent" and saved_separate[item] == "Absent":
-                    disclosive = True
-                    msg += "Error: model has not been fitted to data.\n"
-
-                elif curr_separate[item] == "Absent":
-                    disclosive = True
-                    msg += "Error: current version of model has had trees removed after fitting.\n"
-
-                elif saved_separate[item] == "Absent":
+                msg2, disclosive2 = check_present(item, curr_separate, saved_separate)
+                msg += msg2
+                if disclosive2:
                     disclosive = True
-                    msg += "Error: current version of model has had trees manually edited.\n"
-
                 else:
                     try:
                         num1 = len(curr_separate[item])
@@ -93,7 +132,7 @@ def additional_checks(
                                     msg += f"Forest base estimators {idx} differ."
                                     msg += msg2
 
-                    except BaseException as error:
+                    except BaseException as error:  # pylint:disable=broad-except
                         msg += (
                             "In Safe ModelToMakeSafer.additional_checks: "
                             f"Unable to check {item} as an exception occurred: {error}.\n"
diff --git a/safemodel/classifiers/safedecisiontreeclassifier.py b/safemodel/classifiers/safedecisiontreeclassifier.py
index 286218ec..0ea4dc61 100644
--- a/safemodel/classifiers/safedecisiontreeclassifier.py
+++ b/safemodel/classifiers/safedecisiontreeclassifier.py
@@ -8,13 +8,15 @@
 import numpy as np
 from dictdiffer import diff
 from sklearn.tree import DecisionTreeClassifier
-from sklearn.tree._tree import Tree
 
-from ..safemodel import SafeModel
 from ..reporting import get_reporting_string
+from ..safemodel import SafeModel
+
+# from sklearn.tree._tree import Tree
+
 
 def decision_trees_are_equal(
-    tree1: sklearn.tree, tree2: sklearn.tree
+    tree1: DecisionTreeClassifier, tree2: DecisionTreeClassifier
 ) -> tuple[bool, str]:
     """Compares two estimators of type sklearn.tree
     e.g. two decisionTreeClassifiers
@@ -30,11 +32,12 @@ def decision_trees_are_equal(
 
         # comparison on list of "simple" parameters
         match = list(diff(tree1_dict, tree2_dict, expand=True))
-        if len(match) > 0:
+        num_differences = len(match)
+        if num_differences > 0:
             same = False
             msg += get_reporting_string(name="basic_params_differ", match=match)
-            #f"Warning: basic parameters differ in {len(match)} places:\n"
-            for i in range(len(match)):
+            # f"Warning: basic parameters differ in {len(match)} places:\n"
+            for i in range(num_differences):
                 if match[i][0] == "change":
                     msg += f"parameter {match[i][1]} changed from {match[i][2][1]} "
                     msg += f"to {match[i][2][0]}\n"
@@ -43,20 +46,20 @@ def decision_trees_are_equal(
 
         # now internal tree params
         same2, msg2 = decision_tree_internal_trees_are_equal(tree1_tree, tree2_tree)
-        if same2 == False:
+        if same2 is False:
             same = False
             msg += msg2
 
-    except BaseException as error:
+    except BaseException as error:  # pylint:disable=broad-except
         msg += get_reporting_string(name="unable_to_check", error=error)
-        #f"Unable to check as an exception occurred: {error}"
+        # f"Unable to check as an exception occurred: {error}"
         same = False
 
     return same, msg
 
 
 def decision_tree_internal_trees_are_equal(
-    tree1_tree: Tree, tree2_tree: Tree
+    tree1_tree: Any, tree2_tree: Any
 ) -> tuple[bool, str]:
     """Tests for equality of the internal structures in a sklearn.tree._tree
     e.g. the structure, feature and threshold in each internal node etc."""
@@ -80,14 +83,14 @@ def decision_tree_internal_trees_are_equal(
     try:
         if tree1_tree == "Absent" and tree2_tree == "Absent":
             msg += get_reporting_string(name="neither_tree_trained")
-            #"neither tree trained"
+            # "neither tree trained"
         elif tree1_tree == "Absent":
             msg += get_reporting_string(name="tree1_not_trained")
-            #"tree1 not trained"
+            # "tree1 not trained"
             same = False
         elif tree2_tree == "Absent":
             msg += get_reporting_string(name="tree2_not_trained")
-            #"tree2 not trained"
+            # "tree2 not trained"
             same = False
         else:
             for attr in tree_internal_att_names:
@@ -95,27 +98,31 @@ def decision_tree_internal_trees_are_equal(
                 t2val = getattr(tree2_tree, attr)
                 if isinstance(t1val, np.ndarray):
                     if not np.array_equal(t1val, t2val):
-                        msg += get_reporting_string(name="internal_attribute_differs",
-                                                    attr=attr)
-                        #f"internal tree attribute {attr} differs\n"
+                        msg += get_reporting_string(
+                            name="internal_attribute_differs", attr=attr
+                        )
+                        # f"internal tree attribute {attr} differs\n"
                         same = False
                 else:
                     if t1val != t2val:
-                        msg += get_reporting_string(name="internal_attribute_differs",
-                                                    attr=attr)
-                        #f"internal tree attribute {attr} differs\n"
+                        msg += get_reporting_string(
+                            name="internal_attribute_differs", attr=attr
+                        )
+                        # f"internal tree attribute {attr} differs\n"
                         same = False
-    except BaseException as error:
+    except BaseException as error:  # pylint:disable=broad-except
         msg += get_reporting_string(name="exception_occurred", error=error)
-        #f"An exception occurred: {error}"
+        # f"An exception occurred: {error}"
     return same, msg
 
 
-def get_tree_k_anonymity(thetree: sklearn.tree) -> int:
+def get_tree_k_anonymity(thetree: DecisionTreeClassifier, X: Any) -> int:
+    """returns the smallest number of data items in any leaf"""
     leaves = thetree.apply(X)
     uniqs_counts = np.unique(leaves, return_counts=True)
     k_anonymity = np.min(uniqs_counts[1])
-    # print(f' leaf ids {uniqs_counts[0]} and counts {uniqs_counts[1]} the  k-anonymity of the tree is {k_anonymity}')
+    # print(f' leaf ids {uniqs_counts[0]} and counts {uniqs_counts[1]}'
+    #        f'the  k-anonymity of the tree is {k_anonymity}')
     return k_anonymity
 
 
@@ -125,22 +132,31 @@ class SafeDecisionTreeClassifier(SafeModel, DecisionTreeClassifier):
     def __init__(self, **kwargs: Any) -> None:
         """Creates model and applies constraints to params."""
         SafeModel.__init__(self)
-        self.basemodel_paramnames=[
-            'criterion','splitter','max_depth','min_samples_split',
-            'min_samples_leaf','min_weight_fraction_leaf','max_features',
-            'random_state','max_leaf_nodes','min_impurity_decrease',
-            'class_weight','ccp_alpha']
-
-        the_kwds=dict()
-        for key,val in kwargs.items():
+        self.basemodel_paramnames = [
+            "criterion",
+            "splitter",
+            "max_depth",
+            "min_samples_split",
+            "min_samples_leaf",
+            "min_weight_fraction_leaf",
+            "max_features",
+            "random_state",
+            "max_leaf_nodes",
+            "min_impurity_decrease",
+            "class_weight",
+            "ccp_alpha",
+        ]
+
+        the_kwds = {}
+        for key, val in kwargs.items():
             if key in self.basemodel_paramnames:
-                the_kwds[key]=val
+                the_kwds[key] = val
         DecisionTreeClassifier.__init__(self, **the_kwds)
         self.model_type: str = "DecisionTreeClassifier"
         super().preliminary_check(apply_constraints=True, verbose=True)
-        self.ignore_items = ["model_save_file","basemodel_paramnames", "ignore_items"]
+        self.ignore_items = ["model_save_file", "basemodel_paramnames", "ignore_items"]
         self.examine_seperately_items = ["tree_"]
-
+        self.k_anonymity = 0
 
     def additional_checks(
         self, curr_separate: dict, saved_separate: dict
@@ -158,14 +174,16 @@ def additional_checks(
             disclosive = True
         if len(curr_separate) > 1:
             msg += get_reporting_string(name="unexpected_item")
-            #(
+            # (
             #    "unexpected item in curr_seperate dict "
             #    " passed by generic additional checks."
-            #)
+            # )
 
         return msg, disclosive
 
-    def fit(self, x: np.ndarray, y: np.ndarray):
+    def fit(  # pylint: disable=arguments-differ
+        self, x: np.ndarray, y: np.ndarray
+    ) -> None:
         """Do fit and then store k-anonymity and  model dict"""
         super().fit(x, y)
         # calculate k-anonymity her since we have the tainigf data
diff --git a/safemodel/classifiers/safekeras.py b/safemodel/classifiers/safekeras.py
index ed8ed9ab..26694e0b 100644
--- a/safemodel/classifiers/safekeras.py
+++ b/safemodel/classifiers/safekeras.py
@@ -1,40 +1,45 @@
-# safekeras.py:
-# UWE 2022
-
+""" safekeras.py:
+ Jim Smith, Andrew McCarty and Richjard Preen
+ UWE 2022
+"""
 # general imports
-import copy
+# import copy
 import os
-import sys
+
+# import sys
 from typing import Any, Tuple
 
 import numpy as np
 
 # tensorflow imports
 import tensorflow as tf
-import tensorflow_privacy as tf_privacy
+import tensorflow_privacy as tfp
 from dictdiffer import diff
-from tensorflow.keras import Model as KerasModel
-from tensorflow_privacy import DPModel
+from tensorflow.keras import Model as KerasModel  # pylint: disable = import-error
 from tensorflow_privacy.privacy.analysis import compute_dp_sgd_privacy
-from tensorflow_privacy.privacy.optimizers import dp_optimizer_keras
+
+from safemodel.reporting import get_reporting_string
 
 # safemodel superclass
 from safemodel.safemodel import SafeModel
-from safemodel.reporting import get_reporting_string
+
+# from tensorflow_privacy.privacy.optimizers import dp_optimizer_keras
 
 
 def same_configs(m1: Any, m2: Any) -> Tuple[bool, str]:
-    if len(m1.layers) != len(m2.layers):
+    """Checks if two models havethe same architecture"""
+    num_layers = len(m1.layers)
+    if len(m2.layers) != num_layers:
         return False, "different numbers of layers"
-    for layer in range(len(m1.layers)):
+    for layer in range(num_layers):
         m1_layer_config = m1.layers[layer].get_config()
         m2_layer_config = m2.layers[layer].get_config()
         match = list(diff(m1_layer_config, m2_layer_config, expand=True))
-        if len(match) > 0:
-            disclosive = True
+        num_diffs = len(match)
+        if num_diffs > 0:
             msg = get_reporting_string(name="layer_configs_differ", match=match)
-            #f"Layer {layer} configs differ in {len(match)} places:\n"
-            for i in range(len(match)):
+            # f"Layer {layer} configs differ in {len(match)} places:\n"
+            for i in range(num_diffs):
                 if match[i][0] == "change":
                     msg += f"parameter {match[i][1]} changed from {match[i][2][1]} "
                     msg += f"to {match[i][2][0]} after model was fitted.\n"
@@ -46,10 +51,11 @@ def same_configs(m1: Any, m2: Any) -> Tuple[bool, str]:
 
 
 def same_weights(m1: Any, m2: Any) -> Tuple[bool, str]:
-    if len(m1.layers) != len(m2.layers):
+    """checks if two nets with same architecture havethe same weights"""
+    num_layers = len(m1.layers)
+    if num_layers != len(m2.layers):
         return False, "different numbers of layers"
-    numlayers = len(m1.layers)
-    for layer in range(numlayers):
+    for layer in range(num_layers):
         m1layer = m1.layers[layer].get_weights()
         m2layer = m2.layers[layer].get_weights()
         if len(m1layer) != len(m2layer):
@@ -74,15 +80,15 @@ def test_checkpoint_equality(v1: str, v2: str) -> Tuple[bool, str]:
 
     try:
         model1 = tf.keras.models.load_model(v1)
-    except Exception as e:
+    except Exception as e:  # pylint:disable=broad-except
         msg = get_reporting_string(name="error_reloading_model_v1", e=e)
-        #f"Error re-loading  model from {v1}:  {e}"
+        # f"Error re-loading  model from {v1}:  {e}"
         return False, msg
     try:
         model2 = tf.keras.models.load_model(v2)
-    except Exception as e:
+    except Exception as e:  # pylint:disable=broad-except
         msg = get_reporting_string(name="error_reloading_model_v2", e=e)
-        #f"Error re-loading  model from {v2}: {e}"
+        # f"Error re-loading  model from {v2}: {e}"
         return False, msg
 
     same_config, config_message = same_configs(model1, model2)
@@ -95,35 +101,97 @@ def test_checkpoint_equality(v1: str, v2: str) -> Tuple[bool, str]:
     return same, msg
 
 
+def check_DP_used(optimizer) -> Tuple[bool, str]:
+    """checks whether the DP optimizer was actually the one used"""
+    DPused = False
+    reason = "None"
+
+    key_needed = "_was_dp_gradients_called"
+    critical_val = optimizer.__dict__.get(key_needed, "missing")
+
+    if critical_val is True:
+        reason = get_reporting_string(name="dp_optimizer_run")
+        DPused = True
+    elif critical_val == "missing":
+        reason = get_reporting_string(name="no_dp_gradients_key")
+    elif critical_val is False:
+        reason = get_reporting_string(name="changed_opt_no_fit")
+    else:
+        reason = get_reporting_string(name="unrecognised_combination")
+
+    return DPused, reason
+
+
+def check_optimizer_allowed(optimizer) -> Tuple[bool, str]:
+    """checks if the model's optimizer is in our white-list
+    default setting is not allowed
+    """
+    allowed = False
+    reason = get_reporting_string(name="optimizer_not_allowed", optimizer=optimizer)
+    allowed_optimizers = [
+        "tensorflow_privacy.DPKerasAdagradOptimizer",
+        "tensorflow_privacy.DPKerasAdamOptimizer",
+        "tensorflow_privacy.DPKerasSGDOptimizer",
+    ]
+    if optimizer in allowed_optimizers:
+        allowed = True
+        reason = get_reporting_string(name="optimizer_allowed", optimizer=optimizer)
+
+    return reason, allowed
+
+
+def check_optimizer_is_DP(optimizer) -> Tuple[bool, str]:
+    """checks whether optimizer is one of tensorflow's DP versions"""
+    DPused = False
+    reason = "None"
+    if "_was_dp_gradients_called" not in optimizer.__dict__:
+        reason = get_reporting_string(name="no_dp_gradients_key")
+    else:
+        reason = get_reporting_string(name="found_dp_gradients_key")
+        DPused = True
+    return DPused, reason
+
+
 class SafeKerasModel(KerasModel, SafeModel):
-    """Privacy Protected Wrapper around  tf.keras.Model class from tensorflow 2.8"""
+    """Privacy Protected Wrapper around  tf.keras.Model class from tensorflow 2.8
+    disabling pylont warnings about number of instance attributes
+    as this is necessarily complex
+    """
 
+    # pylint: disable=too-many-instance-attributes
     def __init__(self, *args: Any, **kwargs: Any) -> None:
         """Creates model and applies constraints to params"""
 
-        the_args = args
+        # the_args = args
         the_kwargs = kwargs
 
         # initialise all the values that get provided as options to keras
         # and also l2 norm clipping and learning rates, batch sizes
-        if "inputs" in kwargs.keys():
+        ##inputs = kwargs.get("inputs","notFound")
+        ##if inputs=="notFound":
+        ##    inputs = args[0] if len(args) == 3 else None
+        if "inputs" in kwargs.keys():  # pylint: disable=consider-iterating-dictionary
             inputs = the_kwargs["inputs"]
         elif len(args) == 3:  # defaults is for Model(input,outputs,names)
             inputs = args[0]
-
         self.outputs = None
-        if "outputs" in kwargs.keys():
+        ##outputs = kwargs.get("outputs","notFound")
+        ##if outputs=="notFound":
+        ##    outputs = args[1] if len(args) == 3 else None
+        if "outputs" in kwargs.keys():  # pylint: disable=consider-iterating-dictionary
             outputs = the_kwargs["outputs"]
         elif len(args) == 3:
             # self.outputs = args[1]
             outputs = args[1]
 
-        super().__init__(inputs=inputs, outputs=outputs)
+        # call the keras super class first as this comes first in chain
+        super().__init__(  # pylint: disable=unexpected-keyword-arg
+            inputs=inputs, outputs=outputs
+        )
 
         # set values where the user has supplied them
         # if not supplied set to a value that preliminary_check
         # will over-ride with TRE-specific values from rules.json
-
         defaults = {
             "l2_norm_clip": 1.0,
             "noise_multiplier": 0.5,
@@ -132,16 +200,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
             "batch_size": 25,
             "num_microbatches": None,
             "learning_rate": 0.1,
-            "optimizer": tf_privacy.DPKerasSGDOptimizer,
+            "optimizer": tfp.DPKerasSGDOptimizer,
             "num_samples": 250,
             "epochs": 20,
+            "current_epsilon": 999,
         }
 
-        for key in defaults.keys():
-            if key in kwargs.keys():
+        for key, val in defaults.items():
+            if kwargs.get(key, "missing") != "missing":
                 setattr(self, key, kwargs[key])
             else:
-                setattr(self, key, defaults[key])
+                setattr(self, key, val)
 
         if self.batch_size == 0:
             print("batch_size should not be 0 - division by zero")
@@ -165,10 +234,7 @@ def dp_epsilon_met(
             epochs=epochs,
             delta=self.delta,
         )
-        if privacy[0] < self.min_epsilon:
-            ok = True
-        else:
-            ok = False
+        ok = privacy[0] < self.min_epsilon
         return ok, privacy[0]
 
     def check_epsilon(
@@ -180,149 +246,65 @@ def check_epsilon(
         msg = ""
         if batch_size == 0:
             print(get_reporting_string(name="division_by_zero"))
-            #("Division by zero setting batch_size =1")
             batch_size = 1
-
-        ok, self.current_epsilon = self.dp_epsilon_met(
+        (
+            ok,
+            self.current_epsilon,  # pylint: disable=attribute-defined-outside-init
+        ) = self.dp_epsilon_met(
             num_examples=num_samples, batch_size=batch_size, epochs=epochs
         )
 
-        if ok:
-            msg = get_reporting_string(name="dp_requirements_met",
-                                       current_epsilon=self.current_epsilon,
-                                       num_samples=num_samples, batch_size=batch_size,
-                                       epochs=epochs)
-
-            #(
-            #    "The requirements for DP are met, "
-            #    f"current epsilon is: {self.current_epsilon}."
-            #    "Calculated from the parameters:  "
-            #    f"Num Samples = {num_samples}, "
-            #    f"batch_size = {batch_size}, epochs = {epochs}.\n"
-            #)
-        if not ok:
-            msg = get_reporting_string(name="dp_requirements_not_met",
-                                       current_epsilon=self.current_epsilon,
-                                       num_samples=num_samples,
-                                       batch_size=batch_size,
-                                       epochs=epochs)
-            #(
-            #    f"The requirements for DP are not met, "
-            #    f"current epsilon is: {self.current_epsilon}.\n"
-            #    f"To attain recommended DP the following parameters can be changed:  "
-            #    f"Num Samples = {num_samples},"
-            #    f"batch_size = {batch_size},"
-            #    f"epochs = {epochs}.\n"
-            #)
+        key_name = "dp_requirements_met" if ok else "dp_requirements_not_met"
+        get_reporting_string(
+            name=key_name,
+            current_epsilon=self.current_epsilon,
+            num_samples=num_samples,
+            batch_size=batch_size,
+            epochs=epochs,
+        )
         print(msg)
         return ok, msg
 
-    def check_optimizer_is_DP(self, optimizer) -> Tuple[bool, str]:
-        DPused = False
-        reason = "None"
-        if "_was_dp_gradients_called" not in optimizer.__dict__:
-            reason = get_reporting_string(name="no_dp_gradients_key")
-            #(
-            #    "optimizer does not contain key _was_dp_gradients_called"
-            #    " so is not DP."
-            #)
-            DPused = False
-        else:
-            reason = get_reporting_string(name="found_dp_gradients_key")
-            #(
-            #    "optimizer does  contain key _was_dp_gradients_called"
-            #    " so should be DP."
-            #)
-            DPused = True
-        return DPused, reason
-
-    def check_DP_used(self, optimizer) -> Tuple[bool, str]:
-        DPused = False
-        reason = "None"
-        if "_was_dp_gradients_called" not in optimizer.__dict__:
-            reason = get_reporting_string(name="no_dp_gradients_key")
-            #(
-            #    "optimizer does not contain key _was_dp_gradients_called "
-            #    "so is not DP."
-            #)
-            DPused = False
-        elif optimizer._was_dp_gradients_called == False:
-            reason = get_reporting_string(name="changed_opt_no_fit")
-            #"optimizer has been changed but fit() has not been rerun."
-            DPused = False
-        elif optimizer._was_dp_gradients_called == True:
-            reason = get_reporting_string(name="dp_optimizer_run")
-            #(
-            #    " value of optimizer._was_dp_gradients_called is True, "
-            #    "so DP variant of optimizer has been run"
-            #)
-            DPused = True
-        else:
-            reason = get_reporting_string(name="unrecognised_combination")
-            #"unrecognised combination"
-            DPused = False
-
-        return DPused, reason
-
-    def check_optimizer_allowed(self, optimizer) -> Tuple[bool, str]:
-        disclosive = True
-        reason = "None"
-        allowed_optimizers = [
-            "tensorflow_privacy.DPKerasAdagradOptimizer",
-            "tensorflow_privacy.DPKerasAdamOptimizer",
-            "tensorflow_privacy.DPKerasSGDOptimizer",
-        ]
-        print(f"{str(self.optimizer)}")
-        if self.optimizer in allowed_optimizers:
-            disclosive = False
-            reason = get_reporting_string(name="optimizer_allowed",
-                                          optimizer=self.optimizer)
-            #f"optimizer {self.optimizer} allowed"
-        else:
-            disclosive = True
-            reason = get_reporting_string(name="optimizer_not_allowed",
-                                          optimizer=self.optimizer)
-            #f"optimizer {self.optimizer} is not allowed"
-
-        return reason, disclosive
-
     def compile(
         self, optimizer=None, loss="categorical_crossentropy", metrics=["accuracy"]
-    ):
-
+    ):  # pylint:disable=dangerous-default-value)
+        """
+        Replaces the optimiser with a DP variant if needed and creates the
+        necessary DP params in the opt and loss dict, then calls tf compile.
+        Allow None as default value for optimizer param because we explicitly
+        deal with it.
+        """
         replace_message = get_reporting_string(name="warn_possible_disclosure_risk")
-        #"WARNING: model parameters may present a disclosure risk"
+        # "WARNING: model parameters may present a disclosure risk"
         using_DP_SGD = get_reporting_string(name="using_dp_sgd")
-        #"Changed parameter optimizer = 'DPKerasSGDOptimizer'"
+        # "Changed parameter optimizer = 'DPKerasSGDOptimizer'"
         Using_DP_Adagrad = get_reporting_string(name="using_dp_adagrad")
-        #"Changed parameter optimizer = 'DPKerasAdagradOptimizer'"
+        # "Changed parameter optimizer = 'DPKerasAdagradOptimizer'"
         using_DP_Adam = get_reporting_string(name="using_dp_adam")
-        #"Changed parameter optimizer = 'DPKerasAdamOptimizer'"
+        # "Changed parameter optimizer = 'DPKerasAdamOptimizer'"
 
         optimizer_dict = {
-            None: (using_DP_SGD, tf_privacy.DPKerasSGDOptimizer),
-            tf_privacy.DPKerasSGDOptimizer: ("", tf_privacy.DPKerasSGDOptimizer),
-            tf_privacy.DPKerasAdagradOptimizer: (
-                "",
-                tf_privacy.DPKerasAdagradOptimizer,
-            ),
-            tf_privacy.DPKerasAdamOptimizer: ("", tf_privacy.DPKerasAdamOptimizer),
+            None: (using_DP_SGD, tfp.DPKerasSGDOptimizer),
+            tfp.DPKerasSGDOptimizer: ("", tfp.DPKerasSGDOptimizer),
+            tfp.DPKerasAdagradOptimizer: ("", tfp.DPKerasAdagradOptimizer),
+            tfp.DPKerasAdamOptimizer: ("", tfp.DPKerasAdamOptimizer),
             "Adagrad": (
                 replace_message + Using_DP_Adagrad,
-                tf_privacy.DPKerasAdagradOptimizer,
+                tfp.DPKerasAdagradOptimizer,
             ),
-            "Adam": (replace_message + using_DP_Adam, tf_privacy.DPKerasAdamOptimizer),
-            "SGD": (replace_message + using_DP_SGD, tf_privacy.DPKerasSGDOptimizer),
+            "Adam": (replace_message + using_DP_Adam, tfp.DPKerasAdamOptimizer),
+            "SGD": (replace_message + using_DP_SGD, tfp.DPKerasSGDOptimizer),
         }
 
-        val = optimizer_dict.get(self.optimizer, "unknown")
+        val = optimizer_dict.get(optimizer, "unknown")
         if val == "unknown":
             opt_msg = using_DP_SGD
-            opt_used = tf_privacy.DPKerasSGDOptimizer
+            opt_used = tfp.DPKerasSGDOptimizer
         else:
             opt_msg = val[0]
             opt_used = val[1]
 
+        self.optimizer = opt_used  # pylint: disable=attribute-defined-outside-init
         opt = opt_used(
             l2_norm_clip=self.l2_norm_clip,
             noise_multiplier=self.noise_multiplier,
@@ -331,13 +313,11 @@ def compile(
         )
 
         if len(opt_msg) > 0:
-            print(get_reporting_string(name="during_compilation",
-                                       opt_msg=opt_msg))
-                #f"During compilation: {opt_msg}")
+            print(get_reporting_string(name="during_compilation", opt_msg=opt_msg))
 
         super().compile(opt, loss, metrics)
 
-    def fit(
+    def fit(  # pylint:disable=too-many-arguments
         self,
         X: Any,
         Y: Any,
@@ -346,18 +326,19 @@ def fit(
         batch_size: int,
         refine_epsilon: bool = False,
     ) -> Any:
-        """Over-rides the tensorflow fit() method with some
-        extra functionality:
-         (i) records number of samples for checking DP epsilon values
-         (ii) does an automatic epsilon check and reports
-         (iia) if user sets refine_epsilon = true, retiurn w ithout fitting the model
-         (iii) then calls the tensorflow fit() function
-         (iv) finally makes a saved copy of the newly fitted model
+        """
+        Overrides the tensorflow fit() method with some extra functionality:
+        (i) records number of samples for checking DP epsilon values.
+        (ii) does an automatic epsilon check and reports.
+        (iia) if user sets refine_epsilon = true, return without fitting the model.
+        (iii) then calls the tensorflow fit() function.
+        (iv) finally makes a saved copy of the newly fitted model.
         """
 
-        self.num_samples = X.shape[0]
-        self.epochs = epochs
-        self.batch_size = batch_size
+        # pylint can't cope whoe we first declared these via a dict :(
+        self.num_samples = X.shape[0]  # pylint: disable=attribute-defined-outside-init
+        self.epochs = epochs  # pylint: disable=attribute-defined-outside-init
+        self.batch_size = batch_size  # pylint: disable=attribute-defined-outside-init
         # make sure you are passing keywords through - but also checking batch size epochs
         ok, msg = self.check_epsilon(X.shape[0], batch_size, epochs)
 
@@ -366,32 +347,31 @@ def fit(
         if refine_epsilon:
             print(
                 "Not continuing with fitting model, "
-                "as return epsilon was above max recomneded value, "
+                "as return epsilon was above max recommended value, "
                 "and user set refine_epsilon= True"
             )
             return None
-        else:
-            returnval = super().fit(
-                X,
-                Y,
-                validation_data=validation_data,
-                epochs=epochs,
-                batch_size=batch_size,
-            )
 
-            # make a saved copy for later analysis
-            if not os.path.exists("tfsaves"):
-                os.mkdir("tfsaves")
-            self.save("tfsaves/fit_model.tf")
-            self.saved_was_dpused, self.saved_reason = self.check_DP_used(
-                self.optimizer
-            )
-            self.saved_epsilon = self.current_epsilon
-            return returnval
+        returnval = super().fit(
+            X,
+            Y,
+            validation_data=validation_data,
+            epochs=epochs,
+            batch_size=batch_size,
+        )
+
+        # make a saved copy for later analysis
+        if not os.path.exists("tfsaves"):
+            os.mkdir("tfsaves")
+        self.save("tfsaves/fit_model.tf")
+        # pylint: disable=attribute-defined-outside-init
+        self.saved_was_dpused, self.saved_reason = check_DP_used(self.optimizer)
+        self.saved_epsilon = self.current_epsilon
+        return returnval
 
     def posthoc_check(self, verbose: bool = True) -> Tuple[str, bool]:
         """Checks whether model should be considered unsafe
-        foer exanmple, has been changed since fit() was last run,
+        foer example, has been changed since fit() was last run,
         or does not meet DP policy
         """
 
@@ -402,64 +382,62 @@ def posthoc_check(self, verbose: bool = True) -> Tuple[str, bool]:
             "tfsaves/requested_model.tf",
         )
         if not models_same:
-            print(get_report_string(name="recommend_not_release",
-                                    msg=same_msg))
-            #f"Recommendation is not to release because {same_msg}.\n")
+            print(get_reporting_string(name="recommend_not_release", msg=same_msg))
+            # f"Recommendation is not to release because {same_msg}.\n")
             return same_msg, True
 
         # was a dp-enbled optimiser provided?
-        allowed, allowedmessage = self.check_optimizer_allowed(self.optimizer)
+        allowed, allowedmessage = check_optimizer_allowed(self.optimizer)
         if not allowed:
-            print(get_report_string(name="recommend_not_release",
-                                    msg=allowedmessage))
-                #f"Recommendation is not to release because {allowedmessage}.\n")
+            print(
+                get_reporting_string(name="recommend_not_release", msg=allowedmessage)
+            )
+            # f"Recommendation is not to release because {allowedmessage}.\n")
             return allowedmessage, True
 
         # was the dp-optimiser used during fit()
-        dpused, dpusedmessage = self.check_DP_used(self.optimizer)
+        dpused, dpusedmessage = check_DP_used(self.optimizer)
         if not dpused:
-            print(get_report_string(name="recommend_not_release",
-                                    msg=dpusedmessage))
-                #f"Recommendation is not to release because {dpusedmessage}.\n")
+            print(get_reporting_string(name="recommend_not_release", msg=dpusedmessage))
+            # f"Recommendation is not to release because {dpusedmessage}.\n")
             return dpusedmessage, True
 
-        # is this the same as the values immediately after fit()?
+        # have values been changed since saved  immediately after fit()?
         if (
             dpused != self.saved_was_dpused
             or dpusedmessage != self.saved_reason
             or self.saved_epsilon != self.current_epsilon
         ):
-                  return get_reporting_string(name="opt_config_changed"), True
-            #"Optimizer config has been changed since training.", True
+            return get_reporting_string(name="opt_config_changed"), True
 
-        # if so what was the value of epsilon achieved
-        eps_met, current_epsilon = self.dp_epsilon_met(
+        # if not what was the value of epsilon achieved
+        eps_met, cur_eps = self.dp_epsilon_met(
             num_examples=self.num_samples,
             batch_size=self.batch_size,
             epochs=self.epochs,
         )
         if not eps_met:
-                dpepsilonmessage = get_reporting_string(name="epsilon_above_normal")
-                #(
-                #  f"WARNING: epsilon {current_epsilon} "
-                #"is above normal max recommended value.\n"
-                #"Discussion with researcher needed.\n"
-                #)
-                print(get_reporting_string(name="recommend_further_discussion",
-                                       msg=dpepsilonmessage))
-                #f"Recommendation is further discussion needed " f"{dpepsilonmessage}.\n"
-                return dpepsilonmessage, True
-        else:
+            dpepsilonmessage = get_reporting_string(
+                name="epsilon_above_normal",
+                current_epsilon=cur_eps,
+            )
+            if verbose:
+                print(
+                    get_reporting_string(
+                        name="recommend_further_discussion", msg=dpepsilonmessage
+                    )
+                )
+            return dpepsilonmessage, True
+
+        # passed all the tests!!
+        if verbose:
             print(get_reporting_string(name="recommend_allow_release"))
-            #"Recommendation is to allow release.\n")
-            dpepsilonmessage = get_reporting_string(name="allow_release_eps_below_max")
-            #(
-            #"Recommendation: Allow release.\n"
-            #f"Epsilon vale of model {current_epsilon} "
-            #"is below normal max recommended value.\n"
-            #)
 
-            return dpepsilonmessage, False
+        dpepsilonmessage = get_reporting_string(
+            name="allow_release_eps_below_max", current_epsilon=cur_eps
+        )
+
+        return dpepsilonmessage, False
 
     def save(self, name: str = "undefined") -> None:
         """Writes model to file in appropriate format.
@@ -488,14 +466,14 @@ def save(self, name: str = "undefined") -> None:
         while self.model_save_file == "undefined":
             self.model_save_file = input(
                 get_reporting_string(name="input_filename_with_extension")
-                #"Please input a name with extension for the model to be saved."
+                # "Please input a name with extension for the model to be saved."
             )
 
         thename = self.model_save_file.split(".")
         # print(f'in save(), parsed filename is {thename}')
         if len(thename) == 1:
             print(get_reporting_string(name="filename_must_indicate_type"))
-            #"file name must indicate type as a suffix")
+            # "file name must indicate type as a suffix")
         else:
             suffix = self.model_save_file.split(".")[-1]
 
@@ -509,16 +487,21 @@ def save(self, name: str = "undefined") -> None:
                         save_format=suffix,
                     )
 
-                except Exception as er:
-                    print(get_reporting_string(name="error_saving_file",
-                    suffix=suffix, er=er))
-                    #f"saving as a {suffix} file gave this error message:  {er}")
+                except Exception as er:  # pylint:disable=broad-except
+                    print(
+                        get_reporting_string(
+                            name="error_saving_file", suffix=suffix, er=er
+                        )
+                    )
+                    # f"saving as a {suffix} file gave this error message:  {er}")
             else:
-                print(get_reporting_string(name="suffix_not_supported_for_type",
-                                           model_type=self.model_type))
-                    #f"{suffix} file suffix  not supported "
-                    #f"for models of type {self.model_type}.\n"
-
+                print(
+                    get_reporting_string(
+                        name="suffix_not_supported_for_type", model_type=self.model_type
+                    )
+                )
+                # f"{suffix} file suffix  not supported "
+                # f"for models of type {self.model_type}.\n"
 
     def load(self, name: str = "undefined") -> None:
         """
diff --git a/safemodel/classifiers/saferandomforestclassifier.py b/safemodel/classifiers/saferandomforestclassifier.py
index b627e6ab..71f161d3 100644
--- a/safemodel/classifiers/saferandomforestclassifier.py
+++ b/safemodel/classifiers/saferandomforestclassifier.py
@@ -10,29 +10,46 @@
 from sklearn.ensemble import RandomForestClassifier
 from sklearn.tree import DecisionTreeClassifier
 
-from ..safemodel import SafeModel
 from ..reporting import get_reporting_string
+from ..safemodel import SafeModel
 from .safedecisiontreeclassifier import decision_trees_are_equal
 
+# pylint: disable=too-many-ancestors,too-many-instance-attributes
 
 
 class SafeRandomForestClassifier(SafeModel, RandomForestClassifier):
     """Privacy protected Random Forest classifier."""
 
-    def __init__(self, **kwargs: Any) -> None:
+    def __init__(
+        self, **kwargs: Any
+    ) -> None:  # pylint: disable=too-many-instance-attributes
         """Creates model and applies constraints to params"""
         SafeModel.__init__(self)
-        self.basemodel_paramnames=[
-            'n_estimators','criterion','max_depth','min_samples_split',
-            'min_samples_leaf','min_weight_fraction_leaf','max_features',
-            'max_leaf_nodes','min_impurity_decrease','bootstrap',
-            'oob_score','n_jobs','random_state','verbose'
-            'warm_start','class_weight','ccp_alpha','max_samples']
-
-        the_kwds=dict()
-        for key,val in kwargs.items():
+        self.basemodel_paramnames = [
+            "n_estimators",
+            "criterion",
+            "max_depth",
+            "min_samples_split",
+            "min_samples_leaf",
+            "min_weight_fraction_leaf",
+            "max_features",
+            "max_leaf_nodes",
+            "min_impurity_decrease",
+            "bootstrap",
+            "oob_score",
+            "n_jobs",
+            "random_state",
+            "verbose",
+            "warm_start",
+            "class_weight",
+            "ccp_alpha",
+            "max_samples",
+        ]
+
+        the_kwds = {}
+        for key, val in kwargs.items():
             if key in self.basemodel_paramnames:
-                the_kwds[key]=val
+                the_kwds[key] = val
         RandomForestClassifier.__init__(self, **the_kwds)
         self.model_type: str = "RandomForestClassifier"
         super().preliminary_check(apply_constraints=True, verbose=True)
@@ -42,27 +59,30 @@ def __init__(self, **kwargs: Any) -> None:
             "base_estimator_",
         ]
         self.examine_seperately_items = ["base_estimator", "estimators_"]
+        self.k_anonymity = 0
 
-    def additional_checks(
+    def additional_checks(  # pylint: disable=too-many-nested-blocks,too-many-branches
         self, curr_separate: dict, saved_separate: dict
     ) -> tuple[str, str]:
-        """Random Forest-specific checks"""
-        msg=""
-        disclosive=False
-        ## call the super function to deal with any items that are lists
-        #msg, disclosive = super().additional_checks(curr_separate, saved_separate)
+        """Random Forest-specific checks
+        would benefit from refactoring into simpler blocks perhaps
+        """
+        msg = ""
+        disclosive = False
         # now the relevant random-forest specific things
         for item in self.examine_seperately_items:
             if item == "base_estimator":
                 try:
                     the_type = type(self.base_estimator)
                     if not isinstance(self.saved_model["base_estimator_"], the_type):
-                        msg += get_reporting_string(name="warn_fitted_fitted_different_base")
-                        #"Warning: model was fitted with different base estimator type.\n"
+                        msg += get_reporting_string(
+                            name="warn_fitted_fitted_different_base"
+                        )
+                        # "Warning: model was fitted with different base estimator type.\n"
                         disclosive = True
                 except AttributeError:
                     msg += get_reporting_string(name="error_model_not_fitted")
-                    #"Error: model has not been fitted to data.\n"
+                    # "Error: model has not been fitted to data.\n"
                     disclosive = True
 
             elif item == "estimators_":
@@ -70,28 +90,29 @@ def additional_checks(
                 if curr_separate[item] == "Absent" and saved_separate[item] == "Absent":
                     disclosive = True
                     msg += get_reporting_string(name="error_model_not_fitted")
-                    #"Error: model has not been fitted to data.\n"
+                    # "Error: model has not been fitted to data.\n"
 
                 elif curr_separate[item] == "Absent":
                     disclosive = True
                     msg += get_reporting_string(name="trees_removed")
-                    "Error: current version of model has had trees removed after fitting.\n"
 
                 elif saved_separate[item] == "Absent":
                     disclosive = True
                     msg += get_reporting_string(name="trees_edited")
-                    #"Error: current version of model has had trees manually edited.\n"
+                    # "Error: current version of model has had trees manually edited.\n"
 
                 else:
                     try:
                         num1 = len(curr_separate[item])
                         num2 = len(saved_separate[item])
                         if num1 != num2:
-                            msg += get_reporting_string(name="different_num_estimators", num1=num1, num2=num2)
-                            #(
+                            msg += get_reporting_string(
+                                name="different_num_estimators", num1=num1, num2=num2
+                            )
+                            # (
                             #    f"Fitted model has {num2} estimators "
                             #    f"but requested version has {num1}.\n"
-                            #)
+                            # )
                             disclosive = False
                         else:
                             for idx in range(num1):
@@ -100,26 +121,29 @@ def additional_checks(
                                 )
                                 if not same:
                                     disclosive = True
-                                    msg += get_reporting_string(name="forest_estimators_differ", idx=idx)
-                                    #f"Forest base estimators {idx} differ."
+                                    msg += get_reporting_string(
+                                        name="forest_estimators_differ", idx=idx
+                                    )
+                                    # f"Forest base estimators {idx} differ."
                                     msg += msg2
 
-                    except BaseException as error:
-                        msg += get_reporting_string(name="unable_to_check_item", item=item)
-                        #(
-                        #    "In SafeRandomForest.additional_checks: "
-                        #    f"Unable to check {item} as an exception occurred: {error}.\n"
-                        #)
+                    except BaseException as error:  # pylint: disable=broad-except
+                        msg += get_reporting_string(
+                            name="unable_to_check_item", item=item, error=error
+                        )
                         same = False
 
             elif isinstance(curr_separate[item], DecisionTreeClassifier):
                 diffs_list = list(diff(curr_separate[item], saved_separate[item]))
                 if len(diffs_list) > 0:
                     disclosive = True
-                    msg += get_reporting_string(name="structure_differences", item=item, diffs_list=diffs_list)
-                    #f"structure {item} has {len(diffs_list)} differences: {diffs_list}"
+                    msg += get_reporting_string(
+                        name="structure_differences", item=item, diffs_list=diffs_list
+                    )
+                    # f"structure {item} has {len(diffs_list)} differences: {diffs_list}"
         return msg, disclosive
 
+    # pylint: disable=arguments-differ
     def fit(self, x: np.ndarray, y: np.ndarray) -> None:
         """Do fit and then store model dict"""
         super().fit(x, y)
diff --git a/safemodel/classifiers/safesvc.py b/safemodel/classifiers/safesvc.py
index d0f56a61..78dbc3af 100644
--- a/safemodel/classifiers/safesvc.py
+++ b/safemodel/classifiers/safesvc.py
@@ -17,7 +17,7 @@ class SafeSVC(SafeModel, DPSVC):
     def __init__(self, C=1.0, gamma="scale", dhat=1000, eps=10, **kwargs) -> None:
         """Initialises a differentially private SVC."""
         SafeModel.__init__(self)
-        DPSVC.__init__(self,C=C,gamma=gamma,dhat=dhat,eps=eps,**kwargs)
+        DPSVC.__init__(self, C=C, gamma=gamma, dhat=dhat, eps=eps, **kwargs)
         self.model_type: str = "SVC"
         self.ignore_items = [
             "model_save_file",
@@ -29,25 +29,30 @@ def __init__(self, C=1.0, gamma="scale", dhat=1000, eps=10, **kwargs) -> None:
             "weights",
             "noisy_weights",
         ]
-        self.examine_seperately_items = [
-                        "platt_transform",
-                        "svc"
-        ]
+        self.examine_seperately_items = ["platt_transform", "svc"]
 
-    def fit(self, x: np.ndarray, y: np.ndarray) -> None:
+    def fit(self, train_features: np.ndarray, train_labels: np.ndarray) -> None:
         """Do fit and then store model dict"""
-        super().fit(x, y)
+        super().fit(train_features, train_labels)
         self.saved_model = copy.deepcopy(self.__dict__)
 
     def additional_checks(
         self, curr_separate: dict, saved_separate: dict
     ) -> tuple[str, str]:
         """SVC specific checks"""
-        msg=""
-        disclosive=False
+        msg = ""
+        disclosive = False
         for item in self.examine_seperately_items:
-            diffs_list = list(diff(curr_separate[item].__dict__, saved_separate[item].__dict__))
+            diffs_list = list(
+                diff(curr_separate[item].__dict__, saved_separate[item].__dict__)
+            )
             if len(diffs_list) > 0:
                 disclosive = True
-                msg += f"structure {item} has {len(diffs_list)} differences: {diffs_list}"
+                if len(diffs_list) == 1:
+                    msg += f"structure {item} has one difference.\n"  #: {diffs_list}"
+                else:
+                    msg += (
+                        f"structure {item} has several differences.\n"  #: {diffs_list}"
+                    )
+
         return msg, disclosive
diff --git a/safemodel/classifiers/safetf.py b/safemodel/classifiers/safetf.py
index 125bb54a..a38845e1 100644
--- a/safemodel/classifiers/safetf.py
+++ b/safemodel/classifiers/safetf.py
@@ -1,22 +1,39 @@
+""" work in progress to allow use of the DPModel classes
+ Jim smith 2022
+ When ready, linting of  the imports will be enabled
+"""
+
+
+# pylint: disable=unused-import
+from typing import Any
+
 import tensorflow as tf
 import tensorflow_privacy as tf_privacy
+from tensorflow_privacy import DPModel
 
-from tensorflow.keras import Model as KerasModel
-from safemodel.safemodel import SafeModel
+# from tensorflow.keras import Model as KerasModel
 from tensorflow_privacy.privacy.analysis import compute_dp_sgd_privacy
 from tensorflow_privacy.privacy.optimizers import dp_optimizer_keras
 
-import tensorflow_privacy as tf_privacy
-from tensorflow_privacy import DPModel
-from typing import Any
+from safemodel.safemodel import SafeModel
 
 
 class Safe_tf_DPModel(SafeModel, DPModel):
-    """ Privacy Protected tensorflow_privacy DP-SGD subclass of Keras model"""
+    """Privacy Protected tensorflow_privacy DP-SGD subclass of Keras model"""
 
-    def __init__(l2_norm_clip:float, noise_multiplier:float, use_xla:bool=True, *args:any, **kwargs:any) ->None:
+    # remove comment once model starts to be populated
+    # pylint:disable=super-init-not-called
+    def __init__(
+        self,
+        l2_norm_clip: float,
+        noise_multiplier: float,
+        use_xla: bool,
+        *args: any,
+        **kwargs: any
+    ) -> None:
         """creates model and applies constraints to parameters"""
-        safemodel.__init__(self)
-        DPModel.__init__(self, **kwargs)
-        self.model_type: str = "tf_DPModel"
-        super().preliminary_check(apply_constraints=True, verbose=True)
+        # safemodel.__init__(self)
+        # DPModel.__init__(self, **kwargs)
+        # self.model_type: str = "tf_DPModel"
+        # super().preliminary_check(apply_constraints=True, verbose=True)
+        raise NotImplementedError
diff --git a/safemodel/reporting.py b/safemodel/reporting.py
index 52af4930..ae825c00 100644
--- a/safemodel/reporting.py
+++ b/safemodel/reporting.py
@@ -1,4 +1,6 @@
 """methods to producte standard reporting strings"""
+
+
 def get_reporting_string(**kwargs):
     """returns a standard formatted string from a diction of f-strings.
 
@@ -29,42 +31,41 @@ def get_reporting_string(**kwargs):
     # if no name is found return an error string
 
     inter_params = {
-        'attr'            : None,
-        'batch_size'      : None,
-        'current_epsilon' : None,
-        'cur_val'         : None,
-        'diffs_list'      : "",
-        'e'               : None,
-        'epochs'          : None,
-        'er'              : None,
-        'error'           : None,
-        'idx'             : None,
-        'item'            : None,
-        'key'             : None,
-        'layer'           : None,
-        'length'          : None,
-        'match'           : "",
-        'name'            : None,
-        'num1'            : None,
-        'num2'            : None,
-        'num_samples'     : None,
-        'model_type'      : None,
-        'msg'             : None,
-        'operator'        : None,
-        'optimizer'       : None,
-        'opt_msg'         : None,
-        'suffix'          : None,
-        'v1'              : None,
-        'v2'              : None,
-        'val'             : None
-
+        "attr": None,
+        "batch_size": None,
+        "current_epsilon": None,
+        "cur_val": None,
+        "diffs_list": "",
+        "e": None,
+        "epochs": None,
+        "er": None,
+        "error": None,
+        "idx": None,
+        "item": None,
+        "key": None,
+        "layer": None,
+        "length": None,
+        "match": "",
+        "name": None,
+        "num1": None,
+        "num2": None,
+        "num_samples": None,
+        "model_type": None,
+        "msg": None,
+        "operator": None,
+        "optimizer": None,
+        "opt_msg": None,
+        "suffix": None,
+        "v1": None,
+        "v2": None,
+        "val": None,
     }
     # initialise all the values from the keyword parameters
-    #check for parameters in keyword arguments.
-    #if keyword is found set dictionary item value to that
-    #provided by the keyword
-    #if keyword is not found dictionary item value remains None,
-    #and an error message is returned.
+    # check for parameters in keyword arguments.
+    # if keyword is found set dictionary item value to that
+    # provided by the keyword
+    # if keyword is not found dictionary item value remains None,
+    # and an error message is returned.
 
     for param, value in the_kwargs.items():
         inter_params[param] = value
@@ -75,91 +76,64 @@ def get_reporting_string(**kwargs):
     # A dictionary of f-strings follows
 
     report_string = {
-        'NULL': (
-            ''
-        ),
-
-        'less_than_min_value' :(
+        "NULL": (""),
+        "less_than_min_value": (
             f"- parameter {inter_params['key']} = {inter_params['cur_val']}"
             f" identified as less than the recommended min value of {inter_params['val']}."
         ),
-
-        'greater_than_max_value': (
+        "greater_than_max_value": (
             f"- parameter {inter_params['key']} = {inter_params['cur_val']}"
             f" identified as greater than the recommended max value of {inter_params['val']}."
         ),
-
-        'different_than_fixed_value': (
+        "different_than_fixed_value": (
             f"- parameter {inter_params['key']} = {inter_params['cur_val']}"
             f" identified as different than the recommended fixed value of {inter_params['val']}."
         ),
-
-        'different_than_reccomended_type': (
+        "different_than_reccomended_type": (
             f"- parameter {inter_params['key']} = {inter_params['cur_val']}"
             f" identified as different type to recommendation of {inter_params['val']}."
         ),
-
-        'change_param_type': (
+        "change_param_type": (
             f"\nChanged parameter type for {inter_params['key']} to {inter_params['val']}.\n"
         ),
-
-        'not_implemented_for_change': (
+        "not_implemented_for_change": (
             f"Nothing currently implemented to change type of parameter {inter_params['key']} "
             f"from {type(inter_params['cur_val']).__name__} to {inter_params['val']}.\n"
         ),
-
-
-        'changed_param_equal': (
+        "changed_param_equal": (
             f"\nChanged parameter {inter_params['key']} = {inter_params['val']}.\n"
         ),
-        'unknown_operator' : (
+        "unknown_operator": (
             f"- unknown operator in parameter specification {inter_params['operator']}"
         ),
-
-        'warn_possible_disclosure_risk': (
+        "warn_possible_disclosure_risk": (
             "WARNING: model parameters may present a disclosure risk:\n"
         ),
-
-        'within_recommended_ranges': (
+        "within_recommended_ranges": (
             "Model parameters are within recommended ranges.\n"
         ),
-
-        'error_not_called_fit':(
+        "error_not_called_fit": (
             "Error: user has not called fit() method or has deleted saved values."
         ),
-
-        'recommend_do_not_release':(
-            "Recommendation: Do not release."
-        ),
-
-
-        'layer_configs_differ': (
+        "recommend_do_not_release": ("Recommendation: Do not release."),
+        "layer_configs_differ": (
             f"Layer {inter_params['layer']} configs differ in {inter_params['length']} places:\n"
         ),
-
-        'error_reloading_model_v1': (
+        "error_reloading_model_v1": (
             f"Error re-loading  model from {inter_params['v1']}:  {inter_params['e']}"
-
         ),
-
-
-        'error_reloading_model_v2': (
+        "error_reloading_model_v2": (
             f"Error re-loading  model from {inter_params['v2']}: {inter_params['e']}"
         ),
-
-        'division_by_zero': (
-            "Division by zero setting batch_size =1"
-        ),
-
-        'dp_requirements_met' : (
+        "division_by_zero": ("Division by zero setting batch_size =1"),
+        "dp_requirements_met": (
             "The requirements for DP are met, "
             f"current epsilon is: {inter_params['current_epsilon']}."
             "Calculated from the parameters:  "
             f"Num Samples = {inter_params['num_samples']}, "
             f"batch_size = {inter_params['batch_size']}, epochs = {inter_params['epochs']}.\n"
         ),
-
-        'dp_requirements_not_met': (
+        "dp_requirements_not_met": (
             f"The requirements for DP are not met, "
             f"current epsilon is: {inter_params['current_epsilon']}.\n"
             f"To attain recommended DP the following parameters can be changed:  "
@@ -167,181 +141,108 @@ def get_reporting_string(**kwargs):
             f"batch_size = {inter_params['batch_size']},"
             f"epochs = {inter_params['epochs']}.\n"
         ),
-
-        'basic_params_differ': (
+        "basic_params_differ": (
             f"Warning: basic parameters differ in {inter_params['length']} places:\n"
         ),
-
-        'unable_to_check' : (
+        "unable_to_check": (
             f"Unable to check as an exception occurred: {inter_params['error']}"
         ),
-
-
-        'neither_tree_trained': (
-            "neither tree trained"
-        ),
-
-        'tree1_not_trained': (
-            "tree1 not trained"
-        ),
-
-        'tree2_not_trained': (
-            "tree2 not trained"
-        ),
-
-        'internal_attribute_differs' : (
+        "neither_tree_trained": ("neither tree trained"),
+        "tree1_not_trained": ("tree1 not trained"),
+        "tree2_not_trained": ("tree2 not trained"),
+        "internal_attribute_differs": (
             f"internal tree attribute {inter_params['attr']} differs\n"
         ),
-
-        'exception_occurred' : (
-            f"An exception occurred: {inter_params['error']}"
-        ),
-
-        'unexpected_item': (
+        "exception_occurred": (f"An exception occurred: {inter_params['error']}"),
+        "unexpected_item": (
             "unexpected item in curr_seperate dict "
             " passed by generic additional checks."
         ),
-
-        'warn_fitted_different_base' : (
+        "warn_fitted_different_base": (
             "Warning: model was fitted with different base estimator type.\n"
         ),
-
-        'error_model_not_fitted' : (
-            "Error: model has not been fitted to data.\n"
-        ),
-
-        'trees_removed': (
+        "error_model_not_fitted": ("Error: model has not been fitted to data.\n"),
+        "trees_removed": (
             "Error: current version of model has had trees removed after fitting.\n"
         ),
-
-        'trees_edited' : (
+        "trees_edited": (
             "Error: current version of model has had trees manually edited.\n"
         ),
-
-        'different_num_estimators' : (
+        "different_num_estimators": (
             f"Fitted model has {inter_params['num2']} estimators "
             f"but requested version has {inter_params['num1']}.\n"
         ),
-
-        'forest_estimators_differ': (
+        "forest_estimators_differ": (
             f"Forest base estimators {inter_params['idx']} differ."
         ),
-
-        'unable_to_check_item' : (
+        "unable_to_check_item": (
             "In SafeRandomForest.additional_checks: "
             f"Unable to check {inter_params['item']} as an exception occurred:"
             f"{inter_params['error']}.\n"
         ),
-
-        'structure_differences': (
+        "structure_differences": (
             f"structure {inter_params['item']} has "
             f"{len(inter_params['diffs_list'])} differences: "
             f"{inter_params['diffs_list']}"
         ),
-
-        'no_dp_gradients_key' : (
-            "optimizer does not contain key _was_dp_gradients_called"
-            " so is not DP."
+        "no_dp_gradients_key": (
+            "optimizer does not contain key _was_dp_gradients_called" " so is not DP."
         ),
-
-        'found_dp_gradients_key' : (
-            "optimizer does  contain key _was_dp_gradients_called"
-            " so should be DP."
+        "found_dp_gradients_key": (
+            "optimizer does  contain key _was_dp_gradients_called" " so should be DP."
         ),
-
-        'changed_opt_no_fit' : (
+        "changed_opt_no_fit": (
             "optimizer has been changed but fit() has not been rerun."
         ),
-
-        'dp_optimizer_run' :(
-                " value of optimizer._was_dp_gradients_called is True, "
-                "so DP variant of optimizer has been run"
+        "dp_optimizer_run": (
+            " value of optimizer._was_dp_gradients_called is True, "
+            "so DP variant of optimizer has been run"
         ),
-
-        'unrecognised_combin ation' : (
-            "unrecognised combination"
-        ),
-
-        'optimizer_allowed' : (
-            f"optimizer {inter_params['optimizer']} allowed"
-        ),
-
-        'optimizer_not_allowed' : (
+        "unrecognised_combin ation": ("unrecognised combination"),
+        "optimizer_allowed": (f"optimizer {inter_params['optimizer']} allowed"),
+        "optimizer_not_allowed": (
             f"optimizer {inter_params['optimizer']} is not allowed"
         ),
-
-        'using_dp_sgd' : (
-            "Changed parameter optimizer = 'DPKerasSGDOptimizer'"
-        ),
-
-        'using_dp_adagrad' : (
-            "Changed parameter optimizer = 'DPKerasAdagradOptimizer'"
-        ),
-
-        'using_dp_adam' : (
-            "Changed parameter optimizer = 'DPKerasAdamOptimizer'"
-        ),
-
-        'during_compilation' : (
-            f"During compilation: {inter_params['opt_msg']}"
-        ),
-
-        'recommend_not_release' : (
+        "using_dp_sgd": ("Changed parameter optimizer = 'DPKerasSGDOptimizer'"),
+        "using_dp_adagrad": ("Changed parameter optimizer = 'DPKerasAdagradOptimizer'"),
+        "using_dp_adam": ("Changed parameter optimizer = 'DPKerasAdamOptimizer'"),
+        "during_compilation": (f"During compilation: {inter_params['opt_msg']}"),
+        "recommend_not_release": (
             f"Recommendation is not to release because {inter_params['msg']}.\n"
         ),
-
-        'error_saving_file' : (
+        "error_saving_file": (
             f"saving as a {inter_params['suffix']} "
             f"file gave this error message:  {inter_params['er']}"
         ),
-
-        'loading_from_unsupported': (
+        "loading_from_unsupported": (
             f"loading from a {inter_params['suffix']} "
             "file is currently not supported"
         ),
-
-        'opt_config_changed' : (
-            "Optimizer config has been changed since training."
-        ),
-
-        'epsilon_above_normal' : (
+        "opt_config_changed": ("Optimizer config has been changed since training."),
+        "epsilon_above_normal": (
             f"WARNING: epsilon {inter_params['current_epsilon']} "
             "is above normal max recommended value.\n"
             "Discussion with researcher needed.\n"
         ),
-
-        'recommend_further_discussion' : (
-            f"Recommendation is further discussion needed "
-            f"{inter_params['msg']}.\n"
+        "recommend_further_discussion": (
+            f"Recommendation is further discussion needed " f"{inter_params['msg']}.\n"
         ),
-
-        'recommend_allow_release' : (
-            "Recommendation is to allow release.\n"
-        ),
-
-        'allow_release_eps_below_max' : (
+        "recommend_allow_release": ("Recommendation is to allow release.\n"),
+        "allow_release_eps_below_max": (
             "Recommendation: Allow release.\n"
             f"Epsilon vale of model {inter_params['current_epsilon']} "
             "is below normal max recommended value.\n"
         ),
-
-        'input_filename_with_extension': (
+        "input_filename_with_extension": (
             "Please input a name with extension for the model to be saved."
         ),
-
-        'filename_must_indicate_type' : (
-            "file name must indicate type as a suffix"
-        ),
-
-        'suffix_not_supported_for_type' : (
+        "filename_must_indicate_type": ("file name must indicate type as a suffix"),
+        "suffix_not_supported_for_type": (
             f"{inter_params['suffix']} file suffix  not supported "
             f"for models of type {inter_params['model_type']}.\n"
-        )
-
+        ),
     }
 
-
     # return the correct formatted string
 
-
-    return report_string[inter_params['name']]
+    return report_string[inter_params["name"]]
diff --git a/safemodel/safemodel.py b/safemodel/safemodel.py
index 92d9fa64..482948a2 100644
--- a/safemodel/safemodel.py
+++ b/safemodel/safemodel.py
@@ -3,31 +3,32 @@
 from __future__ import annotations
 
 import copy
+import datetime
 import getpass
 import json
 import logging
+import os
 import pathlib
 import pickle
-import os
 from typing import Any
-import datetime
-import tensorflow as tf
-
 
 import joblib
+import tensorflow as tf
 from dictdiffer import diff
 
-from attacks import attribute_attack, worst_case_attack, dataset,report
-from attacks.likelihood_attack import LIRAAttackArgs, LIRAAttack # pylint: disable = import-error
+from attacks import attribute_attack, dataset, report, worst_case_attack
+from attacks.likelihood_attack import (  # pylint: disable = import-error
+    LIRAAttack,
+    LIRAAttackArgs,
+)
 
+# pylint : disable=too-many-branches
 from .reporting import get_reporting_string
 
 logger = logging.getLogger(__file__)
 logger.setLevel(logging.DEBUG)
 
 
-
-
 def check_min(key: str, val: Any, cur_val: Any) -> tuple[str, bool]:
     """Checks minimum value constraint.
 
@@ -55,27 +56,30 @@ def check_min(key: str, val: Any, cur_val: Any) -> tuple[str, bool]:
 
 
     """
-    if isinstance(cur_val,(int,float)):
+    if isinstance(cur_val, (int, float)):
         if cur_val < val:
             disclosive = True
-            print(f'key = {key}')
-            msg = get_reporting_string(name="less_than_min_value",
-                                       key=key, cur_val=cur_val, val=val)
+            print(f"key = {key}")
+            msg = get_reporting_string(
+                name="less_than_min_value", key=key, cur_val=cur_val, val=val
+            )
             print(msg)
-            #(
+            # (
             #    f"- parameter {key} = {cur_val}"
             #    f" identified as less than the recommended min value of {val}."
-            #)
+            # )
         else:
             disclosive = False
             msg = ""
         return msg, disclosive
 
     disclosive = True
-    msg = get_reporting_string(name="different_than_recommended_type",
-                               key=key, cur_val=cur_val, val=val)
+    msg = get_reporting_string(
+        name="different_than_recommended_type", key=key, cur_val=cur_val, val=val
+    )
     return msg, disclosive
 
+
 def check_max(key: str, val: Any, cur_val: Any) -> tuple[str, bool]:
     """Checks maximum value constraint.
 
@@ -103,23 +107,25 @@ def check_max(key: str, val: Any, cur_val: Any) -> tuple[str, bool]:
 
 
     """
-    if isinstance(cur_val,(int,float)):
+    if isinstance(cur_val, (int, float)):
         if cur_val > val:
             disclosive = True
-            msg = get_reporting_string(name="greater_than_max_value", key=key,
-                                       cur_val=cur_val, val=val)
-            #(
-            #f"- parameter {key} = {cur_val}"
-            #f" identified as greater than the recommended max value of {val}."
-            #)
+            msg = get_reporting_string(
+                name="greater_than_max_value", key=key, cur_val=cur_val, val=val
+            )
+            # (
+            # f"- parameter {key} = {cur_val}"
+            # f" identified as greater than the recommended max value of {val}."
+            # )
         else:
             disclosive = False
             msg = ""
         return msg, disclosive
 
     disclosive = True
-    msg = get_reporting_string(name="different_than_recommended_type",
-                               key=key, cur_val=cur_val, val=val)
+    msg = get_reporting_string(
+        name="different_than_recommended_type", key=key, cur_val=cur_val, val=val
+    )
     return msg, disclosive
 
 
@@ -154,12 +160,13 @@ def check_equal(key: str, val: Any, cur_val: Any) -> tuple[str, bool]:
     """
     if cur_val != val:
         disclosive = True
-        msg = get_reporting_string(name="different_than_fixed_value", key=key, cur_val=cur_val,
-                                   val=val)
-        #(
+        msg = get_reporting_string(
+            name="different_than_fixed_value", key=key, cur_val=cur_val, val=val
+        )
+        # (
         #    f"- parameter {key} = {cur_val}"
         #    f" identified as different than the recommended fixed value of {val}."
-        #)
+        # )
     else:
         disclosive = False
         msg = ""
@@ -194,19 +201,20 @@ def check_type(key: str, val: Any, cur_val: Any) -> tuple[str, bool]:
     """
     if type(cur_val).__name__ != val:
         disclosive = True
-        msg = get_reporting_string(name="different_than_recommended_type",
-                                   key=key, cur_val=cur_val, val=val)
-        #(
+        msg = get_reporting_string(
+            name="different_than_recommended_type", key=key, cur_val=cur_val, val=val
+        )
+        # (
         #    f"- parameter {key} = {cur_val}"
         #    f" identified as different type to recommendation of {val}."
-        #)
+        # )
     else:
         disclosive = False
         msg = ""
     return msg, disclosive
 
 
-class SafeModel: # pylint: disable = too-many-instance-attributes
+class SafeModel:  # pylint: disable = too-many-instance-attributes
     """Privacy protected model base class.
 
     Attributes
@@ -261,7 +269,7 @@ def __init__(self) -> None:
         self.model_save_file: str = "None"
         self.ignore_items: list[str] = []
         self.examine_seperately_items: list[str] = []
-        self.basemodel_paramnames=[]
+        self.basemodel_paramnames = []
         self.filename: str = "None"
         self.researcher: str = "None"
         try:
@@ -269,23 +277,21 @@ def __init__(self) -> None:
         except (ImportError, KeyError, OSError):
             self.researcher = "unknown"
 
-
-
-    def get_params(self,deep=True):
+    def get_params(self, deep=True):
         """gets dictionary of parameter values
         restricted to those expected by base classifier.
         """
-        the_params={}
-        for key,val in self.__dict__.items():
-            if key  in self.basemodel_paramnames:
-                the_params[key]=val
+        the_params = {}
+        for key, val in self.__dict__.items():
+            if key in self.basemodel_paramnames:
+                the_params[key] = val
         if deep:
-            pass #not implemented yet
+            pass  # not implemented yet
         return the_params
 
-
-
-    def save(self, name: str = "undefined") -> None:
+    def save(
+        self, name: str = "undefined"
+    ) -> None:  # pylint: disable=too-many-branches
         """Writes model to file in appropriate format.
 
         Parameters
@@ -308,7 +314,6 @@ def save(self, name: str = "undefined") -> None:
         possible back door into attacks.
         """
 
-
         self.model_save_file = name
         while self.model_save_file == "undefined":
             self.model_save_file = input(
@@ -352,7 +357,9 @@ def save(self, name: str = "undefined") -> None:
                     )
 
                 except (ImportError, NotImplementedError) as exception_err:
-                    print(f"saving as a {suffix} file gave this error message:  {exception_err}")
+                    print(
+                        f"saving as a {suffix} file gave this error message:  {exception_err}"
+                    )
             else:
                 print(
                     f"{suffix} file suffix currently not supported "
@@ -412,22 +419,23 @@ def __apply_constraints(
             if (val == "int") and (type(cur_val).__name__ == "float"):
                 self.__dict__[key] = int(self.__dict__[key])
                 msg = get_reporting_string(name="change_param_type", key=key, val=val)
-                #f"\nChanged parameter type for {key} to {val}.\n"
+                # f"\nChanged parameter type for {key} to {val}.\n"
             elif (val == "float") and (type(cur_val).__name__ == "int"):
                 self.__dict__[key] = float(self.__dict__[key])
                 msg = get_reporting_string(name="change_param_type", key=key, val=val)
-                #f"\nChanged parameter type for {key} to {val}.\n"
+                # f"\nChanged parameter type for {key} to {val}.\n"
             else:
-                msg = get_reporting_string(name="not_implemented_for_change", key=key,
-                                           cur_val=cur_val, val=val)
-                #(
+                msg = get_reporting_string(
+                    name="not_implemented_for_change", key=key, cur_val=cur_val, val=val
+                )
+                # (
                 #    f"Nothing currently implemented to change type of parameter {key} "
                 #    f"from {type(cur_val).__name__} to {val}.\n"
-                #)
+                # )
         else:
             setattr(self, key, val)
             msg = get_reporting_string(name="changed_param_equal", key=key, val=val)
-            #f"\nChanged parameter {key} = {val}.\n"
+            # f"\nChanged parameter {key} = {val}.\n"
         return msg
 
     def __check_model_param(
@@ -450,8 +458,10 @@ def __check_model_param(
         elif operator == "is_type":
             msg, disclosive = check_type(key, val, cur_val)
         else:
-            msg = get_reporting_string(name="unknown_operator", key=key, val=val, cur_val=cur_val)
-            #f"- unknown operator in parameter specification {operator}"
+            msg = get_reporting_string(
+                name="unknown_operator", key=key, val=val, cur_val=cur_val
+            )
+            # f"- unknown operator in parameter specification {operator}"
         if apply_constraints and disclosive:
             msg += self.__apply_constraints(operator, key, val, cur_val)
         return msg, disclosive
@@ -515,11 +525,15 @@ def preliminary_check(
         """
         disclosive: bool = False
         msg: str = ""
+        notok_start = get_reporting_string(name="warn_possible_disclosure_risk")
+        ok_start = get_reporting_string(name="within_recommended_ranges")
         rules: dict = self.__get_constraints()
         for rule in rules:
             operator = rule["operator"]
             if operator == "and":
-                temp_msg, temp_disc = self.__check_model_param_and(rule, apply_constraints)
+                temp_msg, temp_disc = self.__check_model_param_and(
+                    rule, apply_constraints
+                )
             elif operator == "or":
                 temp_msg, temp_disc = self.__check_model_param_or(rule)
             else:
@@ -527,14 +541,12 @@ def preliminary_check(
             msg += temp_msg
             if temp_disc:
                 disclosive = True
+
         if disclosive:
-            start_msg = get_reporting_string(name="warn_possible_disclosure_risk")
-            msg = start_msg + msg
-            #"WARNING: model parameters may present a disclosure risk:\n" + msg
+            msg = notok_start + msg
         else:
-            msg = get_reporting_string(name="within_recommended_ranges")
-            msg += temp_msg
-            #"Model parameters are within recommended ranges.\n" + msg
+            msg = ok_start + msg
+
         if verbose:
             print(msg)
         return msg, disclosive
@@ -556,7 +568,9 @@ def get_current_and_saved_models(self) -> tuple[dict, dict]:
                     current_model[key] = copy.deepcopy(value)
                 except copy.Error as key_type:
                     logger.warning("%s cannot be copied", key)
-                    logger.warning("...%s error; %s", str(type(key_type)), str(key_type))
+                    logger.warning(
+                        "...%s error; %s", str(type(key_type)), str(key_type)
+                    )
             # logger.debug('...done')
         # logger.info('copied')
 
@@ -589,26 +603,26 @@ def examine_seperate_items(
         in both current and saved copies"""
         msg = ""
         disclosive = False
+
         for item in self.examine_seperately_items:
             if curr_vals[item] == "Absent" and saved_vals[item] == "Absent":
-                # not sure if this is necessarily disclosive
+                disclosive = True
                 msg += f"Note that item {item} missing from both versions"
 
-            elif curr_vals[item] == "Absent" and not saved_vals[item] == "Absent":
-                disclosive = True
+            if curr_vals[item] == "Absent" and not saved_vals[item] == "Absent":
                 msg += f"Error, item {item} present in  saved but not current model"
-            elif saved_vals[item] == "Absent" and not curr_vals[item] == "Absent":
+                disclosive = True
+
+            if saved_vals[item] == "Absent" and not curr_vals[item] == "Absent":
                 disclosive = True
                 msg += f"Error, item {item} present in current but not saved model"
-            else:  # ok, so can call mode-specific extra checks
-                msg2, disclosive2 = self.additional_checks(curr_vals, saved_vals)
-                if len(msg2) > 0:
-                    msg += msg2
-                if disclosive2:
-                    disclosive = True
+
+        if not disclosive:  # ok, so can call mode-specific extra checks
+            msg, disclosive = self.additional_checks(curr_vals, saved_vals)
+
         return msg, disclosive
 
-    def posthoc_check(self) -> tuple[str, bool]:
+    def posthoc_check(self) -> tuple[str, bool]:  # pylint: disable=too-many-branches
         """Checks whether model has been interfered with since fit() was last run"""
 
         disclosive = False
@@ -617,9 +631,9 @@ def posthoc_check(self) -> tuple[str, bool]:
         current_model, saved_model = self.get_current_and_saved_models()
         if len(saved_model) == 0:
             msg = get_reporting_string(name="error_not_called_fit")
-            #"Error: user has not called fit() method or has deleted saved values."
+            # "Error: user has not called fit() method or has deleted saved values."
             msg += get_reporting_string(name="recommend_do_not_release")
-            #"Recommendation: Do not release."
+            # "Recommendation: Do not release."
             disclosive = True
 
         else:
@@ -640,10 +654,11 @@ def posthoc_check(self) -> tuple[str, bool]:
             if len(match) > 0:
                 disclosive = True
 
-                msg += get_reporting_string(name="basic_params_differ",
-                                            match=match, length=(len(match)))
+                msg += get_reporting_string(
+                    name="basic_params_differ", match=match, length=(len(match))
+                )
 
-                #f"Warning: basic parameters differ in {len(match)} places:\n"
+                # f"Warning: basic parameters differ in {len(match)} places:\n"
                 for this_match in match:
                     if this_match[0] == "change":
                         msg += f"parameter {this_match[1]} changed from {this_match[2][1]} "
@@ -661,7 +676,7 @@ def posthoc_check(self) -> tuple[str, bool]:
 
         return msg, disclosive
 
-    def additional_checks(
+    def additional_checks(  # pylint: disable=too-many-branches
         self, curr_separate: dict, saved_separate: dict
     ) -> tuple[str, bool]:
 
@@ -694,7 +709,6 @@ def additional_checks(
 
         """
 
-
         msg = ""
         disclosive = False
         for item in self.examine_seperately_items:
@@ -721,10 +735,11 @@ def additional_checks(
                             disclosive = True
                             break
 
-
         return msg, disclosive
 
-    def request_release(self, filename: str = "undefined",data_obj:dataset.Data=None) -> None:
+    def request_release(
+        self, filename: str = "undefined", data_obj: dataset.Data = None
+    ) -> None:  # pylint: disable=too-many-branches
         """Saves model to filename specified and creates a report for the TRE
         output checkers.
 
@@ -779,34 +794,35 @@ def request_release(self, filename: str = "undefined",data_obj:dataset.Data=None
                 output["recommendation"] = "Do not allow release"
                 output["reason"] = msg_prel + msg_post
 
-
             ##Run attacks programmatically if possible
             if data_obj is not None:
-                #make filenames and save a copy of the data
-                with open(os.path.splitext(filename)[0] +"_data.pickle", 'wb') as handle:
+                # make filenames and save a copy of the data
+                with open(
+                    os.path.splitext(filename)[0] + "_data.pickle", "wb"
+                ) as handle:
                     pickle.dump(data_obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
 
-                for attack_name in ['worst_case','lira','attribute']:
-                    output[f"{attack_name}_results"]=  self.run_attack(data_obj,
-                                      attack_name,
-                                      f"{os.path.splitext(filename)[0]}_{attack_name}_res"
-                                      )
-
-
+                for attack_name in ["worst_case", "lira", "attribute"]:
+                    output[f"{attack_name}_results"] = self.run_attack(
+                        data_obj,
+                        attack_name,
+                        f"{os.path.splitext(filename)[0]}_{attack_name}_res",
+                    )
 
             now = datetime.datetime.now()
             output["timestamp"] = str(now.strftime("%Y-%m-%d %H:%M:%S"))
 
-            json_str = json.dumps(output, indent=4,cls=report.NumpyArrayEncoder)
+            json_str = json.dumps(output, indent=4, cls=report.NumpyArrayEncoder)
             outputfilename = self.researcher + "_checkfile.json"
             with open(outputfilename, "a", encoding="utf-8") as file:
                 file.write(json_str)
 
-
-    def run_attack(self,
-                   data_obj:dataset.Data=None,
-                   attack_name:str="worst_case",
-                   filename:str="undefined")->dict:
+    def run_attack(
+        self,
+        data_obj: dataset.Data = None,
+        attack_name: str = "worst_case",
+        filename: str = "undefined",
+    ) -> dict:
 
         """Runs a specified attack on the trained model and saves a report to file
 
@@ -833,7 +849,8 @@ def run_attack(self,
         Single Attribute Inference: attributes
         """
         if attack_name == "worst_case":
-            attack_args = worst_case_attack.WorstCaseAttackArgs(n_reps=10,
+            attack_args = worst_case_attack.WorstCaseAttackArgs(
+                n_reps=10,
                 # number of baseline (dummy) experiments to do
                 n_dummy_reps=1,
                 # Threshold to determine significance of things
@@ -844,43 +861,45 @@ def run_attack(self,
                 # Proportion of data to use as a test set for the attack model;
                 test_prop=0.5,
                 # Report name is None - don't make json or pdf files
-                report_name=None
+                report_name=None,
             )
             attack_obj = worst_case_attack.WorstCaseAttack(attack_args)
-            attack_obj.attack(dataset=data_obj,target_model=self)
+            attack_obj.attack(dataset=data_obj, target_model=self)
             output = attack_obj.make_report()
-            metadata = output['metadata']
+            metadata = output["metadata"]
 
-        elif attack_name=="lira":
-            args = LIRAAttackArgs(n_shadow_models=100, report_name="lira_example_report")
+        elif attack_name == "lira":
+            args = LIRAAttackArgs(
+                n_shadow_models=100, report_name="lira_example_report"
+            )
             attack_obj = LIRAAttack(args)
-            attack_obj.attack(data_obj,self)
-            output = attack_obj.make_report() # also makes .pdf and .json files
-            metadata = output['metadata']
+            attack_obj.attack(data_obj, self)
+            output = attack_obj.make_report()  # also makes .pdf and .json files
+            metadata = output["metadata"]
 
         elif attack_name == "attribute":
-            attack_args = attribute_attack.AttributeAttackArgs(report_name="aia_example")
+            attack_args = attribute_attack.AttributeAttackArgs(
+                report_name="aia_example"
+            )
             attack_obj = attribute_attack.AttributeAttack(attack_args)
             attack_obj.attack(data_obj, self)
             output = attack_obj.make_report()
             metadata = output["metadata"]
 
         else:
-            metadata= {}
+            metadata = {}
             metadata["outcome"] = "unrecognised attack type requested"
 
-        print(f'attack {attack_name}, metadata {metadata}')
-
+        print(f"attack {attack_name}, metadata {metadata}")
 
         try:
-            with open(f'{filename}.json', 'w',encoding='utf-8') as fp:
-                json.dump(metadata, fp,cls=report.NumpyArrayEncoder)
+            with open(f"{filename}.json", "w", encoding="utf-8") as fp:
+                json.dump(metadata, fp, cls=report.NumpyArrayEncoder)
         except TypeError:
-            print(f'couldnt serialise metadata {metadata} for attack {attack_name}')
+            print(f"couldn't serialise metadata {metadata} for attack {attack_name}")
 
         return metadata
 
-
     def __str__(self) -> str:
         """Returns string with model description."""
         return self.model_type + " with parameters: " + str(self.__dict__)


Test patch:
diff --git a/tests/datasets/nursery_as_dataset.pickle b/tests/datasets/nursery_as_dataset.pickle
new file mode 100644
index 00000000..9915f6d1
Binary files /dev/null and b/tests/datasets/nursery_as_dataset.pickle differ
diff --git a/tests/test_attacks.py b/tests/test_attacks.py
new file mode 100644
index 00000000..b7af7e5d
--- /dev/null
+++ b/tests/test_attacks.py
@@ -0,0 +1,16 @@
+"""Jim Smith October 2022"""
+import pytest
+
+from attacks import attack, dataset
+from safemodel.classifiers import SafeDecisionTreeClassifier
+
+
+def test_superclass():
+    """Test that the exceptions are raised if the superclass is called in error"""
+    my_attack = attack.Attack()
+    dataset_obj = dataset.Data()
+    target_obj = SafeDecisionTreeClassifier()
+    with pytest.raises(NotImplementedError):
+        my_attack.attack(target_obj, dataset_obj)
+    with pytest.raises(NotImplementedError):
+        print(str(my_attack))  # .__str__()
diff --git a/tests/test_attacks_via_safemodel.py b/tests/test_attacks_via_safemodel.py
new file mode 100644
index 00000000..86e1b024
--- /dev/null
+++ b/tests/test_attacks_via_safemodel.py
@@ -0,0 +1,172 @@
+"""
+Tests attacks called via safemodel classes
+uses a saubsmapled nursery dataset as this tests more of the attack code
+currently using random forests
+"""
+import os
+import pickle
+from pathlib import Path
+
+import numpy as np
+from sklearn.datasets import fetch_openml
+from sklearn.model_selection import train_test_split
+from sklearn.preprocessing import LabelEncoder, OneHotEncoder
+
+from attacks.dataset import Data
+from safemodel.classifiers import SafeDecisionTreeClassifier
+
+# pylint: disable=too-many-locals,bare-except,duplicate-code
+
+
+def cleanup_file(name: str):
+    """removes unwanted files or directory"""
+    if os.path.exists(name) and os.path.isfile(name):  # h5
+        os.remove(name)
+
+
+def get_nursery_dataset() -> Data:
+    """returns a randomly sampled 10+10% of
+    the nursery data set as a Data object
+    if needed fetches it from openml and saves. it
+
+    """
+
+    the_file = "tests/datasets/nursery_as_dataset.pickle"
+    save_locally = True
+    need_download = True
+
+    if Path(the_file).is_file():
+        try:
+            with open(the_file, "rb") as f:
+                the_data = pickle.load(f)
+            need_download = False
+        except:
+            need_download = True
+
+    if need_download:
+
+        nursery_data = fetch_openml(data_id=26, as_frame=True)
+        x = np.asarray(nursery_data.data, dtype=str)
+        y = np.asarray(nursery_data.target, dtype=str)
+        # change labels from recommend to priority for the two odd cases
+        num = len(y)
+        for i in range(num):
+            if y[i] == "recommend":
+                y[i] = "priority"
+
+        n_features = np.shape(x)[1]
+        indices: list[list[int]] = [
+            [0, 1, 2],  # parents
+            [3, 4, 5, 6, 7],  # has_nurs
+            [8, 9, 10, 11],  # form
+            [12, 13, 14, 15],  # children
+            [16, 17, 18],  # housing
+            [19, 20],  # finance
+            [21, 22, 23],  # social
+            [24, 25, 26],  # health
+        ]
+
+        # [Researcher] Split into training and test sets
+        # target model train / test split - these are strings
+        (x_train_orig, x_test_orig, y_train_orig, y_test_orig,) = train_test_split(
+            x,
+            y,
+            test_size=0.05,
+            stratify=y,
+            shuffle=True,
+        )
+
+        # now resample the training data reduce number of examples
+        _, x_train_orig, _, y_train_orig = train_test_split(
+            x_train_orig,
+            y_train_orig,
+            test_size=0.05,
+            stratify=y_train_orig,
+            shuffle=True,
+        )
+
+        # [Researcher] Preprocess dataset
+        # one-hot encoding of features and integer encoding of labels
+        label_enc = LabelEncoder()
+        feature_enc = OneHotEncoder()
+        x_train = feature_enc.fit_transform(x_train_orig).toarray()
+        y_train = label_enc.fit_transform(y_train_orig)
+        x_test = feature_enc.transform(x_test_orig).toarray()
+        y_test = label_enc.transform(y_test_orig)
+
+        # [TRE / Researcher] Wrap the data in a dataset object
+        the_data = Data()
+        the_data.name = "nursery"
+        the_data.add_processed_data(x_train, y_train, x_test, y_test)
+        the_data.add_raw_data(
+            x, y, x_train_orig, y_train_orig, x_test_orig, y_test_orig
+        )
+        for i in range(n_features):
+            the_data.add_feature(nursery_data.feature_names[i], indices[i], "onehot")
+
+    if need_download and save_locally:
+        # make directory if needed then save
+        output_file = Path(the_file)
+        output_file.parent.mkdir(exist_ok=True, parents=True)
+        with open(the_file, "wb") as f:
+            pickle.dump(the_data, f)
+
+    return the_data
+
+
+def test_run_attack_lira():
+    """calls the lira attack via safemodel"""
+    the_data = get_nursery_dataset()
+
+    # build a model
+    model = SafeDecisionTreeClassifier(random_state=1, max_depth=5)
+    model.fit(the_data.x_train, the_data.y_train)
+    _, disclosive = model.preliminary_check()
+    assert not disclosive
+
+    print(np.unique(the_data.y_test, return_counts=True))
+
+    print(np.unique(model.predict(the_data.x_test), return_counts=True))
+
+    fname = "delete-me"
+    metadata = model.run_attack(the_data, "lira", fname)
+    cleanup_file("delete-me.json")
+    assert len(metadata) > 0  # something has been added
+
+
+def test_run_attack_worstcase():
+    """calls the worst case attack via safemodel"""
+    the_data = get_nursery_dataset()
+
+    model = SafeDecisionTreeClassifier(random_state=1, max_depth=5)
+    model.fit(the_data.x_train, the_data.y_train)
+    _, disclosive = model.preliminary_check()
+    assert not disclosive
+
+    fname = "delete-me"
+    metadata = model.run_attack(the_data, "worst_case", fname)
+    cleanup_file("delete-me.json")
+    # cleanup_files("modelDOTrun_attack_output_worst_case.json")
+    assert len(metadata) > 0  # something has been added
+
+
+def test_run_attack_attribute():
+    """calls the attribute  attack via safemodel"""
+    the_data = get_nursery_dataset()
+
+    model = SafeDecisionTreeClassifier(random_state=1, max_depth=5)
+    model.fit(the_data.x_train, the_data.y_train)
+    _, disclosive = model.preliminary_check()
+    assert not disclosive
+
+    fname = "delete-me"
+    metadata = model.run_attack(the_data, "attribute", fname)
+    for outfile in [
+        "delete-me.json",
+        "aia_example.json",
+        "aia_example.pdf",
+        "aia_report_cat_frac.png",
+        "aia_report_cat_risk.png",
+    ]:
+        cleanup_file(outfile)
+    assert len(metadata) > 0  # something has been added
diff --git a/tests/test_data_interface.py b/tests/test_data_interface.py
index 93da1dee..378ee12e 100644
--- a/tests/test_data_interface.py
+++ b/tests/test_data_interface.py
@@ -1,33 +1,39 @@
-'''
+"""
 Test the data interface code
-'''
+"""
 
 import unittest
+
 import pandas as pd
-from preprocessing.loaders import get_data_sklearn, UnknownDataset  # pylint: disable = import-error
+
+from preprocessing.loaders import (  # pylint: disable = import-error
+    UnknownDataset,
+    get_data_sklearn,
+)
 
 
 class TestLoaders(unittest.TestCase):
-    '''
+    """
     Test the data loaders
-    '''
+    """
+
     def test_iris(self):
-        '''Nursery data'''
+        """Nursery data"""
         feature_df, target_df = get_data_sklearn("iris")
         self.assertIsInstance(feature_df, pd.DataFrame)
         self.assertIsInstance(target_df, pd.DataFrame)
 
     def test_unknown(self):
-        '''
+        """
         Test that a nonsense string raises the correct exception
-        '''
+        """
         with self.assertRaises(UnknownDataset):
             _, _ = get_data_sklearn("NONSENSE")
 
     def test_standard(self):
-        '''
+        """
         Test that standardisation creates standard features
-        '''
+        """
         feature_df, _ = get_data_sklearn("standard iris")
         for column in feature_df.columns:
             temp = feature_df[column].mean()
@@ -36,9 +42,9 @@ def test_standard(self):
             self.assertAlmostEqual(temp, 1.0)
 
     def test_minmax(self):
-        '''
+        """
         Test the minmax scaling
-        '''
+        """
         feature_df, _ = get_data_sklearn("minmax iris")
         for column in feature_df.columns:
             temp = feature_df[column].min()
diff --git a/tests/test_metrics.py b/tests/test_metrics.py
index 73e4b87e..de9e3c60 100644
--- a/tests/test_metrics.py
+++ b/tests/test_metrics.py
@@ -1,79 +1,83 @@
-'''
+"""
 Test the metrics
-'''
+"""
 
 import unittest
+
 import numpy as np
-from attacks.metrics import get_metrics, _tpr_at_fpr
+
+from attacks.metrics import _tpr_at_fpr, get_metrics
 from attacks.mia_extremecase import min_max_disc
 
 # pylint: disable = invalid-name
 
 PREDICTED_CLASS = np.array([0, 1, 0, 0, 1, 1])
 TRUE_CLASS = np.array([0, 0, 0, 1, 1, 1])
-PREDICTED_PROBS = np.array([
-    [0.9, 0.1],
-    [0.4, 0.6],
-    [0.8, 0.2],
-    [0.55, 0.45],
-    [0.1, 0.9],
-    [0.01, 0.99]
-])
+PREDICTED_PROBS = np.array(
+    [[0.9, 0.1], [0.4, 0.6], [0.8, 0.2], [0.55, 0.45], [0.1, 0.9], [0.01, 0.99]]
+)
+
 
 class DummyClassifier:
-    '''
+    """
     Mocks the predict and predict_proba methods
-    '''
+    """
+
     def predict(self, _):
-        '''
+        """
         return dummy predictions
-        '''
+        """
         return PREDICTED_CLASS
+
     def predict_proba(self, _):
-        '''
+        """
         return dummy predicted probabilities
-        '''
+        """
         return PREDICTED_PROBS
 
+
 class TestMetrics(unittest.TestCase):
-    '''
+    """
     Test the metrics with some dummy predictions
-    '''
+    """
+
     def test_metrics(self):
-        '''
+        """
         Test each individual metric with dummy data
-        '''
+        """
         clf = DummyClassifier()
         testX = []
         testy = TRUE_CLASS
         metrics = get_metrics(clf, testX, testy, permute_rows=False)
-        self.assertAlmostEqual(metrics['TPR'], 2 / 3)
-        self.assertAlmostEqual(metrics['FPR'], 1 / 3)
-        self.assertAlmostEqual(metrics['FAR'], 1 / 3)
-        self.assertAlmostEqual(metrics['TNR'], 2 / 3)
-        self.assertAlmostEqual(metrics['PPV'], 2 / 3)
-        self.assertAlmostEqual(metrics['NPV'], 2 / 3)
-        self.assertAlmostEqual(metrics['FNR'], 1 / 3)
-        self.assertAlmostEqual(metrics['ACC'], 4 / 6)
-        self.assertAlmostEqual(metrics['F1score'], (8 / 9) / (2 / 3 + 2 / 3))
-        self.assertAlmostEqual(metrics['Advantage'], 1 / 3)
-        self.assertAlmostEqual(metrics['AUC'], 8 / 9)
+        self.assertAlmostEqual(metrics["TPR"], 2 / 3)
+        self.assertAlmostEqual(metrics["FPR"], 1 / 3)
+        self.assertAlmostEqual(metrics["FAR"], 1 / 3)
+        self.assertAlmostEqual(metrics["TNR"], 2 / 3)
+        self.assertAlmostEqual(metrics["PPV"], 2 / 3)
+        self.assertAlmostEqual(metrics["NPV"], 2 / 3)
+        self.assertAlmostEqual(metrics["FNR"], 1 / 3)
+        self.assertAlmostEqual(metrics["ACC"], 4 / 6)
+        self.assertAlmostEqual(metrics["F1score"], (8 / 9) / (2 / 3 + 2 / 3))
+        self.assertAlmostEqual(metrics["Advantage"], 1 / 3)
+        self.assertAlmostEqual(metrics["AUC"], 8 / 9)
+
 
 class TestFPRatTPR(unittest.TestCase):
-    '''
+    """
     Test code that computes TPR at fixed FPR
-    '''
+    """
+
     def test_tpr(self):
-        '''Test tpr at fpr'''
+        """Test tpr at fpr"""
         y_true = TRUE_CLASS
         y_score = PREDICTED_PROBS[:, 1]
 
         tpr = _tpr_at_fpr(y_true, y_score, fpr=0)
-        self.assertAlmostEqual(tpr, 2/3)
+        self.assertAlmostEqual(tpr, 2 / 3)
         tpr = _tpr_at_fpr(y_true, y_score, fpr=0.001)
-        self.assertAlmostEqual(tpr, 2/3)
+        self.assertAlmostEqual(tpr, 2 / 3)
         tpr = _tpr_at_fpr(y_true, y_score, fpr=0.1)
-        self.assertAlmostEqual(tpr, 2/3)
+        self.assertAlmostEqual(tpr, 2 / 3)
         tpr = _tpr_at_fpr(y_true, y_score, fpr=0.4)
         self.assertAlmostEqual(tpr, 1)
         tpr = _tpr_at_fpr(y_true, y_score, fpr=1.0)
@@ -81,13 +85,14 @@ def test_tpr(self):
 
 
 class TestExtreme(unittest.TestCase):
-    '''
+    """
     Test the extreme metrics
-    '''
+    """
+
     def test_extreme_default(self):
-        '''
+        """
         Tets with the dummy data
-        '''
+        """
         pred_probs = DummyClassifier().predict_proba(None)[:, 1]
         maxd, mind, mmd, _ = min_max_disc(TRUE_CLASS, pred_probs)
 
@@ -99,9 +104,9 @@ def test_extreme_default(self):
         self.assertAlmostEqual(mmd, 1.0)
 
     def test_extreme_higer_prop(self):
-        '''
+        """
         Tets with the dummy data but increase proportion to 0.5
-        '''
+        """
         pred_probs = DummyClassifier().predict_proba(None)[:, 1]
         maxd, mind, mmd, _ = min_max_disc(TRUE_CLASS, pred_probs, x_prop=0.5)
 
diff --git a/tests/test_safedecisiontreeclassifier.py b/tests/test_safedecisiontreeclassifier.py
index 4e66fcbc..2d53e359 100644
--- a/tests/test_safedecisiontreeclassifier.py
+++ b/tests/test_safedecisiontreeclassifier.py
@@ -6,19 +6,27 @@
 import numpy as np
 from sklearn import datasets
 
+from safemodel import reporting
 from safemodel.classifiers import SafeDecisionTreeClassifier
 
 
 def get_data():
     """Returns data for testing."""
     iris = datasets.load_iris()
-    x = np.asarray(iris['data'], dtype=np.float64)
-    y = np.asarray(iris['target'], dtype=np.float64)
+    x = np.asarray(iris["data"], dtype=np.float64)
+    y = np.asarray(iris["target"], dtype=np.float64)
     x = np.vstack([x, (7, 2.0, 4.5, 1)])
     y = np.append(y, 4)
     return x, y
 
 
+def test_reporting():
+    """Check that getting report name requires name"""
+    correct_msg = "Error - get_reporting_string: No 'name' given"
+    msg = reporting.get_reporting_string()
+    assert msg == correct_msg
+
+
 def test_decisiontree_unchanged():
     """SafeDecisionTreeClassifier using unchanged values."""
     x, y = get_data()
diff --git a/tests/test_safekeras.py b/tests/test_safekeras.py
index bfee8269..3fa047f7 100644
--- a/tests/test_safekeras.py
+++ b/tests/test_safekeras.py
@@ -1,25 +1,40 @@
 """This module contains unit tests for SafeKerasModel."""
 
+import getpass
 import os
+import platform
 import shutil
-import getpass
 
 import numpy as np
 import tensorflow as tf
 from sklearn import datasets
 from sklearn.model_selection import train_test_split
-from tensorflow.keras.layers import Dense, Input # pylint: disable = import-error, no-name-in-module
+from tensorflow.keras.layers import (  # pylint: disable = import-error, no-name-in-module
+    Dense,
+    Input,
+)
 
-from safemodel.classifiers import SafeKerasModel
+from safemodel.classifiers import SafeKerasModel, safekeras
 
+EPOCHS = 1
 n_classes = 4
+# expected accuracy
+ACC = 0.3250 if platform.system() == "Darwin" else 0.3583333492279053
+
+
+def cleanup_file(name: str):
+    """removes unwanted files or directory"""
+    if os.path.exists(name) and os.path.isfile(name):  # h5
+        os.remove(name)
+    elif os.path.exists(name) and os.path.isdir(name):  # tf
+        shutil.rmtree(name)
 
 
 def get_data():
     """Returns data for testing."""
     iris = datasets.load_iris()
-    xall = np.asarray(iris['data'], dtype=np.float64)
-    yall = np.asarray(iris['target'], dtype=np.float64)
+    xall = np.asarray(iris["data"], dtype=np.float64)
+    yall = np.asarray(iris["target"], dtype=np.float64)
     xall = np.vstack([xall, (7, 2.0, 4.5, 1)])
     yall = np.append(yall, n_classes)
     X, Xval, y, yval = train_test_split(
@@ -48,7 +63,7 @@ def make_model():
         outputs=output,
         name="test",
         num_samples=X.shape[0],
-        epochs=10,
+        epochs=EPOCHS,
     )
 
     return model, X, y, Xval, yval
@@ -80,14 +95,14 @@ def test_second_keras_model_created():
         outputs=output,
         name="test",
         num_samples=X.shape[0],
-        epochs=10,
+        epochs=EPOCHS,
     )
     model2 = SafeKerasModel(
         inputs=input_data,
         outputs=output,
         name="test",
         num_samples=X.shape[0],
-        epochs=10,
+        epochs=EPOCHS,
     )
     rightname = "KerasModel"
     assert (
@@ -104,7 +119,7 @@ def test_keras_model_compiled_as_DP():
         from_logits=False, reduction=tf.losses.Reduction.NONE
     )
     model.compile(loss=loss, optimizer=None)
-    isDP, _ = model.check_optimizer_is_DP(model.optimizer)
+    isDP, _ = safekeras.check_optimizer_is_DP(model.optimizer)
     assert isDP, "failed check that optimizer is dP"
 
     right_epsilon = 20.363059561511612
@@ -123,19 +138,21 @@ def test_keras_basic_fit():
     )
     model.compile(loss=loss, optimizer=None)
 
-    isDP, msg = model.check_optimizer_is_DP(model.optimizer)
+    isDP, msg = safekeras.check_optimizer_is_DP(model.optimizer)
     assert isDP, "failed check that optimizer is dP"
 
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
-    DPused, msg = model.check_DP_used(model.optimizer)
+    DPused, msg = safekeras.check_DP_used(model.optimizer)
     assert (
         DPused
     ), "Failed check that DP version of optimiser was actually used in training"
 
     loss, acc = model.evaluate(X, y)
-    expected_accuracy = 0.3583333492279053
-    assert round(acc,6) == round(expected_accuracy,6), "failed check that accuracy is as expected"
+    expected_accuracy = ACC
+    assert round(acc, 6) == round(
+        expected_accuracy, 6
+    ), "failed check that accuracy is as expected"
 
     msg, disclosive = model.preliminary_check()
     correct_msg = "Model parameters are within recommended ranges.\n"
@@ -152,34 +169,26 @@ def test_keras_save_actions():
         from_logits=False, reduction=tf.losses.Reduction.NONE
     )
     model.compile(loss=loss, optimizer=None)
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
     # start with .tf and .h5 which should work
     names = ("safekeras.tf", "safekeras.h5")
     for name in names:
         # clear existing files
-        if os.path.exists(name) and os.path.isfile(name):  # h5
-            os.remove(name)
-        elif os.path.exists(name) and os.path.isdir(name):  # tf
-            shutil.rmtree(name)
+        cleanup_file(name)
         # save file
         model.save(name)
         assert os.path.exists(name), f"Failed test to save model as {name}"
         # clean up
-        if os.path.isfile(name):  # h5
-            os.remove(name)
-        elif os.path.isdir(name):
-            shutil.rmtree(name)
+        cleanup_file(name)
 
     # now other versions which should not
     names = ("safekeras.sav", "safekeras.pkl", "randomfilename")
     for name in names:
-        if os.path.exists(name):
-            os.remove(name)
+        cleanup_file(name)
         model.save(name)
         assert os.path.exists(name) is False, f"Failed test NOT to save model as {name}"
-        if os.path.exists(name):
-            os.remove(name)
+        cleanup_file(name)
 
 
 def test_keras_unsafe_l2_norm():
@@ -191,21 +200,23 @@ def test_keras_unsafe_l2_norm():
     )
     model.compile(loss=loss, optimizer=None)
 
-    isDP, msg = model.check_optimizer_is_DP(model.optimizer)
+    isDP, msg = safekeras.check_optimizer_is_DP(model.optimizer)
     assert isDP, "failed check that optimizer is dP"
 
     model.l2_norm_clip = 0.9
 
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
-    DPused, msg = model.check_DP_used(model.optimizer)
+    DPused, msg = safekeras.check_DP_used(model.optimizer)
     assert (
         DPused
     ), "Failed check that DP version of optimiser was actually used in training"
 
     loss, acc = model.evaluate(X, y)
-    expected_accuracy = 0.3583333492279053
-    assert round(acc,6) == round(expected_accuracy,6), "failed check that accuracy is as expected"
+    expected_accuracy = ACC
+    assert round(acc, 6) == round(
+        expected_accuracy, 6
+    ), "failed check that accuracy is as expected"
 
     msg, disclosive = model.preliminary_check()
     correct_msg = (
@@ -226,21 +237,23 @@ def test_keras_unsafe_noise_multiplier():
     )
     model.compile(loss=loss, optimizer=None)
 
-    isDP, msg = model.check_optimizer_is_DP(model.optimizer)
+    isDP, msg = safekeras.check_optimizer_is_DP(model.optimizer)
     assert isDP, "failed check that optimizer is dP"
 
     model.noise_multiplier = 1.0
 
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
-    DPused, msg = model.check_DP_used(model.optimizer)
+    DPused, msg = safekeras.check_DP_used(model.optimizer)
     assert (
         DPused
     ), "Failed check that DP version of optimiser was actually used in training"
 
     loss, acc = model.evaluate(X, y)
-    expected_accuracy = 0.3583333492279053
-    assert round(acc,6) == round(expected_accuracy,6), "failed check that accuracy is as expected"
+    expected_accuracy = ACC
+    assert round(acc, 6) == round(
+        expected_accuracy, 6
+    ), "failed check that accuracy is as expected"
 
     msg, disclosive = model.preliminary_check()
     correct_msg = (
@@ -252,6 +265,7 @@ def test_keras_unsafe_noise_multiplier():
     assert msg == correct_msg, "failed check params are within range"
     assert disclosive is True, "failed check disclosive is True"
 
+
 def test_keras_unsafe_min_epsilon():
     """SafeKeras using unsafe values."""
     model, X, y, Xval, yval = make_model()
@@ -261,21 +275,23 @@ def test_keras_unsafe_min_epsilon():
     )
     model.compile(loss=loss, optimizer=None)
 
-    isDP, msg = model.check_optimizer_is_DP(model.optimizer)
+    isDP, msg = safekeras.check_optimizer_is_DP(model.optimizer)
     assert isDP, "failed check that optimizer is dP"
 
     model.min_epsilon = 4
 
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
-    DPused, msg = model.check_DP_used(model.optimizer)
+    DPused, msg = safekeras.check_DP_used(model.optimizer)
     assert (
         DPused
     ), "Failed check that DP version of optimiser was actually used in training"
 
     loss, acc = model.evaluate(X, y)
-    expected_accuracy = 0.3583333492279053
-    assert round(acc,6) == round(expected_accuracy,6), "failed check that accuracy is as expected"
+    expected_accuracy = ACC
+    assert round(acc, 6) == round(
+        expected_accuracy, 6
+    ), "failed check that accuracy is as expected"
 
     msg, disclosive = model.preliminary_check()
     correct_msg = (
@@ -286,6 +302,7 @@ def test_keras_unsafe_min_epsilon():
     assert msg == correct_msg, "failed check correct warning message"
     assert disclosive is True, "failed check disclosive is True"
 
+
 def test_keras_unsafe_delta():
     """SafeKeras using unsafe values."""
     model, X, y, Xval, yval = make_model()
@@ -295,21 +312,23 @@ def test_keras_unsafe_delta():
     )
     model.compile(loss=loss, optimizer=None)
 
-    isDP, msg = model.check_optimizer_is_DP(model.optimizer)
+    isDP, msg = safekeras.check_optimizer_is_DP(model.optimizer)
     assert isDP, "failed check that optimizer is dP"
 
     model.delta = 1e-6
 
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
-    DPused, msg = model.check_DP_used(model.optimizer)
+    DPused, msg = safekeras.check_DP_used(model.optimizer)
     assert (
         DPused
     ), "Failed check that DP version of optimiser was actually used in training"
 
     loss, acc = model.evaluate(X, y)
-    expected_accuracy = 0.3583333492279053
-    assert round(acc,6) == round(expected_accuracy,6), "failed check that accuracy is as expected"
+    expected_accuracy = ACC
+    assert round(acc, 6) == round(
+        expected_accuracy, 6
+    ), "failed check that accuracy is as expected"
 
     msg, disclosive = model.preliminary_check()
     correct_msg = (
@@ -319,6 +338,7 @@ def test_keras_unsafe_delta():
     assert msg == correct_msg, "failed check params are within range"
     assert disclosive is True, "failed check disclosive is True"
 
+
 def test_keras_unsafe_batch_size():
     """SafeKeras using unsafe values."""
     model, X, y, Xval, yval = make_model()
@@ -328,21 +348,23 @@ def test_keras_unsafe_batch_size():
     )
     model.compile(loss=loss, optimizer=None)
 
-    isDP, msg = model.check_optimizer_is_DP(model.optimizer)
+    isDP, msg = safekeras.check_optimizer_is_DP(model.optimizer)
     assert isDP, "failed check that optimizer is dP"
 
     model.batch_size = 34
 
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
-    DPused, msg = model.check_DP_used(model.optimizer)
+    DPused, msg = safekeras.check_DP_used(model.optimizer)
     assert (
         DPused
     ), "Failed check that DP version of optimiser was actually used in training"
 
     loss, acc = model.evaluate(X, y)
-    expected_accuracy = 0.3583333492279053
-    assert round(acc,6) == round(expected_accuracy,6), "failed check that accuracy is as expected"
+    expected_accuracy = ACC
+    assert round(acc, 6) == round(
+        expected_accuracy, 6
+    ), "failed check that accuracy is as expected"
 
     msg, disclosive = model.preliminary_check()
     correct_msg = "Model parameters are within recommended ranges.\n"
@@ -359,21 +381,23 @@ def test_keras_unsafe_learning_rate():
     )
     model.compile(loss=loss, optimizer=None)
 
-    isDP, msg = model.check_optimizer_is_DP(model.optimizer)
+    isDP, msg = safekeras.check_optimizer_is_DP(model.optimizer)
     assert isDP, "failed check that optimizer is dP"
 
     model.learning_rate = 0.2
 
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
-    DPused, msg = model.check_DP_used(model.optimizer)
+    DPused, msg = safekeras.check_DP_used(model.optimizer)
     assert (
         DPused
     ), "Failed check that DP version of optimiser was actually used in training"
 
     loss, acc = model.evaluate(X, y)
-    expected_accuracy = 0.3583333492279053
-    assert round(acc,6) == round(expected_accuracy,6), "failed check that accuracy is as expected"
+    expected_accuracy = ACC
+    assert round(acc, 6) == round(
+        expected_accuracy, 6
+    ), "failed check that accuracy is as expected"
 
     msg, disclosive = model.preliminary_check()
     correct_msg = "Model parameters are within recommended ranges.\n"
@@ -381,6 +405,7 @@ def test_keras_unsafe_learning_rate():
     assert msg == correct_msg, "failed check warning message incorrect"
     assert disclosive is False, "failed check disclosive is false"
 
+
 def test_create_checkfile():
     """Test create checkfile"""
     # create, compile and train model
@@ -390,16 +415,13 @@ def test_create_checkfile():
         from_logits=False, reduction=tf.losses.Reduction.NONE
     )
     model.compile(loss=loss, optimizer=None)
-    model.fit(X, y, validation_data=(Xval, yval), epochs=10, batch_size=20)
+    model.fit(X, y, validation_data=(Xval, yval), epochs=EPOCHS, batch_size=20)
 
     # start with .tf and .h5 which should work
     names = ("safekeras.tf", "safekeras.h5")
     for name in names:
         # clear existing files
-        if os.path.exists(name) and os.path.isfile(name):  # h5
-            os.remove(name)
-        elif os.path.exists(name) and os.path.isdir(name):  # tf
-            shutil.rmtree(name)
+        cleanup_file(name)
         # save file
         model.save(name)
         assert os.path.exists(name), f"Failed test to save model as {name}"
@@ -409,10 +431,12 @@ def test_create_checkfile():
 
         researcher = getpass.getuser()
         outputfilename = researcher + "_checkfile.json"
-        assert os.path.exists(outputfilename), f"Failed test to save checkfile as {outputfilename}"
+        assert os.path.exists(
+            outputfilename
+        ), f"Failed test to save checkfile as {outputfilename}"
 
         # Using readlines()
-        with open(outputfilename, 'r', encoding='utf-8') as file1:
+        with open(outputfilename, "r", encoding="utf-8") as file1:
             lines = file1.readlines()
 
         count = 0
@@ -422,17 +446,12 @@ def test_create_checkfile():
             print(f"Line{count}: {line.strip()}")
 
         # clean up
-        if os.path.isfile(name):  # h5
-            os.remove(name)
-        elif os.path.isdir(name):
-            shutil.rmtree(name)
+        cleanup_file(name)
 
     # now other versions which should not
     names = ("safekeras.sav", "safekeras.pkl", "randomfilename")
     for name in names:
-        if os.path.exists(name):
-            os.remove(name)
+        cleanup_file(name)
         model.save(name)
         assert os.path.exists(name) is False, f"Failed test NOT to save model as {name}"
-        if os.path.exists(name):
-            os.remove(name)
+        cleanup_file(name)
diff --git a/tests/test_saferandomforestclassifier.py b/tests/test_saferandomforestclassifier.py
index 8dbd84a2..0fe52c3b 100644
--- a/tests/test_saferandomforestclassifier.py
+++ b/tests/test_saferandomforestclassifier.py
@@ -12,8 +12,8 @@
 def get_data():
     """Returns data for testing."""
     iris = datasets.load_iris()
-    x = np.asarray(iris['data'], dtype=np.float64)
-    y = np.asarray(iris['target'], dtype=np.float64)
+    x = np.asarray(iris["data"], dtype=np.float64)
+    y = np.asarray(iris["target"], dtype=np.float64)
     x = np.vstack([x, (7, 2.0, 4.5, 1)])
     y = np.append(y, 4)
     return x, y
diff --git a/tests/test_safesvc.py b/tests/test_safesvc.py
index 3a21976d..6bbdcf80 100644
--- a/tests/test_safesvc.py
+++ b/tests/test_safesvc.py
@@ -1,32 +1,37 @@
-'''
+"""
 Test the various models we have defined
-'''
+"""
 
 import unittest
-from sklearn.svm import SVC
-from sklearn import datasets
+
 import numpy as np
+from sklearn import datasets
+from sklearn.linear_model import LogisticRegression
+from sklearn.svm import SVC
+
 from safemodel.classifiers import SafeSVC
 
+
 def get_data():
     """Returns data for testing."""
     cancer = datasets.load_breast_cancer()
-    x = np.asarray(cancer['data'], dtype=np.float64)
-    y = np.asarray(cancer['target'], dtype=np.float64)
+    x = np.asarray(cancer["data"], dtype=np.float64)
+    y = np.asarray(cancer["target"], dtype=np.float64)
 
     return x, y
 
+
 class TestDPSVC(unittest.TestCase):
-    '''
+    """
     Test the differentially private SVC
-    '''
+    """
 
     def test_run(self):
-        '''
+        """
         Test the model runs
-        '''
+        """
         dpsvc = SafeSVC()
-        svc = SVC(kernel="rbf", gamma="scale", C=1., probability=True)
+        svc = SVC(kernel="rbf", gamma="scale", C=1.0, probability=True)
         x, y = get_data()
 
         dpsvc.fit(x, y)
@@ -44,3 +49,106 @@ def test_run(self):
 
         # Check that the two models have equal shape
         self.assertTupleEqual(dp_predprob.shape, sv_predprob.shape)
+
+    def test_svc_recommended(self):
+        """SafeSupportVectorClassifier using recommended values."""
+        x, y = get_data()
+        model = SafeSVC(gamma=1.0)
+        model.fit(x, y)
+        msg, disclosive = model.preliminary_check()
+        correct_msg = "Model parameters are within recommended ranges.\n"
+        assert msg == correct_msg
+        assert disclosive is False
+
+    def test_svc_khat(self):
+        """SafeSupportVectorClassifier khat method."""
+        x, y = get_data()
+        model = SafeSVC(gamma=1.0)
+        model.fit(x, y)
+        y_matrix = np.ones((len(y), 30))
+        assert len(y_matrix.shape) == 2
+        _ = model.k_hat_svm(x, y_matrix)
+
+    def test_svc_wrongdata(self):
+        """SafeSupportVectorClassifier with wrong datatypes."""
+        x, y = get_data()
+        model = SafeSVC(gamma=1.0)
+        # wrong y datatype
+        with self.assertRaises(Exception) as context:
+            model.fit(x, 1)
+        self.assertTrue("DPSCV needs np.ndarray inputs" in str(context.exception))
+
+        # wrong x datatype
+        with self.assertRaises(Exception) as context:
+            model.fit(1, 1)
+        self.assertTrue("DPSCV needs np.ndarray inputs" in str(context.exception))
+
+        # wrong label values
+        yplus = y + 5
+        errstr = "DP SVC can only handle binary classification"
+        with self.assertRaises(Exception) as context:
+            model.fit(x, yplus)
+        self.assertTrue(errstr in str(context.exception))
+
+    def test_svc_gamma_zero(self):
+        """SafeSupportVectorClassifier still makes predictions if we provide daft params"""
+        x, y = get_data()
+        model = SafeSVC(gamma=0.0, eps=0.0)
+        model.fit(x, y)
+        predictions = model.predict(x)
+        assert len(predictions) == len(x)
+
+    def test_svc_gamma_auto(self):
+        """SafeSupportVectorClassifier still makes predictions if we provide daft params"""
+        x, y = get_data()
+        model = SafeSVC(gamma="auto")
+        model.fit(x, y)
+        assert model.gamma == 1.0 / x.shape[1]
+
+    def test_svc_setparams(self):
+        """SafeSupportVectorClassifier using unchanged values."""
+        x, y = get_data()
+        model = SafeSVC(gamma=1.0)
+        model.fit(x, y)
+
+        newvals = {"gamma": 5, "eps": 0, "dhat": 999}
+        model.set_params(**newvals)
+        model.fit(x, y)
+        assert model.eps == 0
+        assert model.lambdaval == 0
+        assert model.dhat == 999
+        assert model.gamma == 5
+
+        # unknown
+        unrecognised = {"foo": 99}
+        model.set_params(**unrecognised)
+        # should log to file mewssage "Unsupported parameter: foo"
+
+    def test_svc_unchanged(self):
+        """SafeSupportVectorClassifier using unchanged values."""
+        x, y = get_data()
+        model = SafeSVC(gamma=1.0)
+        model.fit(x, y)
+
+        msg, disclosive = model.posthoc_check()
+        correct_msg = ""
+        assert msg == correct_msg
+        assert disclosive is False
+
+    def test_svc_nonstd_params_changed_postfit(self):
+        """SafeSupportVectorClassifier with params changed after fit."""
+        x, y = get_data()
+        model = SafeSVC(gamma=1.0)
+        model.fit(x, y)
+
+        # change values
+        model.svc = SVC().fit(x, y)  # make it disclosive
+        model.platt_transform = LogisticRegression()
+        msg, disclosive = model.posthoc_check()
+        correct_msg = (
+            "structure platt_transform has one difference.\n"
+            "structure svc has several differences.\n"
+        )
+
+        assert msg == correct_msg
+        assert disclosive is True
diff --git a/tests/test_safetf.py b/tests/test_safetf.py
new file mode 100644
index 00000000..69c02d1e
--- /dev/null
+++ b/tests/test_safetf.py
@@ -0,0 +1,14 @@
+"""Jim Smith 2022
+    Not currently implemented
+    """
+
+import pytest
+
+from safemodel.classifiers import safetf
+
+
+def test_Safe_tf_DPModel_l2_and_noise():
+    """Tests user is informed this is not implemented yet"""
+    with pytest.raises(NotImplementedError):
+        # with values for the l2 and noise params
+        safetf.Safe_tf_DPModel(1.5, 2.0, True)