Repository: ISISComputingGroup/lewis (language: python)

Issue:
get the documentation building again and set up action to deploy to github pages
This ticket is to get the docs building again (using sphinx?) and then to push it to github pages. we have a [workflow](https://github.com/ISISComputingGroup/reusable-workflows/blob/main/.github/workflows/sphinx.yml) for this so it's just enabling pages on this repo and adding back `conf.py` (it was removed recently so check commit history. 

### acceptance criteria
- [ ] docs can be built locally
- [ ] docs are pushed to github pages on `master`



Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index d02f1dc4..00000000
--- a/.flake8
+++ /dev/null
@@ -1,6 +0,0 @@
-[flake8]
-ignore = E501, E203, E402, W503
-# E501 & E203: Formatting handled by Black
-# E402 complains about imports not being at the top
-# W503 complains about splitting if across lines which conflicts with Black
-exclude = .git, docs, .tox
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
new file mode 100644
index 00000000..6aa7e93c
--- /dev/null
+++ b/.github/workflows/documentation.yml
@@ -0,0 +1,8 @@
+name: sphinx
+
+on: [push, pull_request, workflow_call]
+
+jobs:
+  call_sphinx_builder:
+    uses: ISISComputingGroup/reusable-workflows/.github/workflows/sphinx.yml@main
+    secrets: inherit
diff --git a/.gitignore b/.gitignore
index 33ed4e19..35a10c08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,10 @@
 .cache
 dist/
 build/
-docs/_build/
+doc/_build/
 lewis.egg-info
 .DS_Store
+.venv
+doc/generated
+_build
+.approval_tests_temp
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index 5e7a2d5d..00000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[settings]
-# Make it play nice with black
-multi_line_output=3
-include_trailing_comma=True
-line_length=88
-force_grid_wrap=0
-use_parentheses=True
-ensure_newline_before_comments=True
diff --git a/Jenkinsfile b/Jenkinsfile
deleted file mode 100644
index fdf03ca3..00000000
--- a/Jenkinsfile
+++ /dev/null
@@ -1,78 +0,0 @@
-@Library('ecdc-pipeline')
-import ecdcpipeline.ContainerBuildNode
-import ecdcpipeline.PipelineBuilder
-
-project = "lewis"
-
-container_build_nodes = [
-  'centos7': ContainerBuildNode.getDefaultContainerBuildNode('centos7-gcc11')
-]
-
-
-// Define number of old builds to keep.
-num_artifacts_to_keep = '1'
-
-// Set number of old builds to keep.
-properties([[
-  $class: 'BuildDiscarderProperty',
-  strategy: [
-    $class: 'LogRotator',
-    artifactDaysToKeepStr: '',
-    artifactNumToKeepStr: num_artifacts_to_keep,
-    daysToKeepStr: '',
-    numToKeepStr: num_artifacts_to_keep
-  ]
-]]);
-
-// Set periodic trigger at 3:56 every day.
-properties([
-  pipelineTriggers([cron('56 3 * * *')]),
-])
-
-pipeline_builder = new PipelineBuilder(this, container_build_nodes)
-pipeline_builder.activateEmailFailureNotifications()
-
-builders = pipeline_builder.createBuilders { container ->
-  pipeline_builder.stage("${container.key}: Checkout") {
-    dir(pipeline_builder.project) {
-      scm_vars = checkout scm
-    }
-    container.copyTo(pipeline_builder.project, pipeline_builder.project)
-  }  // stage
-
-  pipeline_builder.stage("${container.key}: Dependencies") {
-    container.sh """
-      which python
-      python --version
-      python -m pip install --user -r ${project}/requirements-dev.txt
-      python -m pip install --user 'requests<2.30.0'
-    """
-  } // stage
-
-   pipeline_builder.stage("${container.key}: Test") {
-      def test_output = "TestResults.xml"
-      container.sh """
-        pyenv local 3.7 3.8 3.9 3.10 3.11
-        cd ${project}
-        python -m tox -- --junitxml=${test_output}
-      """
-      container.copyFrom("${project}/${test_output}", ".")
-      xunit thresholds: [failed(unstableThreshold: '0')], tools: [JUnit(deleteOutputFiles: true, pattern: '*.xml', skipNoTestFiles: false, stopProcessingIfError: true)]
-    } // stage
-}  // createBuilders
-
-node {
-  dir("${project}") {
-    scm_vars = checkout scm
-  }
-
-  try {
-    parallel builders
-  } catch (e) {
-    throw e
-  }
-
-  // Delete workspace when build is done
-  cleanWs()
-}
-
diff --git a/README.md b/README.md
index 17ba074e..ab539ef1 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 Lewis - Let's write intricate simulators.
 
 Lewis is a Python framework for simulating hardware devices. It is
-compatible with Python 3.7 to 3.11.
+compatible with Python 3.10 to 3.11.
 
 It is currently not compatible with 3.12 as the asyncchat module has been removed from Python.
 We are going to fix this at some point.
@@ -10,7 +10,7 @@ We are going to fix this at some point.
 Lewis can be installed via pip or ran from source. See relevant usage sections of the docs for more details.
 
 Resources:
-[GitHub](https://github.com/ess-dmsc/lewis)
+[GitHub](https://github.com/ISISComputingGroup/lewis)
 [PyPI](https://pypi.python.org/pypi/lewis)
 
 Lewis was previously named "Plankton" but, due to a
diff --git a/doc/_api.rst b/doc/_api.rst
new file mode 100644
index 00000000..8d74436c
--- /dev/null
+++ b/doc/_api.rst
@@ -0,0 +1,11 @@
+:orphan:
+
+API
+===
+
+.. autosummary::
+   :toctree: generated
+   :template: custom-module-template.rst
+   :recursive:
+
+   lewis
diff --git a/doc/_templates/custom-module-template.rst b/doc/_templates/custom-module-template.rst
new file mode 100644
index 00000000..eb3edd1f
--- /dev/null
+++ b/doc/_templates/custom-module-template.rst
@@ -0,0 +1,40 @@
+
+
+{{ ('``' + fullname + '``') | underline }}
+
+{%- set filtered_members = [] %}
+{%- for item in members %}
+    {%- if item in functions + classes + exceptions + attributes %}
+        {% set _ = filtered_members.append(item) %}
+    {%- endif %}
+{%- endfor %}
+
+.. automodule:: {{ fullname }}
+    :members:
+    :show-inheritance:
+
+    {% block modules %}
+    {% if modules %}
+    .. rubric:: Submodules
+
+    .. autosummary::
+        :toctree:
+        :template: custom-module-template.rst
+        :recursive:
+    {% for item in modules %}
+        {{ item }}
+    {%- endfor %}
+    {% endif %}
+    {% endblock %}
+
+    {% block members %}
+    {% if filtered_members %}
+    .. rubric:: Members
+
+    .. autosummary::
+        :nosignatures:
+    {% for item in filtered_members %}
+        {{ item }}
+    {%- endfor %}
+    {% endif %}
+    {% endblock %}
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 00000000..6d539cb5
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+#
+# lewis documentation build configuration file, created by
+# sphinx-quickstart on Wed Nov  9 16:42:53 2016.
+import os
+import sys
+sys.path.insert(0, os.path.abspath("../lewis"))
+
+
+# -- General configuration ------------------------------------------------
+extensions = [
+    "myst_parser",
+    "sphinx.ext.autodoc",
+    # and making summary tables at the top of API docs
+    "sphinx.ext.autosummary",
+    # This can parse google style docstrings
+    "sphinx.ext.napoleon",
+    # For linking to external sphinx documentation
+    "sphinx.ext.intersphinx",
+    # Add links to source code in API docs
+    "sphinx.ext.viewcode",
+]
+templates_path = ["_templates"]
+# General information about the project.
+project = u"lewis"
+language = 'en'
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+# -- Options for HTML output ---------------------------------------------
+suppress_warnings =["docutils"]
+html_theme = "sphinx_rtd_theme"
+html_logo = "resources/logo/lewis-logo.png"
+
+autoclass_content = "both"
+myst_heading_anchors = 3
+
+napoleon_google_docstring = False
+napoleon_numpy_docstring = True
diff --git a/docs/developer_guide/developing_lewis.md b/doc/developer_guide/developing_lewis.md
similarity index 98%
rename from docs/developer_guide/developing_lewis.md
rename to doc/developer_guide/developing_lewis.md
index dd1b5fe5..173e9bf9 100644
--- a/docs/developer_guide/developing_lewis.md
+++ b/doc/developer_guide/developing_lewis.md
@@ -3,7 +3,7 @@
 Begin by checking-out the source from GitHub:
 
 ```
-(lewis-dev)$ git clone https://github.com/ess-dmsc/lewis.git
+(lewis-dev)$ git clone https://github.com/ISISComputingGroup/lewis.git
 ```
 
 To develop Lewis, it is strongly recommended to work in a dedicated virtual environment, otherwise
diff --git a/docs/developer_guide/framework_details.md b/doc/developer_guide/framework_details.md
similarity index 100%
rename from docs/developer_guide/framework_details.md
rename to doc/developer_guide/framework_details.md
diff --git a/docs/developer_guide/release_checklist.md b/doc/developer_guide/release_checklist.md
similarity index 89%
rename from docs/developer_guide/release_checklist.md
rename to doc/developer_guide/release_checklist.md
index 965cbd2f..ffe36df2 100644
--- a/docs/developer_guide/release_checklist.md
+++ b/doc/developer_guide/release_checklist.md
@@ -14,7 +14,7 @@ prior to proceeding to the next section.
 
 ### Git Milestones
 
- - Go to https://github.com/ess-dmsc/lewis/milestones
+ - Go to https://github.com/ISISComputingGroup/lewis/milestones
  - Ensure all issues and PRs included in this release are tagged correctly
  - Create milestone for next release
  - Ensure any open issues or PRs not included are tagged for next release
@@ -33,7 +33,7 @@ prior to proceeding to the next section.
  - Update `version` in `setup.py`
 
 ### GitHub Release
- - Draft release blurb at https://github.com/ess-dmsc/lewis/releases
+ - Draft release blurb at https://github.com/ISISComputingGroup/lewis/releases
 
 ### Merge Changes
  - Merge any changes made in this section into the main branch
@@ -49,7 +49,7 @@ This should be done in a clean directory.
 ```
 $ python -m venv build
 $ . build/bin/activate
-(build) $ git clone https://github.com/ess-dmsc/lewis.git
+(build) $ git clone https://github.com/ISISComputingGroup/lewis.git
 (build) $ cd lewis
 (build) $ pip install twine wheel
 (build) $ python setup.py sdist bdist_wheel
@@ -84,9 +84,9 @@ work as expected.
 
 ### Git Release
  - Finalize and submit release blurb at:
-   https://github.com/ess-dmsc/lewis/releases
+   https://github.com/ISISComputingGroup/lewis/releases
  - Close the current milestone at:
-   https://github.com/ess-dmsc/lewis/milestones
+   https://github.com/ISISComputingGroup/lewis/milestones
 
 ### Upload PyPI Package
 The ``twine`` utility can be used to upload the packages to PyPI:
diff --git a/docs/developer_guide/writing_devices.md b/doc/developer_guide/writing_devices.md
similarity index 100%
rename from docs/developer_guide/writing_devices.md
rename to doc/developer_guide/writing_devices.md
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 00000000..40808451
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,48 @@
+Welcome to the Lewis documentation!
+===================================
+Lewis is a Python package that makes it easy to develop complex stateful device simulations. It
+is licensed under GPL version 3 and the source is available on github_, where you are welcome to
+open new issues so the package can improve.
+
+Documentation contents:
+
+Quickstart
+==========
+.. toctree::
+   :maxdepth: 2
+   :glob:
+   :caption: Quickstart
+
+   quickstart
+
+
+.. toctree::
+   :maxdepth: 2
+   :glob:
+   :caption: User guide
+
+   user_guide/*
+
+
+.. toctree::
+   :maxdepth: 1
+   :glob:
+   :caption: Developer guide
+
+   developer_guide/*
+
+.. toctree::
+   :maxdepth: 1
+   :caption: Release notes
+   :glob:
+
+   release_notes/release_notes*
+
+.. toctree::
+   :titlesonly:
+   :caption: API reference
+
+   _api
+
+
+.. _github: https://github.com/ISISComputingGroup/lewis
diff --git a/docs/quickstart.md b/doc/quickstart.md
similarity index 97%
rename from docs/quickstart.md
rename to doc/quickstart.md
index c05bf740..98b54e1c 100644
--- a/docs/quickstart.md
+++ b/doc/quickstart.md
@@ -88,7 +88,7 @@ P?
 9.106584
 ```
 
-See [the source code](https://github.com/ess-dmsc/lewis/blob/main/lewis/examples/example_motor/__init__.py) of the example motor if you want to see what makes it tick.
+See [the source code](https://github.com/ISISComputingGroup/lewis/blob/main/lewis/examples/example_motor/__init__.py) of the example motor if you want to see what makes it tick.
 
 ## Connect to Motor via Control Client
 In addition to the simulated TCP Stream interface, Lewis provides a so-called Control Server interface, which allows you to bypass the normal device protocol and access both device and simulation parameters directly while the simulation is running. This can be very useful for debugging and diagnostics, without having to modify the main device interface.
diff --git a/docs/release_notes/release_1_0_0.md b/doc/release_notes/release_1_0_0.md
similarity index 100%
rename from docs/release_notes/release_1_0_0.md
rename to doc/release_notes/release_1_0_0.md
diff --git a/docs/release_notes/release_1_0_1.md b/doc/release_notes/release_1_0_1.md
similarity index 100%
rename from docs/release_notes/release_1_0_1.md
rename to doc/release_notes/release_1_0_1.md
diff --git a/docs/release_notes/release_1_0_2.md b/doc/release_notes/release_1_0_2.md
similarity index 100%
rename from docs/release_notes/release_1_0_2.md
rename to doc/release_notes/release_1_0_2.md
diff --git a/docs/release_notes/release_1_0_3.md b/doc/release_notes/release_1_0_3.md
similarity index 100%
rename from docs/release_notes/release_1_0_3.md
rename to doc/release_notes/release_1_0_3.md
diff --git a/docs/release_notes/release_1_1_0.md b/doc/release_notes/release_1_1_0.md
similarity index 100%
rename from docs/release_notes/release_1_1_0.md
rename to doc/release_notes/release_1_1_0.md
diff --git a/docs/release_notes/release_1_1_1.md b/doc/release_notes/release_1_1_1.md
similarity index 100%
rename from docs/release_notes/release_1_1_1.md
rename to doc/release_notes/release_1_1_1.md
diff --git a/docs/release_notes/release_1_2_0.md b/doc/release_notes/release_1_2_0.md
similarity index 100%
rename from docs/release_notes/release_1_2_0.md
rename to doc/release_notes/release_1_2_0.md
diff --git a/docs/release_notes/release_1_2_1.md b/doc/release_notes/release_1_2_1.md
similarity index 100%
rename from docs/release_notes/release_1_2_1.md
rename to doc/release_notes/release_1_2_1.md
diff --git a/docs/release_notes/release_1_2_2.md b/doc/release_notes/release_1_2_2.md
similarity index 62%
rename from docs/release_notes/release_1_2_2.md
rename to doc/release_notes/release_1_2_2.md
index 120e9955..9abb0472 100644
--- a/docs/release_notes/release_1_2_2.md
+++ b/doc/release_notes/release_1_2_2.md
@@ -2,4 +2,4 @@
 This is a minor release that adds a feature for sending unsolicited messages.
 
 ## New Features
- - Added function for sending unsolicited messages to a device. See [here](https://github.com/ess-dmsc/lewis/commit/08a335a8b11661478c97fd88e3e7d6bb9fcea866).
+ - Added function for sending unsolicited messages to a device. See [here](https://github.com/ISISComputingGroup/lewis/commit/08a335a8b11661478c97fd88e3e7d6bb9fcea866).
diff --git a/docs/release_notes/release_1_3_0.md b/doc/release_notes/release_1_3_0.md
similarity index 100%
rename from docs/release_notes/release_1_3_0.md
rename to doc/release_notes/release_1_3_0.md
diff --git a/docs/release_notes/release_1_3_1.md b/doc/release_notes/release_1_3_1.md
similarity index 100%
rename from docs/release_notes/release_1_3_1.md
rename to doc/release_notes/release_1_3_1.md
diff --git a/docs/release_notes/release_1_3_2.md b/doc/release_notes/release_1_3_2.md
similarity index 100%
rename from docs/release_notes/release_1_3_2.md
rename to doc/release_notes/release_1_3_2.md
diff --git a/docs/release_notes/release_1_3_3.md b/doc/release_notes/release_1_3_3.md
similarity index 100%
rename from docs/release_notes/release_1_3_3.md
rename to doc/release_notes/release_1_3_3.md
diff --git a/doc/release_notes/release_notes.rst b/doc/release_notes/release_notes.rst
new file mode 100644
index 00000000..37aa9027
--- /dev/null
+++ b/doc/release_notes/release_notes.rst
@@ -0,0 +1,9 @@
+.. _release_notes:
+
+Release notes
+=============
+.. toctree::
+   :maxdepth: 1
+   :glob:
+
+   *
\ No newline at end of file
diff --git a/docs/resources/diagrams/SimulationCycles.png b/doc/resources/diagrams/SimulationCycles.png
similarity index 100%
rename from docs/resources/diagrams/SimulationCycles.png
rename to doc/resources/diagrams/SimulationCycles.png
diff --git a/docs/resources/diagrams/SimulationCycles.xml b/doc/resources/diagrams/SimulationCycles.xml
similarity index 100%
rename from docs/resources/diagrams/SimulationCycles.xml
rename to doc/resources/diagrams/SimulationCycles.xml
diff --git a/docs/resources/diagrams/chopper.fsm b/doc/resources/diagrams/chopper.fsm
similarity index 100%
rename from docs/resources/diagrams/chopper.fsm
rename to doc/resources/diagrams/chopper.fsm
diff --git a/docs/resources/logo/lewis-logo-simple.png b/doc/resources/logo/lewis-logo-simple.png
similarity index 100%
rename from docs/resources/logo/lewis-logo-simple.png
rename to doc/resources/logo/lewis-logo-simple.png
diff --git a/docs/resources/logo/lewis-logo-simple.svg b/doc/resources/logo/lewis-logo-simple.svg
similarity index 100%
rename from docs/resources/logo/lewis-logo-simple.svg
rename to doc/resources/logo/lewis-logo-simple.svg
diff --git a/docs/resources/logo/lewis-logo.png b/doc/resources/logo/lewis-logo.png
similarity index 100%
rename from docs/resources/logo/lewis-logo.png
rename to doc/resources/logo/lewis-logo.png
diff --git a/docs/resources/logo/lewis-logo.svg b/doc/resources/logo/lewis-logo.svg
similarity index 100%
rename from docs/resources/logo/lewis-logo.svg
rename to doc/resources/logo/lewis-logo.svg
diff --git a/docs/user_guide/adapter_specifics.md b/doc/user_guide/adapter_specifics.md
similarity index 100%
rename from docs/user_guide/adapter_specifics.md
rename to doc/user_guide/adapter_specifics.md
diff --git a/docs/user_guide/command_line_tools.md b/doc/user_guide/command_line_tools.md
similarity index 73%
rename from docs/user_guide/command_line_tools.md
rename to doc/user_guide/command_line_tools.md
index 503c2e90..7eea2230 100644
--- a/docs/user_guide/command_line_tools.md
+++ b/doc/user_guide/command_line_tools.md
@@ -3,7 +3,7 @@ This page documents the program usage for ``lewis`` and ``lewis-control``, the t
 tools provided as part of a Lewis installation.
 
 ## lewis
-See `lewis.scripts.run`
+See {py:meth}`lewis.scripts.run`
 
 ## lewis-control
-See `lewis.scripts.control`
+See {py:meth}`lewis.scripts.control`
diff --git a/docs/user_guide/remote_access_devices.md b/doc/user_guide/remote_access_devices.md
similarity index 100%
rename from docs/user_guide/remote_access_devices.md
rename to doc/user_guide/remote_access_devices.md
diff --git a/docs/user_guide/remote_access_simulation.md b/doc/user_guide/remote_access_simulation.md
similarity index 97%
rename from docs/user_guide/remote_access_simulation.md
rename to doc/user_guide/remote_access_simulation.md
index 0a441f81..121799e6 100644
--- a/docs/user_guide/remote_access_simulation.md
+++ b/doc/user_guide/remote_access_simulation.md
@@ -1,4 +1,4 @@
-## Remote Access to Simulation Parameters
+# Remote Access to Simulation Parameters
 *Please note that this functionality should only be used on a trusted
 network.*
 
diff --git a/docs/user_guide/usage_with_python.md b/doc/user_guide/usage_with_python.md
similarity index 98%
rename from docs/user_guide/usage_with_python.md
rename to doc/user_guide/usage_with_python.md
index d4501733..00ffd32e 100644
--- a/docs/user_guide/usage_with_python.md
+++ b/doc/user_guide/usage_with_python.md
@@ -40,7 +40,7 @@ the ``lewis`` command is available.
 Clone the repository in a location of your choice, we recommend that you do it inside a virtual
 environment so that you can keep track of the dependencies:
 ```
-$ git clone https://github.com/ess-dmsc/lewis.git
+$ git clone https://github.com/ISISComputingGroup/lewis.git
 ```
 
 If you do not have git available, you can
diff --git a/lewis/__init__.py b/lewis/__init__.py
index dfd28eb7..88521108 100644
--- a/lewis/__init__.py
+++ b/lewis/__init__.py
@@ -1,3 +1,4 @@
+"""Lewis - a library for creating hardware device simulators"""
 # -*- coding: utf-8 -*-
 # *********************************************************************
 # lewis - a library for creating hardware device simulators
@@ -18,3 +19,4 @@
 # *********************************************************************
 
 __version__ = "1.3.3"
+__all__ = []
diff --git a/lewis/adapters/epics.py b/lewis/adapters/epics.py
index 35ad9f91..950e766a 100644
--- a/lewis/adapters/epics.py
+++ b/lewis/adapters/epics.py
@@ -42,13 +42,13 @@
     "Please refer to the documentation for advice."
 )
 
-Driver, SimpleServer = FromOptionalDependency(
-    "pcaspy", missing_pcaspy_exception
-).do_import("Driver", "SimpleServer")
+Driver, SimpleServer = FromOptionalDependency("pcaspy", missing_pcaspy_exception).do_import(
+    "Driver", "SimpleServer"
+)
 
-pcaspy_manager = FromOptionalDependency(
-    "pcaspy.driver", missing_pcaspy_exception
-).do_import("manager")
+pcaspy_manager = FromOptionalDependency("pcaspy.driver", missing_pcaspy_exception).do_import(
+    "manager"
+)
 
 
 class BoundPV:
@@ -76,7 +76,7 @@ class BoundPV:
     :param meta_target: Object that has an attribute named pv.meta_data_property.
     """
 
-    def __init__(self, pv, target, meta_target=None):
+    def __init__(self, pv, target, meta_target=None) -> None:
         self._meta_target = meta_target
         self._target = target
         self._pv = pv
@@ -87,7 +87,7 @@ def value(self):
         return getattr(self._target, self._pv.property)
 
     @value.setter
-    def value(self, new_value):
+    def value(self, new_value) -> None:
         if self.read_only:
             raise AccessViolationException(
                 "The property {} is read only.".format(self._pv.property)
@@ -149,15 +149,13 @@ class PV:
     .. sourcecode:: Python
 
         class Interface(EpicsInterface):
-            pvs = {
-                'example': PV('example', meta_data_property='example_meta')
-            }
+            pvs = {"example": PV("example", meta_data_property="example_meta")}
 
             @property
             def example_meta(self):
                 return {
-                    'lolim': self.device._example_low_limit,
-                    'hilim': self.device._example_high_limit,
+                    "lolim": self.device._example_low_limit,
+                    "hilim": self.device._example_high_limit,
                 }
 
     The PV infos are then updated together with the value, determined by ``poll_interval``.
@@ -171,10 +169,9 @@ class SomeDevice(Device):
             def get_example(self):
                 return 42
 
+
         class Interface(EpicsInterface):
-            pvs = {
-                'example': PV('get_example')
-            }
+            pvs = {"example": PV("get_example")}
 
     It is also possible to model a getter/setter pair, in this case a tuple has to be provided:
 
@@ -189,10 +186,9 @@ def get_example(self):
             def set_example(self, new_example):
                 self._ex = new_example - 2
 
+
         class Interface(EpicsInterface):
-            pvs = {
-                'example': PV(('get_example', 'set_example'))
-            }
+            pvs = {"example": PV(("get_example", "set_example"))}
 
     Any of the two members in the tuple can be substituted with ``None`` in case it does not apply.
     Besides method names it is also allowed to provide callables. Valid callables are for example
@@ -220,8 +216,8 @@ def __init__(
         read_only=False,
         meta_data_property=None,
         doc=None,
-        **kwargs
-    ):
+        **kwargs,
+    ) -> None:
         self.property = "value"
         self.read_only = read_only
         self.poll_interval = poll_interval
@@ -303,11 +299,7 @@ def _get_target(self, prop, *targets):
             # set to True at this point automatically.
             target_prop = getattr(type(target), raw_getter, None)
 
-            if (
-                prop == "value"
-                and isinstance(target_prop, property)
-                and target_prop.fset is None
-            ):
+            if prop == "value" and isinstance(target_prop, property) and target_prop.fset is None:
                 self.read_only = True
 
             # Now the target does not need to be constructed, property or meta_data_property
@@ -349,9 +341,7 @@ def _create_getter(self, func, *targets):
             raise RuntimeError(
                 "The function '{}' does not look like a getter function. A valid getter "
                 "function has no arguments that do not have a default. The self-argument of "
-                "methods does not count towards that number.".format(
-                    final_callable.__name__
-                )
+                "methods does not count towards that number.".format(final_callable.__name__)
             )
 
         @wraps(final_callable)
@@ -382,7 +372,7 @@ def _create_setter(self, func, *targets):
                 "methods does not count towards that number.".format(func.__name__)
             )
 
-        def setter(obj, value):
+        def setter(obj, value) -> None:
             func(value)
 
         return setter
@@ -399,9 +389,7 @@ def _get_callable(self, func, *targets):
         """
         if not callable(func):
             func_name = func
-            func = next(
-                (getattr(obj, func, None) for obj in targets if func in dir(obj)), None
-            )
+            func = next((getattr(obj, func, None) for obj in targets if func in dir(obj)), None)
 
             if not func:
                 raise AttributeError(
@@ -427,7 +415,7 @@ def _function_has_n_args(self, func, n):
 
 @has_log
 class PropertyExposingDriver(Driver):
-    def __init__(self, interface, device_lock):
+    def __init__(self, interface, device_lock) -> None:
         super(PropertyExposingDriver, self).__init__()
 
         self._interface = interface
@@ -437,7 +425,7 @@ def __init__(self, interface, device_lock):
         self._timers = {k: 0.0 for k in self._interface.bound_pvs.keys()}
         self._last_update_call = None
 
-    def write(self, pv, value):
+    def write(self, pv, value) -> bool:
         self.log.debug("PV put request: %s=%s", pv, value)
 
         pv_object = self._interface.bound_pvs.get(pv)
@@ -460,8 +448,7 @@ def write(self, pv, value):
             )
         except AccessViolationException:
             self.log.warning(
-                "Rejected writing value %s to PV %s due to access "
-                "violation, PV is read-only.",
+                "Rejected writing value %s to PV %s due to access " "violation, PV is read-only.",
                 value,
                 pv,
             )
@@ -489,7 +476,7 @@ def _get_param_info(self, pv, meta_keys):
 
         return info_dict
 
-    def process_pv_updates(self, force=False):
+    def process_pv_updates(self, force=False) -> None:
         """
         Update PV values that have changed for PVs that are due to update according to their
         respective poll interval timers.
@@ -515,9 +502,7 @@ def process_pv_updates(self, force=False):
                             meta_updates.append((pv, pv_meta))
 
                     except (AttributeError, TypeError):
-                        self.log.exception(
-                            "An error occurred while updating PV %s.", pv
-                        )
+                        self.log.exception("An error occurred while updating PV %s.", pv)
                     finally:
                         self._timers[pv] = 0.0
 
@@ -526,7 +511,7 @@ def process_pv_updates(self, force=False):
 
         self._last_update_call = datetime.now()
 
-    def _process_value_updates(self, updates):
+    def _process_value_updates(self, updates) -> None:
         if updates:
             update_log = []
             for pv, value in updates:
@@ -538,7 +523,7 @@ def _process_value_updates(self, updates):
             # Calling this manually is only required for values, not for meta
             self.updatePVs()
 
-    def _process_meta_updates(self, updates):
+    def _process_meta_updates(self, updates) -> None:
         if updates:
             update_log = []
             for pv, info in updates:
@@ -557,16 +542,14 @@ class EpicsAdapter(Adapter):
 
     .. sourcecode:: Python
 
-        options = {
-            'prefix': 'PVPREFIX:'
-        }
+        options = {"prefix": "PVPREFIX:"}
 
     :param options: Dictionary with options.
     """
 
     default_options = {"prefix": ""}
 
-    def __init__(self, options=None):
+    def __init__(self, options=None) -> None:
         super(EpicsAdapter, self).__init__(options)
 
         self._server = None
@@ -590,7 +573,7 @@ def documentation(self):
 
         return "\n\n".join([inspect.getdoc(self.interface) or "", "PVs\n==="] + pvs)
 
-    def start_server(self):
+    def start_server(self) -> None:
         """
         Creates a pcaspy-server.
 
@@ -611,15 +594,10 @@ def start_server(self):
 
             self.log.info(
                 "Started serving PVs: %s",
-                ", ".join(
-                    (
-                        self._options.prefix + pv
-                        for pv in self.interface.bound_pvs.keys()
-                    )
-                ),
+                ", ".join((self._options.prefix + pv for pv in self.interface.bound_pvs.keys())),
             )
 
-    def stop_server(self):
+    def stop_server(self) -> None:
         self._driver = None
         self._server = None
 
@@ -627,7 +605,7 @@ def stop_server(self):
     def is_running(self):
         return self._server is not None
 
-    def handle(self, cycle_delay=0.1):
+    def handle(self, cycle_delay=0.1) -> None:
         """
         Call this method to spend about ``cycle_delay`` seconds processing
         requests in the pcaspy server. Under load, for example when running ``caget`` at a
@@ -655,10 +633,7 @@ class EpicsInterface(InterfaceBase):
     .. sourcecode:: Python
 
         class SimpleDeviceEpicsInterface(EpicsInterface):
-            pvs = {
-                'VELO': PV('speed', read_only=True),
-                'POS': PV('position', lolo=0, hihi=100)
-            }
+            pvs = {"VELO": PV("speed", read_only=True), "POS": PV("position", lolo=0, hihi=100)}
 
     For more complex behavior, the interface could contain properties that do not
     exist in the device itself. If the device should also have a PV called STOP
@@ -668,9 +643,9 @@ class SimpleDeviceEpicsInterface(EpicsInterface):
 
         class SimpleDeviceEpicsInterface(EpicsInterface):
             pvs = {
-                'VELO': PV('speed', read_only=True),
-                'POS': PV('position', lolo=0, hihi=100),
-                'STOP': PV('stop', type='int'),
+                "VELO": PV("speed", read_only=True),
+                "POS": PV("position", lolo=0, hihi=100),
+                "STOP": PV("stop", type="int"),
             }
 
             @property
@@ -702,7 +677,7 @@ def stop(self, value):
     protocol = "epics"
     pvs = None
 
-    def __init__(self):
+    def __init__(self) -> None:
         super(EpicsInterface, self).__init__()
         self.bound_pvs = None
 
@@ -710,7 +685,7 @@ def __init__(self):
     def adapter(self):
         return EpicsAdapter
 
-    def _bind_device(self):
+    def _bind_device(self) -> None:
         """
         This method transforms the ``self.pvs`` dict of :class:`PV` objects ``self.bound_pvs``,
         a dict of :class:`BoundPV` objects, the keys are always the PV-names that are exposed
diff --git a/lewis/adapters/modbus.py b/lewis/adapters/modbus.py
index e703029b..055bc400 100644
--- a/lewis/adapters/modbus.py
+++ b/lewis/adapters/modbus.py
@@ -54,7 +54,7 @@ class ModbusDataBank:
     :param kwargs: Configuration
     """
 
-    def __init__(self, **kwargs):
+    def __init__(self, **kwargs) -> None:
         self._data = kwargs["data"]
         self._start_addr = kwargs["start_addr"]
 
@@ -71,12 +71,10 @@ def get(self, addr, count):
         data = self._data[addr : addr + count]
         if len(data) != count:
             addr += self._start_addr
-            raise IndexError(
-                "Invalid address range [{:#06x} - {:#06x}]".format(addr, addr + count)
-            )
+            raise IndexError("Invalid address range [{:#06x} - {:#06x}]".format(addr, addr + count))
         return data
 
-    def set(self, addr, values):
+    def set(self, addr, values) -> None:
         """
         Write list ``values`` to ``addr`` memory location in DataBank.
 
@@ -89,9 +87,7 @@ def set(self, addr, values):
         if not 0 <= addr <= end <= len(self._data):
             addr += self._start_addr
             raise IndexError(
-                "Invalid address range [{:#06x} - {:#06x}]".format(
-                    addr, addr + len(values)
-                )
+                "Invalid address range [{:#06x} - {:#06x}]".format(addr, addr + len(values))
             )
         self._data[addr:end] = values
 
@@ -113,7 +109,7 @@ class ModbusBasicDataBank(ModbusDataBank):
     :param last_addr: Last valid address
     """
 
-    def __init__(self, default_value=0, start_addr=0x0000, last_addr=0xFFFF):
+    def __init__(self, default_value=0, start_addr=0x0000, last_addr=0xFFFF) -> None:
         super(ModbusBasicDataBank, self).__init__(
             start_addr=start_addr, data=[default_value] * (last_addr - start_addr + 1)
         )
@@ -122,7 +118,7 @@ def __init__(self, default_value=0, start_addr=0x0000, last_addr=0xFFFF):
 class ModbusDataStore:
     """Convenience struct to hold the four types of DataBanks in Modbus"""
 
-    def __init__(self, di=None, co=None, ir=None, hr=None):
+    def __init__(self, di=None, co=None, ir=None, hr=None) -> None:
         self.di = di
         self.co = co
         self.ir = ir
@@ -158,7 +154,7 @@ class ModbusTCPFrame:
     :except EOFError: Not enough data for complete frame; no data consumed.
     """
 
-    def __init__(self, stream=None):
+    def __init__(self, stream=None) -> None:
         self.transaction_id = 0
         self.protocol_id = 0
         self.length = 2
@@ -169,7 +165,7 @@ def __init__(self, stream=None):
         if stream is not None:
             self.from_bytearray(stream)
 
-    def from_bytearray(self, stream):
+    def from_bytearray(self, stream) -> None:
         """
         Constructs this frame from input data stream, consuming as many bytes as necessary from
         the beginning of the stream.
@@ -277,7 +273,7 @@ class ModbusProtocol:
     :param datastore: ModbusDataStore instance to reference when processing requests
     """
 
-    def __init__(self, sender, datastore):
+    def __init__(self, sender, datastore) -> None:
         self._buffer = bytearray()
         self._datastore = datastore
         self._send = lambda req: sender(req.to_bytearray())
@@ -294,7 +290,7 @@ def __init__(self, sender, datastore):
             0x10: self._handle_write_multiple_registers,
         }
 
-    def process(self, data, device_lock):
+    def process(self, data, device_lock) -> None:
         """
         Process as much of given data as possible.
 
@@ -346,9 +342,7 @@ def _get_handler(self, fcode):
 
     def _illegal_function_exception(self, request):
         """Log and return an illegal function code exception"""
-        self.log.error(
-            "Unsupported Function Code: {0} ({0:#04x})".format(request.fcode)
-        )
+        self.log.error("Unsupported Function Code: {0} ({0:#04x})".format(request.fcode))
         return request.create_exception(MBEX.ILLEGAL_FUNCTION)
 
     def _handle_read_coils(self, request):
@@ -536,22 +530,20 @@ def _handle_write_multiple_registers(self, request):
 
 @has_log
 class ModbusHandler(asyncore.dispatcher_with_send):
-    def __init__(self, sock, interface, server):
+    def __init__(self, sock, interface, server) -> None:
         asyncore.dispatcher_with_send.__init__(self, sock=sock)
-        self._datastore = ModbusDataStore(
-            interface.di, interface.co, interface.ir, interface.hr
-        )
+        self._datastore = ModbusDataStore(interface.di, interface.co, interface.ir, interface.hr)
         self._modbus = ModbusProtocol(self.send, self._datastore)
         self._server = server
 
         self._set_logging_context(interface)
         self.log.info("Client connected from %s:%s", *sock.getpeername())
 
-    def handle_read(self):
+    def handle_read(self) -> None:
         data = self.recv(8192)
         self._modbus.process(data, self._server.device_lock)
 
-    def handle_close(self):
+    def handle_close(self) -> None:
         self.log.info("Closing connection to client %s:%s", *self.socket.getpeername())
         self._server.remove_handler(self)
         self.close()
@@ -559,7 +551,7 @@ def handle_close(self):
 
 @has_log
 class ModbusServer(asyncore.dispatcher):
-    def __init__(self, host, port, interface, device_lock):
+    def __init__(self, host, port, interface, device_lock) -> None:
         asyncore.dispatcher.__init__(self)
         self.device_lock = device_lock
         self.interface = interface
@@ -573,17 +565,17 @@ def __init__(self, host, port, interface, device_lock):
 
         self._accepted_connections = []
 
-    def handle_accept(self):
+    def handle_accept(self) -> None:
         pair = self.accept()
         if pair is not None:
             sock, _ = pair
             handler = ModbusHandler(sock, self.interface, self)
             self._accepted_connections.append(handler)
 
-    def remove_handler(self, handler):
+    def remove_handler(self, handler) -> None:
         self._accepted_connections.remove(handler)
 
-    def handle_close(self):
+    def handle_close(self) -> None:
         self.log.info("Shutting down server, closing all remaining client connections.")
 
         for handler in self._accepted_connections:
@@ -595,11 +587,11 @@ def handle_close(self):
 class ModbusAdapter(Adapter):
     default_options = {"bind_address": "0.0.0.0", "port": 502}
 
-    def __init__(self, options=None):
+    def __init__(self, options=None) -> None:
         super(ModbusAdapter, self).__init__(options)
         self._server = None
 
-    def start_server(self):
+    def start_server(self) -> None:
         self._server = ModbusServer(
             self._options.bind_address,
             self._options.port,
@@ -607,7 +599,7 @@ def start_server(self):
             self.device_lock,
         )
 
-    def stop_server(self):
+    def stop_server(self) -> None:
         if self._server is not None:
             self._server.close()
             self._server = None
@@ -616,7 +608,7 @@ def stop_server(self):
     def is_running(self):
         return self._server is not None
 
-    def handle(self, cycle_delay=0.1):
+    def handle(self, cycle_delay=0.1) -> None:
         asyncore.loop(cycle_delay, count=1)
 
 
diff --git a/lewis/adapters/stream.py b/lewis/adapters/stream.py
index d037310f..35f65dfd 100644
--- a/lewis/adapters/stream.py
+++ b/lewis/adapters/stream.py
@@ -22,6 +22,7 @@
 import inspect
 import re
 import socket
+from typing import NoReturn
 
 from scanf import scanf_compile
 
@@ -33,7 +34,7 @@
 
 @has_log
 class StreamHandler(asynchat.async_chat):
-    def __init__(self, sock, target, stream_server):
+    def __init__(self, sock, target, stream_server) -> None:
         asynchat.async_chat.__init__(self, sock=sock)
         self.set_terminator(target.in_terminator.encode())
         self._readtimeout = target.readtimeout
@@ -47,7 +48,7 @@ def __init__(self, sock, target, stream_server):
         self._set_logging_context(target)
         self.log.info("Client connected from %s:%s", *sock.getpeername())
 
-    def process(self, msec):
+    def process(self, msec) -> None:
         if not self._buffer:
             return
 
@@ -59,16 +60,14 @@ def process(self, msec):
                 self._readtimer = 0
                 request = self._get_request()
                 with self._stream_server.device_lock:
-                    error = RuntimeError(
-                        "ReadTimeout while waiting for command terminator."
-                    )
+                    error = RuntimeError("ReadTimeout while waiting for command terminator.")
                     reply = self._handle_error(request, error)
                 self._send_reply(reply)
 
         if self._buffer:
             self._readtimer += msec
 
-    def collect_incoming_data(self, data):
+    def collect_incoming_data(self, data) -> None:
         self._buffer.append(data)
         self._readtimer = 0
 
@@ -78,7 +77,7 @@ def _get_request(self):
         self.log.debug("Got request %s", request)
         return request
 
-    def _push(self, reply):
+    def _push(self, reply) -> None:
         try:
             if isinstance(reply, str):
                 reply = reply.encode()
@@ -91,7 +90,7 @@ def _push(self, reply):
         except TypeError as e:
             self.log.error("Problem creating reply, type error {}!".format(e))
 
-    def _send_reply(self, reply):
+    def _send_reply(self, reply) -> None:
         if reply is not None:
             self.log.debug("Sending reply %s", reply)
             self._push(reply)
@@ -100,7 +99,7 @@ def _handle_error(self, request, error):
         self.log.debug("Error while processing request", exc_info=error)
         return self._target.handle_error(request, error)
 
-    def found_terminator(self):
+    def found_terminator(self) -> None:
         self._readtimer = 0
 
         request = self._get_request()
@@ -108,11 +107,7 @@ def found_terminator(self):
         with self._stream_server.device_lock:
             try:
                 cmd = next(
-                    (
-                        cmd
-                        for cmd in self._target.bound_commands
-                        if cmd.can_process(request)
-                    ),
+                    (cmd for cmd in self._target.bound_commands if cmd.can_process(request)),
                     None,
                 )
 
@@ -131,11 +126,11 @@ def found_terminator(self):
 
         self._send_reply(reply)
 
-    def unsolicited_reply(self, reply):
+    def unsolicited_reply(self, reply) -> None:
         self.log.debug("Sending unsolicited reply %s", reply)
         self._push(reply)
 
-    def handle_close(self):
+    def handle_close(self) -> None:
         self.log.info("Closing connection to client %s:%s", *self.socket.getpeername())
         self._stream_server.remove_handler(self)
         asynchat.async_chat.handle_close(self)
@@ -143,7 +138,7 @@ def handle_close(self):
 
 @has_log
 class StreamServer(asyncore.dispatcher):
-    def __init__(self, host, port, target, device_lock):
+    def __init__(self, host, port, target, device_lock) -> None:
         asyncore.dispatcher.__init__(self)
         self.target = target
         self.device_lock = device_lock
@@ -157,7 +152,7 @@ def __init__(self, host, port, target, device_lock):
 
         self._accepted_connections = []
 
-    def handle_accept(self):
+    def handle_accept(self) -> None:
         pair = self.accept()
         if pair is not None:
             sock, addr = pair
@@ -165,10 +160,10 @@ def handle_accept(self):
 
             self._accepted_connections.append(handler)
 
-    def remove_handler(self, handler):
+    def remove_handler(self, handler) -> None:
         self._accepted_connections.remove(handler)
 
-    def close(self):
+    def close(self) -> None:
         # As this is an old style class, the base class method must
         # be called directly. This is important to still perform all
         # the teardown-work that asyncore.dispatcher does.
@@ -181,7 +176,7 @@ def close(self):
 
         self._accepted_connections = []
 
-    def process(self, msec):
+    def process(self, msec) -> None:
         for handler in self._accepted_connections:
             handler.process(msec)
 
@@ -199,7 +194,7 @@ class PatternMatcher:
         :class:`regex`, :class:`scanf` are concrete implementations of this class.
     """
 
-    def __init__(self, pattern):
+    def __init__(self, pattern) -> None:
         self._pattern = pattern
 
     @property
@@ -208,16 +203,16 @@ def pattern(self):
         return self._pattern
 
     @property
-    def arg_count(self):
+    def arg_count(self) -> NoReturn:
         """Number of arguments that are matched in a request."""
         raise NotImplementedError("The arg_count property must be implemented.")
 
     @property
-    def argument_mappings(self):
+    def argument_mappings(self) -> NoReturn:
         """Mapping functions that can be applied to the arguments returned by :meth:`match`."""
         raise NotImplementedError("The argument_mappings property must be implemented.")
 
-    def match(self, request):
+    def match(self, request) -> NoReturn:
         """
         Tries to match the request against the internally stored pattern. Returns any matched
         function arguments.
@@ -234,7 +229,7 @@ class regex(PatternMatcher):
     expression.
     """
 
-    def __init__(self, pattern):
+    def __init__(self, pattern) -> None:
         super(regex, self).__init__(pattern)
 
         self.compiled_pattern = re.compile(pattern.encode())
@@ -244,7 +239,7 @@ def arg_count(self):
         return self.compiled_pattern.groups
 
     @property
-    def argument_mappings(self):
+    def argument_mappings(self) -> None:
         return None
 
     def match(self, request):
@@ -266,8 +261,8 @@ class scanf(regex):
 
     .. sourcecode:: Python
 
-        exact = scanf('T=%f')
-        not_exact = scanf('T=%f', exact_match=False)
+        exact = scanf("T=%f")
+        not_exact = scanf("T=%f", exact_match=False)
 
     The first pattern only matches the string ``T=4.0``, whereas the second would also match
     ``T=4.0garbage``. Please note that the specifiers like ``%f`` are automatically turned into
@@ -279,7 +274,7 @@ class scanf(regex):
     .. _scanf: https://github.com/joshburnett/scanf
     """
 
-    def __init__(self, pattern, exact_match=True):
+    def __init__(self, pattern, exact_match=True) -> None:
         self._scanf_pattern = pattern
 
         generated_regex, self._argument_mappings = scanf_compile(pattern)
@@ -348,11 +343,9 @@ class Func:
 
     def __init__(
         self, func, pattern, argument_mappings=None, return_mapping=None, doc=None
-    ):
+    ) -> None:
         if not callable(func):
-            raise RuntimeError(
-                "Can not construct a Func-object from a non callable object."
-            )
+            raise RuntimeError("Can not construct a Func-object from a non callable object.")
 
         self.func = func
 
@@ -384,9 +377,7 @@ def __init__(
                 )
             )
 
-        if argument_mappings is not None and (
-            self.matcher.arg_count != len(argument_mappings)
-        ):
+        if argument_mappings is not None and (self.matcher.arg_count != len(argument_mappings)):
             raise RuntimeError(
                 "Supplied argument mappings for function matched by pattern '{}' specify {} "
                 "argument(s), but the function has {} arguments.".format(
@@ -483,7 +474,7 @@ class CommandBase:
 
     def __init__(
         self, func, pattern, argument_mappings=None, return_mapping=None, doc=None
-    ):
+    ) -> None:
         super(CommandBase, self).__init__()
 
         self.func = func
@@ -492,7 +483,7 @@ def __init__(
         self.return_mapping = return_mapping
         self.doc = doc
 
-    def bind(self, target):
+    def bind(self, target) -> NoReturn:
         raise NotImplementedError("Binders need to implement the bind method.")
 
 
@@ -542,7 +533,7 @@ def __init__(
         argument_mappings=None,
         return_mapping=lambda x: None if x is None else str(x),
         doc=None,
-    ):
+    ) -> None:
         super(Cmd, self).__init__(func, pattern, argument_mappings, return_mapping, doc)
 
     def bind(self, target):
@@ -617,10 +608,8 @@ def __init__(
         argument_mappings=None,
         return_mapping=lambda x: None if x is None else str(x),
         doc=None,
-    ):
-        super(Var, self).__init__(
-            target_member, None, argument_mappings, return_mapping, doc
-        )
+    ) -> None:
+        super(Var, self).__init__(target_member, None, argument_mappings, return_mapping, doc)
 
         self.target = None
 
@@ -654,7 +643,7 @@ def getter():
 
         if self.write_pattern is not None:
 
-            def setter(new_value):
+            def setter(new_value) -> None:
                 setattr(target, self.func, new_value)
 
             # Copy docstring if target is a @property
@@ -691,7 +680,7 @@ class StreamAdapter(Adapter):
 
     default_options = {"telnet_mode": False, "bind_address": "0.0.0.0", "port": 9999}
 
-    def __init__(self, options=None):
+    def __init__(self, options=None) -> None:
         super(StreamAdapter, self).__init__(options)
         self._server = None
 
@@ -702,9 +691,7 @@ def documentation(self):
                 cmd.matcher.pattern,
                 format_doc_text(cmd.doc or inspect.getdoc(cmd.func) or ""),
             )
-            for cmd in sorted(
-                self.interface.bound_commands, key=lambda x: x.matcher.pattern
-            )
+            for cmd in sorted(self.interface.bound_commands, key=lambda x: x.matcher.pattern)
         ]
 
         options = format_doc_text(
@@ -726,7 +713,7 @@ def documentation(self):
             + commands
         )
 
-    def start_server(self):
+    def start_server(self) -> None:
         """
         Starts the TCP stream server, binding to the configured host and port.
         Host and port are configured via the command line arguments.
@@ -747,7 +734,7 @@ def start_server(self):
                 self.device_lock,
             )
 
-    def stop_server(self):
+    def stop_server(self) -> None:
         if self._server is not None:
             self._server.close()
             self._server = None
@@ -756,7 +743,7 @@ def stop_server(self):
     def is_running(self):
         return self._server is not None
 
-    def handle(self, cycle_delay=0.1):
+    def handle(self, cycle_delay=0.1) -> None:
         """
         Spend approximately ``cycle_delay`` seconds to process requests to the server.
 
@@ -815,6 +802,7 @@ def get_speed(self):
     In addition, the :meth:`handle_error`-method can be overridden. It is called when an exception
     is raised while handling commands.
     """
+
     protocol = "stream"
 
     in_terminator = "\r"
@@ -824,7 +812,7 @@ def get_speed(self):
 
     commands = None
 
-    def __init__(self):
+    def __init__(self) -> None:
         super(StreamInterface, self).__init__()
         self.bound_commands = None
 
@@ -832,7 +820,7 @@ def __init__(self):
     def adapter(self):
         return StreamAdapter
 
-    def _bind_device(self):
+    def _bind_device(self) -> None:
         """
         This method implements ``_bind_device`` from :class:`~lewis.core.devices.InterfaceBase`.
         It binds Cmd and Var definitions to implementations in Interface and Device.
@@ -854,15 +842,16 @@ def _bind_device(self):
                 pattern = bound_cmd.matcher.pattern
                 if pattern in patterns:
                     raise RuntimeError(
-                        "The regular expression {} is "
-                        "associated with multiple commands.".format(pattern)
+                        "The regular expression {} is " "associated with multiple commands.".format(
+                            pattern
+                        )
                     )
 
                 patterns.add(pattern)
 
                 self.bound_commands.append(bound_cmd)
 
-    def handle_error(self, request, error):
+    def handle_error(self, request, error) -> None:
         """
         Override this method to handle exceptions that are raised during command processing.
         The default implementation does nothing, so that any errors are silently ignored.
diff --git a/lewis/core/approaches.py b/lewis/core/approaches.py
index 07e14890..edf5b3ee 100644
--- a/lewis/core/approaches.py
+++ b/lewis/core/approaches.py
@@ -23,7 +23,7 @@
 """
 
 
-def linear(current, target, rate, dt):
+def linear(current: float, target: float, rate: float, dt: float):
     """
     This function returns the new value after moving towards
     target at the given speed constantly for the time dt.
diff --git a/lewis/core/control_client.py b/lewis/core/control_client.py
index a0bb2013..d3605a09 100644
--- a/lewis/core/control_client.py
+++ b/lewis/core/control_client.py
@@ -49,11 +49,9 @@ class RemoteException(Exception):
     :param message: Exception message on the server side.
     """
 
-    def __init__(self, exception_type, message):
+    def __init__(self, exception_type, message) -> None:
         super(RemoteException, self).__init__(
-            "Exception on server side of type '{}': '{}'".format(
-                exception_type, message
-            )
+            "Exception on server side of type '{}': '{}'".format(exception_type, message)
         )
 
         self.server_side_type = exception_type
@@ -83,7 +81,7 @@ class ControlClient:
     :param timeout: Timeout in milliseconds for ZMQ operations.
     """
 
-    def __init__(self, host="127.0.0.1", port="10000", timeout=3000):
+    def __init__(self, host="127.0.0.1", port="10000", timeout=3000) -> None:
         self.timeout = timeout if timeout is not None else -1
 
         self._socket = self._get_zmq_req_socket()
@@ -168,7 +166,7 @@ class manipulation, this class must never be used directly.
 
     .. sourcecode:: Python
 
-        proxy = type('SomeClassName', (ObjectProxy, ), {})(connection, methods, prefix)
+        proxy = type("SomeClassName", (ObjectProxy,), {})(connection, methods, prefix)
 
     There is however, the class ControlClient, which automates all that
     and provides objects that are ready to use.
@@ -185,7 +183,7 @@ class manipulation, this class must never be used directly.
     :param prefix: Usually object name on the server plus dot.
     """
 
-    def __init__(self, connection, members, prefix=""):
+    def __init__(self, connection, members, prefix="") -> None:
         self._properties = set()
 
         self._connection = connection
@@ -231,7 +229,7 @@ def _make_request(self, method, *args):
             else:
                 raise ProtocolException(response["error"]["message"])
 
-    def _add_member_proxies(self, members):
+    def _add_member_proxies(self, members) -> None:
         for member in [str(m) for m in members]:
             if ":set" in member or ":get" in member:
                 self._properties.add(member.split(":")[-2].split(".")[-1])
@@ -242,9 +240,7 @@ def _add_member_proxies(self, members):
             setattr(
                 type(self),
                 prop,
-                property(
-                    self._create_getter_proxy(prop), self._create_setter_proxy(prop)
-                ),
+                property(self._create_getter_proxy(prop), self._create_setter_proxy(prop)),
             )
 
     def _create_getter_proxy(self, property_name):
diff --git a/lewis/core/control_server.py b/lewis/core/control_server.py
index f3769f29..deef2783 100644
--- a/lewis/core/control_server.py
+++ b/lewis/core/control_server.py
@@ -77,9 +77,7 @@ class ExposedObject:
     :param lock: ``threading.Lock`` that is used when accessing ``obj``.
     """
 
-    def __init__(
-        self, obj, members=None, exclude=None, exclude_inherited=False, lock=None
-    ):
+    def __init__(self, obj, members=None, exclude=None, exclude_inherited=False, lock=None) -> None:
         super(ExposedObject, self).__init__()
 
         self._object = obj
@@ -104,7 +102,7 @@ def _public_members(self):
         """
         return [prop for prop in dir(self._object) if not prop.startswith("_")]
 
-    def _add_member_wrappers(self, member):
+    def _add_member_wrappers(self, member) -> None:
         """
         This method probes the supplied member of the wrapped object and inserts an appropriate
         entry into the internal method map. Getters and setters for properties get a suffix
@@ -112,9 +110,7 @@ def _add_member_wrappers(self, member):
 
         :param member: The member of the wrapped object to expose
         """
-        method_object = getattr(type(self._object), member, None) or getattr(
-            self._object, member
-        )
+        method_object = getattr(type(self._object), member, None) or getattr(self._object, member)
 
         if callable(method_object):
             self._add_function(member, getattr(self._object, member))
@@ -136,22 +132,20 @@ def get_api(self):
     def __getitem__(self, item):
         return self._function_map[item]
 
-    def __len__(self):
+    def __len__(self) -> int:
         return len(self._function_map)
 
     def __iter__(self):
         return iter(self._function_map)
 
-    def __contains__(self, item):
+    def __contains__(self, item) -> bool:
         return item in self._function_map
 
-    def _add_property(self, name):
+    def _add_property(self, name) -> None:
         self._add_function("{}:get".format(name), lambda: getattr(self._object, name))
-        self._add_function(
-            "{}:set".format(name), lambda value: setattr(self._object, name, value)
-        )
+        self._add_function("{}:set".format(name), lambda value: setattr(self._object, name, value))
 
-    def _add_function(self, name, function):
+    def _add_function(self, name, function) -> None:
         if not callable(function):
             raise TypeError("Only callable objects can be exposed.")
 
@@ -168,7 +162,7 @@ def locking_wrapper_function(*args, **kwargs):
 
         self._function_map[name] = function
 
-    def _remove_function(self, name):
+    def _remove_function(self, name) -> None:
         del self._function_map[name]
 
 
@@ -185,7 +179,7 @@ class ExposedObjectCollection(ExposedObject):
 
     .. sourcecode:: Python
 
-        name:api
+        name: api
 
     A list of exposed objects can be obtained by calling the following method from the client:
 
@@ -196,7 +190,7 @@ class ExposedObjectCollection(ExposedObject):
     :param named_objects: Dictionary of of name: object pairs.
     """
 
-    def __init__(self, named_objects):
+    def __init__(self, named_objects) -> None:
         super(ExposedObjectCollection, self).__init__(self, ("get_objects",))
         self._object_map = {}
 
@@ -206,7 +200,7 @@ def __init__(self, named_objects):
 
         self._add_function("get_objects", self.get_objects)
 
-    def add_object(self, obj, name):
+    def add_object(self, obj, name) -> None:
         """
         Adds the supplied object to the collection under the supplied name. If the name is already
         in use, a RuntimeError is raised. If the object is not an instance of
@@ -226,7 +220,7 @@ def add_object(self, obj, name):
             glue = "." if not method_name.startswith(":") else ""
             self._add_function(name + glue + method_name, exposed_object[method_name])
 
-    def remove_object(self, name):
+    def remove_object(self, name) -> None:
         """
         Remove the object exposed under that name. If no object is registered under the supplied
         name, a RuntimeError is raised.
@@ -269,7 +263,7 @@ class ControlServer:
     :param connection_string: String with host:port pair for binding control server.
     """
 
-    def __init__(self, object_map, connection_string):
+    def __init__(self, object_map, connection_string) -> None:
         super(ControlServer, self).__init__()
 
         try:
@@ -277,17 +271,13 @@ def __init__(self, object_map, connection_string):
         except ValueError:
             raise LewisException(
                 "'{}' is not a valid control server initialization string. "
-                'A string of the form "host:port" is expected.'.format(
-                    connection_string
-                )
+                'A string of the form "host:port" is expected.'.format(connection_string)
             )
 
         try:
             self.host = socket.gethostbyname(host)
         except socket.gaierror:
-            raise LewisException(
-                "Could not resolve control server host: {}".format(host)
-            )
+            raise LewisException("Could not resolve control server host: {}".format(host))
 
         self.port = port
 
@@ -312,7 +302,7 @@ def exposed_object(self):
         """
         return self._exposed_object
 
-    def start_server(self):
+    def start_server(self) -> None:
         """
         Binds the server to the configured host and port and starts listening.
         """
@@ -339,7 +329,7 @@ def _unhandled_exception_response(self, request_id, exception):
             },
         }
 
-    def process(self, blocking=False):
+    def process(self, blocking=False) -> None:
         """
         Each time this method is called, the socket tries to retrieve data and passes
         it to the JSONRPCResponseManager, which in turn passes the RPC to the
@@ -357,14 +347,10 @@ def process(self, blocking=False):
                          is triggered. Default is False to preserve behavior of prior versions.
         """
         if self._socket is None:
-            raise RuntimeError(
-                "The server has not been started yet, use start_server to do so."
-            )
+            raise RuntimeError("The server has not been started yet, use start_server to do so.")
 
         try:
-            request = self._socket.recv_unicode(
-                flags=zmq.NOBLOCK if not blocking else 0
-            )
+            request = self._socket.recv_unicode(flags=zmq.NOBLOCK if not blocking else 0)
 
             self.log.debug("Got request %s", request)
 
diff --git a/lewis/core/devices.py b/lewis/core/devices.py
index 39609a5f..9dc86865 100644
--- a/lewis/core/devices.py
+++ b/lewis/core/devices.py
@@ -25,6 +25,7 @@
 """
 
 import importlib
+from typing import NoReturn
 
 from lewis.core.exceptions import LewisException
 from lewis.core.logging import has_log
@@ -53,12 +54,12 @@ class InterfaceBase:
 
     protocol = None
 
-    def __init__(self):
+    def __init__(self) -> None:
         super(InterfaceBase, self).__init__()
         self._device = None
 
     @property
-    def adapter(self):
+    def adapter(self) -> NoReturn:
         """
         Adapter type that is required to process and expose interfaces of this type. Must be
         implemented in subclasses.
@@ -77,11 +78,11 @@ def device(self):
         return self._device
 
     @device.setter
-    def device(self, new_device):
+    def device(self, new_device) -> None:
         self._device = new_device
         self._bind_device()
 
-    def _bind_device(self):
+    def _bind_device(self) -> None:
         """
         This method should perform any binding steps between device and interface. The result
         of this binding step is generally used by the adapter to process network traffic.
@@ -155,9 +156,11 @@ class DeviceBuilder:
             broken=dict(
                 device_type=SimulatedDeviceType,
                 parameters=dict(
-                    override_initial_state='error',
-                    override_initial_data=dict(
-                        target=-10, position=-20.0))))
+                    override_initial_state="error",
+                    override_initial_data=dict(target=-10, position=-20.0),
+                ),
+            )
+        )
 
     The other location is a sub-package called `setups`, which should in turn contain modules. Each
     module must contain a variable ``device_type`` and a variable ``parameters`` which are
@@ -179,7 +182,7 @@ class DeviceBuilder:
     a RuntimeError is raised.
     """
 
-    def __init__(self, module):
+    def __init__(self, module) -> None:
         self._module = module
 
         submodules = get_submodules(self._module)
@@ -193,9 +196,7 @@ def __init__(self, module):
             self._module.__name__,
             ", ".join(device_t.__name__ for device_t in self._device_types),
             ", ".join(self._setups.keys()),
-            ", ".join(
-                "(%s: %s)" % (k, v.__name__) for k, v in self._interfaces.items()
-            ),
+            ", ".join("(%s: %s)" % (k, v.__name__) for k, v in self._interfaces.items()),
         )
 
     def _discover_devices(self, devices_package):
@@ -226,9 +227,7 @@ def _discover_setups(self, setups_package):
                     )
 
                 all_setups[name] = {
-                    "device_type": getattr(
-                        setup_module, "device_type", self.default_device_type
-                    ),
+                    "device_type": getattr(setup_module, "device_type", self.default_device_type),
                     "parameters": getattr(setup_module, "parameters", {}),
                 }
 
@@ -242,9 +241,7 @@ def _discover_interfaces(self, interface_package):
 
         if interface_package is not None:
             for interface_module in get_submodules(interface_package).values():
-                all_interfaces += list(
-                    get_members(interface_module, is_interface).values()
-                )
+                all_interfaces += list(get_members(interface_module, is_interface).values())
 
         all_interfaces += list(get_members(self._module, is_interface).values())
 
@@ -369,9 +366,7 @@ def create_device(self, setup=None):
         )
 
         try:
-            return self._create_device_instance(
-                device_type, **setup_data.get("parameters", {})
-            )
+            return self._create_device_instance(device_type, **setup_data.get("parameters", {}))
         except RuntimeError:
             raise LewisException(
                 "The setup '{}' you tried to load does not specify a valid device type, but the "
@@ -419,8 +414,8 @@ class DeviceRegistry:
 
         from lewis.core.devices import DeviceRegistry
 
-        registry = DeviceRegistry('lewis.devices')
-        chopper_builder = registry.device_builder('chopper')
+        registry = DeviceRegistry("lewis.devices")
+        chopper_builder = registry.device_builder("chopper")
 
         # construct device, interface, ...
 
@@ -429,7 +424,7 @@ class DeviceRegistry:
     :param device_module: Name of device module from which devices are loaded.
     """
 
-    def __init__(self, device_module):
+    def __init__(self, device_module) -> None:
         try:
             self._device_module = importlib.import_module(device_module)
         except ImportError:
diff --git a/lewis/core/logging.py b/lewis/core/logging.py
index 02f53378..c939998e 100644
--- a/lewis/core/logging.py
+++ b/lewis/core/logging.py
@@ -33,7 +33,7 @@
 """
 
 import logging
-from typing import Callable, overload, ParamSpec, TypeVar, Type, Protocol
+from typing import Callable, ParamSpec, Protocol, Type, TypeVar, overload
 
 
 class HasLog(Protocol):
diff --git a/lewis/core/processor.py b/lewis/core/processor.py
index 78057a9f..8dfe2f93 100644
--- a/lewis/core/processor.py
+++ b/lewis/core/processor.py
@@ -42,13 +42,13 @@ class CanProcess:
     The doBefore- and doAfterProcess methods are only called if a doProcess-method exists.
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         super(CanProcess, self).__init__()
 
     def __call__(self, dt=0):
         self.process(dt)
 
-    def process(self, dt=0):
+    def process(self, dt=0) -> None:
         if hasattr(self, "doProcess"):
             if hasattr(self, "doBeforeProcess"):
                 self.doBeforeProcess(dt)
@@ -77,7 +77,7 @@ class CanProcessComposite(CanProcess):
     and doAfterProcess methods.
     """
 
-    def __init__(self, iterable=()):
+    def __init__(self, iterable=()) -> None:
         super(CanProcessComposite, self).__init__()
 
         self._processors = []
@@ -85,13 +85,13 @@ def __init__(self, iterable=()):
         for item in iterable:
             self.add_processor(item)
 
-    def add_processor(self, other):
+    def add_processor(self, other) -> None:
         if isinstance(other, CanProcess):
             self._append_processor(other)
 
-    def _append_processor(self, processor):
+    def _append_processor(self, processor) -> None:
         self._processors.append(processor)
 
-    def doProcess(self, dt):
+    def doProcess(self, dt) -> None:
         for processor in self._processors:
             processor.process(dt)
diff --git a/lewis/core/simulation.py b/lewis/core/simulation.py
index beb70aa5..ac53bf3c 100644
--- a/lewis/core/simulation.py
+++ b/lewis/core/simulation.py
@@ -81,7 +81,7 @@ class Simulation:
     :param control_server: 'host:port'-string to construct control server or None.
     """
 
-    def __init__(self, device, adapters=(), device_builder=None, control_server=None):
+    def __init__(self, device, adapters=(), device_builder=None, control_server=None) -> None:
         super(Simulation, self).__init__()
 
         self._device_builder = device_builder
@@ -102,9 +102,7 @@ def __init__(self, device, adapters=(), device_builder=None, control_server=None
 
         # Constructing the control server must be deferred until the end,
         # because the construction is not complete at this point
-        self._control_server = (
-            None  # Just initialize to None and use property setter afterwards
-        )
+        self._control_server = None  # Just initialize to None and use property setter afterwards
         self._control_server_thread = None
         self.control_server = control_server
 
@@ -154,13 +152,9 @@ def setups(self):
         A list of setups that are available. Use :meth:`switch_setup` to
         change the setup.
         """
-        return (
-            list(self._device_builder.setups.keys())
-            if self._device_builder is not None
-            else []
-        )
+        return list(self._device_builder.setups.keys()) if self._device_builder is not None else []
 
-    def switch_setup(self, new_setup):
+    def switch_setup(self, new_setup) -> None:
         """
         This method switches the setup, which means that it replaces the currently
         simulated device with a new device, as defined by the setup.
@@ -181,7 +175,7 @@ def switch_setup(self, new_setup):
             )
             raise
 
-    def start(self):
+    def start(self) -> None:
         """
         Starts the simulation.
         """
@@ -207,23 +201,21 @@ def start(self):
 
         self.log.info("Simulation has ended.")
 
-    def _start_control_server(self):
+    def _start_control_server(self) -> None:
         if self._control_server is not None and self._control_server_thread is None:
 
-            def control_server_loop():
+            def control_server_loop() -> None:
                 self._control_server.start_server()
 
                 while not self._stop_commanded:
                     self._control_server.process(blocking=True)
 
-                self.log.info(
-                    "Stopped processing control server commands, ending thread."
-                )
+                self.log.info("Stopped processing control server commands, ending thread.")
 
             self._control_server_thread = Thread(target=control_server_loop)
             self._control_server_thread.start()
 
-    def _stop_control_server(self):
+    def _stop_control_server(self) -> None:
         if self._control_server_thread is not None:
             self._control_server_thread.join(timeout=1.0)
             self._control_server_thread = None
@@ -245,7 +237,7 @@ def _process_cycle(self, delta):
 
         return delta
 
-    def _process_simulation_cycle(self, delta):
+    def _process_simulation_cycle(self, delta) -> None:
         """
         If the simulation is not paused, the device's process-method is
         called with the supplied delta, multiplied by the simulation speed.
@@ -277,7 +269,7 @@ def cycle_delay(self):
         return self._cycle_delay
 
     @cycle_delay.setter
-    def cycle_delay(self, delay):
+    def cycle_delay(self, delay) -> None:
         if delay < 0.0:
             raise ValueError("Cycle delay can not be negative.")
 
@@ -312,7 +304,7 @@ def speed(self):
         return self._speed
 
     @speed.setter
-    def speed(self, new_speed):
+    def speed(self, new_speed) -> None:
         if new_speed < 0:
             raise ValueError("Speed can not be negative.")
 
@@ -328,7 +320,7 @@ def runtime(self):
         """
         return self._runtime
 
-    def set_device_parameters(self, parameters):
+    def set_device_parameters(self, parameters) -> None:
         """
         Set multiple parameters of the simulated device "simultaneously". The passed
         parameter is assumed to be device parameter/value dict.
@@ -354,7 +346,7 @@ def set_device_parameters(self, parameters):
 
         self.log.debug("Updated device parameters: %s", parameters)
 
-    def pause(self):
+    def pause(self) -> None:
         """
         Pause the simulation. Can only be called after start has been called.
         """
@@ -365,7 +357,7 @@ def pause(self):
 
         self._running = False
 
-    def resume(self):
+    def resume(self) -> None:
         """
         Resume a paused simulation. Can only be called after start
         and pause have been called.
@@ -377,7 +369,7 @@ def resume(self):
 
         self._running = True
 
-    def stop(self):
+    def stop(self) -> None:
         """
         Stops the simulation entirely.
         """
@@ -414,11 +406,9 @@ def control_server(self):
         return self._control_server
 
     @control_server.setter
-    def control_server(self, control_server):
+    def control_server(self, control_server) -> None:
         if self.is_started and self._control_server:
-            raise RuntimeError(
-                "Can not replace control server while simulation is running."
-            )
+            raise RuntimeError("Can not replace control server while simulation is running.")
 
         self._control_server = self._create_control_server(control_server)
 
@@ -434,20 +424,20 @@ class SimulationFactory:
 
     .. sourcecode:: Python
 
-        factory = SimulationFactory('lewis.devices')
+        factory = SimulationFactory("lewis.devices")
 
     The actual creation happens via the :meth:`create`-method:
 
     .. sourcecode:: Python
 
-        simulation = factory.create('device_name', protocol='protocol')
+        simulation = factory.create("device_name", protocol="protocol")
 
     The simulation can then be started and stopped as desired.
 
     .. warning:: This class is meant for internal use at the moment and may change frequently.
     """
 
-    def __init__(self, devices_package):
+    def __init__(self, devices_package) -> None:
         self._reg = DeviceRegistry(devices_package)
 
     @property
diff --git a/lewis/core/statemachine.py b/lewis/core/statemachine.py
index 25c6b1d8..5aadcac2 100644
--- a/lewis/core/statemachine.py
+++ b/lewis/core/statemachine.py
@@ -48,11 +48,11 @@ class HasContext:
     StateMachine was provided with a context itself).
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         super(HasContext, self).__init__()
         self._context = None
 
-    def set_context(self, new_context):
+    def set_context(self, new_context) -> None:
         """Assigns the new context to the member variable ``_context``."""
         self._context = new_context
 
@@ -73,10 +73,10 @@ class State(HasContext):
     custom behaviour. Device context is provided via :class:`HasContext` mixin.
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         super(State, self).__init__()
 
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         """
         Handle entry event. Raised once, when this state is entered.
 
@@ -84,7 +84,7 @@ def on_entry(self, dt):
         """
         pass
 
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         """
         Handle in-state event.
 
@@ -96,7 +96,7 @@ def in_state(self, dt):
         """
         pass
 
-    def on_exit(self, dt):
+    def on_exit(self, dt) -> None:
         """
         Handle exit event. Raised once, when this state is exited.
 
@@ -117,7 +117,7 @@ class Transition(HasContext):
     To use this class, create a derived class and override the :meth:`__call__` attribute.
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         super(Transition, self).__init__()
 
     def __call__(self):
@@ -174,18 +174,14 @@ class StateMachine(CanProcess):
     .. seealso:: See :meth:`~StateMachine.doProcess` for details.
     """
 
-    def __init__(self, cfg, context=None):
+    def __init__(self, cfg, context=None) -> None:
         super(StateMachine, self).__init__()
 
         self._set_logging_context(context)
 
-        self._state = (
-            None  # We start outside of any state, first cycle enters initial state
-        )
+        self._state = None  # We start outside of any state, first cycle enters initial state
         self._handler = {}  # Nested dict mapping [state][event] = handler
-        self._transition = (
-            {}
-        )  # Dict mapping [from_state] = [ (to_state, transition), ... ]
+        self._transition = {}  # Dict mapping [from_state] = [ (to_state, transition), ... ]
         self._prefix = {  # Default prefixes used when calling handler functions by name
             "on_entry": "_on_entry_",
             "in_state": "_in_state_",
@@ -195,8 +191,7 @@ def __init__(self, cfg, context=None):
         # Specifying an initial state is not optional
         if "initial" not in cfg:
             raise StateMachineException(
-                "StateMachine configuration must include "
-                "'initial' to specify starting state."
+                "StateMachine configuration must include " "'initial' to specify starting state."
             )
         self._initial = cfg["initial"]
         self._set_handlers(self._initial)
@@ -204,7 +199,7 @@ def __init__(self, cfg, context=None):
         self._setup_state_handlers(cfg.get("states", {}), context)
         self._setup_transition_handlers(cfg.get("transitions", {}), context)
 
-    def _setup_state_handlers(self, state_handler_configuration, context):
+    def _setup_state_handlers(self, state_handler_configuration, context) -> None:
         """
         This method constructs the state handlers from a user-provided dict.
 
@@ -237,7 +232,7 @@ def _setup_state_handlers(self, state_handler_configuration, context):
                     "Must be dict or iterable." % state_name
                 )
 
-    def _setup_transition_handlers(self, transition_handler_configuration, context):
+    def _setup_transition_handlers(self, transition_handler_configuration, context) -> None:
         """
         This method constructs the transition handlers from a user-provided
         dict.
@@ -279,7 +274,7 @@ def can(self, state):
 
         return state in (transition[0] for transition in self._transition[self._state])
 
-    def bind_handlers_by_name(self, instance, override=False, prefix=None):
+    def bind_handlers_by_name(self, instance, override=False, prefix=None) -> None:
         """
         Auto-bind state handlers based on naming convention.
 
@@ -327,7 +322,7 @@ def bind_handlers_by_name(self, instance, override=False, prefix=None):
                     if callable(named_handler):
                         self._handler[state][event] = named_handler
 
-    def doProcess(self, dt):
+    def doProcess(self, dt) -> None:
         """
         Process a cycle of this state machine.
 
@@ -361,9 +356,7 @@ def doProcess(self, dt):
         # General transition
         for target_state, check_func in self._transition.get(self._state, []):
             if check_func():
-                self.log.debug(
-                    "Transition triggered (%s -> %s)", self._state, target_state
-                )
+                self.log.debug("Transition triggered (%s -> %s)", self._state, target_state)
                 self._raise_event("on_exit", dt)
                 self._state = target_state
                 self._raise_event("on_entry", dt)
@@ -372,14 +365,14 @@ def doProcess(self, dt):
         # Always end with an in_state
         self._raise_event("in_state", dt)
 
-    def reset(self):
+    def reset(self) -> None:
         """
         Reset the state machine to before the first cycle. The next process() will
         enter the initial state.
         """
         self._state = None
 
-    def _set_handlers(self, state, *args, **kwargs):
+    def _set_handlers(self, state, *args, **kwargs) -> None:
         """
         Add or update state handlers.
 
@@ -405,7 +398,7 @@ def _set_handlers(self, state, *args, **kwargs):
             "on_exit": on_exit,
         }
 
-    def _set_transition(self, from_state, to_state, transition_check):
+    def _set_transition(self, from_state, to_state, transition_check) -> None:
         """
         Add or update a transition and its condition function.
 
@@ -440,7 +433,7 @@ def _set_transition(self, from_state, to_state, transition_check):
             )
         )
 
-    def _raise_event(self, event, dt):
+    def _raise_event(self, event, dt) -> None:
         """
         Invoke the given event name for the current state, passing dt as a parameter.
 
diff --git a/lewis/core/utils.py b/lewis/core/utils.py
index 6497b9f8..f9fe2bbd 100644
--- a/lewis/core/utils.py
+++ b/lewis/core/utils.py
@@ -30,6 +30,7 @@
 from os import listdir
 from os import path as osp
 from types import ModuleType
+from typing import NoReturn
 
 from lewis.core.exceptions import LewisException, LimitViolationException
 from lewis.core.logging import has_log
@@ -127,7 +128,7 @@ def extract_module_name(absolute_path):
     return None
 
 
-def dict_strict_update(base_dict, update_dict):
+def dict_strict_update(base_dict, update_dict) -> None:
     """
     This function updates base_dict with update_dict if and only if update_dict does not contain
     keys that are not already in base_dict. It is essentially a more strict interpretation of the
@@ -210,7 +211,7 @@ class FromOptionalDependency:
     :param exception: Text for LewisException or custom exception object.
     """
 
-    def __init__(self, module, exception=None):
+    def __init__(self, module, exception=None) -> None:
         self._module = module
 
         if exception is None:
@@ -246,7 +247,7 @@ def do_import(self, *names):
             objects = tuple(getattr(module_object, name) for name in names)
         except ImportError:
 
-            def failing_init(obj, *args, **kwargs):
+            def failing_init(obj, *args, **kwargs) -> NoReturn:
                 raise self._exception
 
             objects = tuple(type(name, (object,), {"__init__": failing_init}) for name in names)
@@ -339,7 +340,7 @@ def set_temperature(self, t_in_kelvin):
     :param silent: A limit violation will not raise an exception if this option is ``True``.
     """
 
-    def __init__(self, lower=None, upper=None, silent=False):
+    def __init__(self, lower=None, upper=None, silent=False) -> None:
         self._lower = lower
         self._upper = upper
         self._silent = silent
diff --git a/lewis/devices/__init__.py b/lewis/devices/__init__.py
index f4edbaed..0cf5dc32 100644
--- a/lewis/devices/__init__.py
+++ b/lewis/devices/__init__.py
@@ -23,7 +23,7 @@
 using a state machine.
 """
 
-import logging
+from logging import Logger
 from typing import Callable
 
 from lewis.core.devices import DeviceBase
@@ -104,7 +104,7 @@ def __init__(
     ) -> None:
         super(StateMachineDevice, self).__init__()
 
-        self.log: logging.Logger
+        self.log: Logger
         self.log.info("Creating device, setting up state machine")
 
         self._initialize_data()
diff --git a/lewis/devices/chopper/devices/bearings.py b/lewis/devices/chopper/devices/bearings.py
index adfc5661..0b20dc56 100644
--- a/lewis/devices/chopper/devices/bearings.py
+++ b/lewis/devices/chopper/devices/bearings.py
@@ -19,21 +19,21 @@
 
 
 class Bearings:
-    def __init__(self):
+    def __init__(self) -> None:
         pass
 
-    def disengage(self):
+    def disengage(self) -> None:
         pass
 
-    def engage(self):
+    def engage(self) -> None:
         pass
 
 
 class MagneticBearings(Bearings):
-    def __init__(self):
+    def __init__(self) -> None:
         super(MagneticBearings, self).__init__()
 
 
 class MechanicalBearings(Bearings):
-    def __init__(self):
+    def __init__(self) -> None:
         super(MechanicalBearings, self).__init__()
diff --git a/lewis/devices/chopper/devices/device.py b/lewis/devices/chopper/devices/device.py
index 106671e1..f78741e8 100644
--- a/lewis/devices/chopper/devices/device.py
+++ b/lewis/devices/chopper/devices/device.py
@@ -28,7 +28,7 @@
 
 
 class SimulatedBearings(CanProcess, MagneticBearings):
-    def __init__(self):
+    def __init__(self) -> None:
         super(SimulatedBearings, self).__init__()
 
         self._csm = StateMachine(
@@ -45,25 +45,25 @@ def __init__(self):
 
         self._levitate = False
 
-    def engage(self):
+    def engage(self) -> None:
         self.levitate()
 
-    def disengage(self):
+    def disengage(self) -> None:
         self.delevitate()
 
-    def levitate(self):
+    def levitate(self) -> None:
         self._levitate = True
 
-    def delevitate(self):
+    def delevitate(self) -> None:
         self._levitate = False
 
-    def levitationComplete(self):
+    def levitationComplete(self) -> bool:
         return True
 
-    def delevitationComplete(self):
+    def delevitationComplete(self) -> bool:
         return True
 
-    def doProcess(self, dt):
+    def doProcess(self, dt) -> None:
         self._csm.process(dt)
 
     @property
@@ -78,7 +78,7 @@ def idle(self):
 class SimulatedChopper(StateMachineDevice):
     _bearings = None
 
-    def _initialize_data(self):
+    def _initialize_data(self) -> None:
         self.speed = 0.0
         self.target_speed = 0.0
 
@@ -114,7 +114,7 @@ def _get_state_handlers(self):
             "parked": states.DefaultParkedState(),
         }
 
-    def _get_initial_state(self):
+    def _get_initial_state(self) -> str:
         return "init"
 
     def _get_transition_handlers(self):
@@ -169,17 +169,17 @@ def state(self):
     def initialized(self):
         return self._initialized
 
-    def initialize(self):
+    def initialize(self) -> None:
         if self._csm.can("bearings") and not self.initialized:
             self._initialized = True
             self._bearings.engage()
 
-    def deinitialize(self):
+    def deinitialize(self) -> None:
         if self._csm.can("bearings") and self.initialized:
             self._shutdown_commanded = True
             self._bearings.disengage()
 
-    def park(self):
+    def park(self) -> None:
         if self._csm.can("parking"):
             self._park_commanded = True
 
@@ -187,7 +187,7 @@ def park(self):
     def parked(self):
         return self._csm.state == "parked"
 
-    def stop(self):
+    def stop(self) -> None:
         if self._csm.can("stopping"):
             self._stop_commanded = True
 
@@ -195,7 +195,7 @@ def stop(self):
     def stopped(self):
         return self._csm.state == "stopped"
 
-    def start(self):
+    def start(self) -> None:
         if self._csm.can("accelerating") and self.target_speed > 0.0:
             self._start_commanded = True
         else:
@@ -205,7 +205,7 @@ def start(self):
     def started(self):
         return self._csm.state == "accelerating"
 
-    def unlock(self):
+    def unlock(self) -> None:
         if self._csm.can("idle"):
             self._idle_commanded = True
 
@@ -213,7 +213,7 @@ def unlock(self):
     def idle(self):
         return self._csm.state == "idle"
 
-    def lock_phase(self):
+    def lock_phase(self) -> None:
         if self._csm.can("phase_locking"):
             self._phase_commanded = True
 
diff --git a/lewis/devices/chopper/devices/states.py b/lewis/devices/chopper/devices/states.py
index 1077ab00..82b403ae 100644
--- a/lewis/devices/chopper/devices/states.py
+++ b/lewis/devices/chopper/devices/states.py
@@ -22,16 +22,16 @@
 
 
 class DefaultInitState(State):
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         self._context._initialize_data()
 
 
 class DefaultParkingState(State):
-    def __init__(self, parking_speed=5.0):
+    def __init__(self, parking_speed=5.0) -> None:
         super(DefaultParkingState, self).__init__()
         self._parking_speed = parking_speed
 
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         self._context.parking_position = approaches.linear(
             self._context.parking_position,
             self._context.target_parking_position,
@@ -39,7 +39,7 @@ def in_state(self, dt):
             dt,
         )
 
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         self._context._park_commanded = False
 
 
@@ -48,60 +48,58 @@ class DefaultParkedState(State):
 
 
 class DefaultStoppingState(State):
-    def __init__(self, acceleration=5.0):
+    def __init__(self, acceleration=5.0) -> None:
         super(DefaultStoppingState, self).__init__()
         self._acceleration = acceleration
 
-    def in_state(self, dt):
-        self._context.speed = approaches.linear(
-            self._context.speed, 0.0, self._acceleration, dt
-        )
+    def in_state(self, dt) -> None:
+        self._context.speed = approaches.linear(self._context.speed, 0.0, self._acceleration, dt)
 
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         self._context._stop_commanded = False
 
 
 class DefaultStoppedState(State):
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         if self._context.auto_park:
             self._context._park_commanded = True
 
 
 class DefaultIdleState(State):
-    def __init__(self, acceleration=0.05):
+    def __init__(self, acceleration=0.05) -> None:
         super(DefaultIdleState, self).__init__()
         self._acceleration = acceleration
 
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         self._context.speed = approaches.linear(
             self._context.speed, self._context.target_speed, self._acceleration, dt
         )
 
 
-def on_entry(self, dt):
+def on_entry(self, dt) -> None:
     self._context._idle_commanded = False
 
 
 class DefaultAcceleratingState(State):
-    def __init__(self, acceleration=5.0):
+    def __init__(self, acceleration=5.0) -> None:
         super(DefaultAcceleratingState, self).__init__()
         self._acceleration = acceleration
 
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         self._context.speed = approaches.linear(
             self._context.speed, self._context.target_speed, self._acceleration, dt
         )
 
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         self._context._start_commanded = False
 
 
 class DefaultPhaseLockingState(State):
-    def __init__(self, phase_locking_speed=5.0):
+    def __init__(self, phase_locking_speed=5.0) -> None:
         super(DefaultPhaseLockingState, self).__init__()
         self._phase_locking_speed = phase_locking_speed
 
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         self._context.phase = approaches.linear(
             self._context.phase,
             self._context.target_phase,
@@ -109,7 +107,7 @@ def in_state(self, dt):
             dt,
         )
 
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         self._context._phase_commanded = False
 
 
diff --git a/lewis/devices/chopper/interfaces/epics_interface.py b/lewis/devices/chopper/interfaces/epics_interface.py
index 780551d8..622af2ce 100644
--- a/lewis/devices/chopper/interfaces/epics_interface.py
+++ b/lewis/devices/chopper/interfaces/epics_interface.py
@@ -73,9 +73,7 @@ class ChopperEpicsInterface(EpicsInterface):
             doc="Readback value of phase setpoint in degrees.",
         ),
         "Phs": PV("target_phase", doc="Phase setpoint in degrees."),
-        "ActPhs": PV(
-            "phase", read_only=True, doc="Current phase of the chopper disc in degrees."
-        ),
+        "ActPhs": PV("phase", read_only=True, doc="Current phase of the chopper disc in degrees."),
         "ParkAng-RB": PV(
             "target_parking_position",
             read_only=True,
@@ -112,7 +110,7 @@ class ChopperEpicsInterface(EpicsInterface):
     _last_command = ""
 
     @property
-    def execute_command(self):
+    def execute_command(self) -> str:
         """
         Command to execute. Possible commands are start, stop, set_phase,
         unlock, park, init, deinit.
@@ -120,7 +118,7 @@ def execute_command(self):
         return ""
 
     @execute_command.setter
-    def execute_command(self, value):
+    def execute_command(self, value) -> None:
         command = self._commands.get(value)
 
         getattr(self.device, command)()
diff --git a/lewis/devices/julabo/devices/device.py b/lewis/devices/julabo/devices/device.py
index dc9aecf1..3cac4d06 100644
--- a/lewis/devices/julabo/devices/device.py
+++ b/lewis/devices/julabo/devices/device.py
@@ -44,7 +44,7 @@ class SimulatedJulabo(StateMachineDevice):
     circulate_commanded = False
     temperature_ramp_rate = 5.0  # Guessed value in C/min
 
-    def _initialize_data(self):
+    def _initialize_data(self) -> None:
         """
         This method is called once on construction. After that, it may be
         manually called again to reset the device to its default state.
@@ -63,7 +63,7 @@ def _get_state_handlers(self):
             "not_circulate": states.DefaultNotCirculatingState(),
         }
 
-    def _get_initial_state(self):
+    def _get_initial_state(self) -> str:
         return "not_circulate"
 
     def _get_transition_handlers(self):
@@ -74,7 +74,7 @@ def _get_transition_handlers(self):
             ]
         )
 
-    def set_set_point(self, param):
+    def set_set_point(self, param) -> str:
         """
         Sets the target temperature.
 
@@ -85,7 +85,7 @@ def set_set_point(self, param):
             self.set_point_temperature = param
         return ""
 
-    def set_circulating(self, param):
+    def set_circulating(self, param) -> str:
         """
         Sets whether to circulate - in effect whether the heater is on.
 
@@ -101,7 +101,7 @@ def set_circulating(self, param):
         return ""
 
     @check_limits(0.1, 99.9)
-    def set_internal_p(self, param):
+    def set_internal_p(self, param) -> str:
         """
         Sets the internal proportional.
         Xp in Julabo speak.
@@ -113,7 +113,7 @@ def set_internal_p(self, param):
         return ""
 
     @check_limits(3, 9999)
-    def set_internal_i(self, param):
+    def set_internal_i(self, param) -> str:
         """
         Sets the internal integral.
         Tn in Julabo speak.
@@ -125,7 +125,7 @@ def set_internal_i(self, param):
         return ""
 
     @check_limits(0, 999)
-    def set_internal_d(self, param):
+    def set_internal_d(self, param) -> str:
         """
         Sets the internal derivative.
         Tv in Julabo speak.
@@ -137,7 +137,7 @@ def set_internal_d(self, param):
         return ""
 
     @check_limits(0.1, 99.9)
-    def set_external_p(self, param):
+    def set_external_p(self, param) -> str:
         """
         Sets the external proportional.
         Xp in Julabo speak.
@@ -149,7 +149,7 @@ def set_external_p(self, param):
         return ""
 
     @check_limits(3, 9999)
-    def set_external_i(self, param):
+    def set_external_i(self, param) -> str:
         """
         Sets the external integral.
         Tn in Julabo speak.
@@ -161,7 +161,7 @@ def set_external_i(self, param):
         return ""
 
     @check_limits(0, 999)
-    def set_external_d(self, param):
+    def set_external_d(self, param) -> str:
         """
         Sets the external derivative.
         Tv in Julabo speak.
diff --git a/lewis/devices/julabo/devices/states.py b/lewis/devices/julabo/devices/states.py
index 6f4942a0..b6536051 100644
--- a/lewis/devices/julabo/devices/states.py
+++ b/lewis/devices/julabo/devices/states.py
@@ -26,7 +26,7 @@ class DefaultNotCirculatingState(State):
 
 
 class DefaultCirculatingState(State):
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         # Approach target temperature at a set rate
         self._context.temperature = approaches.linear(
             self._context.temperature,
diff --git a/lewis/devices/linkam_t95/devices/device.py b/lewis/devices/linkam_t95/devices/device.py
index 22ab47b8..c623909c 100644
--- a/lewis/devices/linkam_t95/devices/device.py
+++ b/lewis/devices/linkam_t95/devices/device.py
@@ -25,7 +25,7 @@
 
 
 class SimulatedLinkamT95(StateMachineDevice):
-    def _initialize_data(self):
+    def _initialize_data(self) -> None:
         """
         This method is called once on construction. After that, it may be
         manually called again to reset the device to its default state.
@@ -63,7 +63,7 @@ def _get_state_handlers(self):
             "cool": states.DefaultCoolState(),
         }
 
-    def _get_initial_state(self):
+    def _get_initial_state(self) -> str:
         return "init"
 
     def _get_transition_handlers(self):
@@ -86,27 +86,23 @@ def _get_transition_handlers(self):
                 ),
                 (
                     ("heat", "hold"),
-                    lambda: self.temperature == self.temperature_limit
-                    or self.hold_commanded,
+                    lambda: self.temperature == self.temperature_limit or self.hold_commanded,
                 ),
                 (("heat", "cool"), lambda: self.temperature > self.temperature_limit),
                 (("heat", "stopped"), lambda: self.stop_commanded),
                 (
                     ("hold", "heat"),
-                    lambda: self.temperature < self.temperature_limit
-                    and not self.hold_commanded,
+                    lambda: self.temperature < self.temperature_limit and not self.hold_commanded,
                 ),
                 (
                     ("hold", "cool"),
-                    lambda: self.temperature > self.temperature_limit
-                    and not self.hold_commanded,
+                    lambda: self.temperature > self.temperature_limit and not self.hold_commanded,
                 ),
                 (("hold", "stopped"), lambda: self.stop_commanded),
                 (("cool", "heat"), lambda: self.temperature < self.temperature_limit),
                 (
                     ("cool", "hold"),
-                    lambda: self.temperature == self.temperature_limit
-                    or self.hold_commanded,
+                    lambda: self.temperature == self.temperature_limit or self.hold_commanded,
                 ),
                 (("cool", "stopped"), lambda: self.stop_commanded),
             ]
diff --git a/lewis/devices/linkam_t95/devices/states.py b/lewis/devices/linkam_t95/devices/states.py
index 3b3aa1ff..8f09202a 100644
--- a/lewis/devices/linkam_t95/devices/states.py
+++ b/lewis/devices/linkam_t95/devices/states.py
@@ -26,19 +26,19 @@ class DefaultInitState(State):
 
 
 class DefaultStoppedState(State):
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         # Reset the stop commanded flag once we enter the stopped state
         self._context.stop_commanded = False
 
 
 class DefaultStartedState(State):
-    def on_entry(self, dt):
+    def on_entry(self, dt) -> None:
         # Reset the start commanded flag once we enter the started state
         self._context.start_commanded = False
 
 
 class DefaultHeatState(State):
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         # Approach target temperature at set temperature rate
         self._context.temperature = approaches.linear(
             self._context.temperature,
@@ -53,7 +53,7 @@ class DefaultHoldState(State):
 
 
 class DefaultCoolState(State):
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         # TODO: Does manual control work like this? Or is it perhaps a separate state?
         if self._context.pump_manual_mode:
             self._context.pump_speed = self._context.manual_target_speed
@@ -78,7 +78,7 @@ def in_state(self, dt):
             dt,
         )
 
-    def on_exit(self, dt):
+    def on_exit(self, dt) -> None:
         # If we exit the cooling state, the cooling pump should no longer run
         self._context.pump_overspeed = False
         self._context.pump_speed = 0
diff --git a/lewis/devices/linkam_t95/interfaces/stream_interface.py b/lewis/devices/linkam_t95/interfaces/stream_interface.py
index 139817be..acd8013c 100644
--- a/lewis/devices/linkam_t95/interfaces/stream_interface.py
+++ b/lewis/devices/linkam_t95/interfaces/stream_interface.py
@@ -24,7 +24,7 @@
 @has_log
 class LinkamT95StreamInterface(StreamInterface):
     """
-    Linkam T95 TCP stream interface
+    Linkam T95 TCP stream interface.
 
     This is the interface of a simulated Linkam T95 device. The device listens on a configured
     host:port-combination, one option to connect to it is via telnet:
@@ -84,20 +84,17 @@ def get_status(self):
         Tarray[2] = 0x80 + self.device.pump_speed
 
         # Temperature
-        Tarray[6:10] = [
-            ord(x) for x in "%04x" % (int(self.device.temperature * 10) & 0xFFFF)
-        ]
+        Tarray[6:10] = [ord(x) for x in "%04x" % (int(self.device.temperature * 10) & 0xFFFF)]
 
         return bytes(Tarray)
 
-    def set_rate(self, param):
+    def set_rate(self, param) -> bytes:
         """
         Models "Rate Command" functionality of device.
 
         Sets the target rate of temperature change.
 
-        :param param: Rate of temperature change in C/min, multiplied by 100, as a string.
-        Must be positive.
+        :param param: Rate of temperature change in C/min, multiplied by 100, as a string. Must be positive.
         :return: Empty string.
         """
         # TODO: Is not having leading zeroes / 4 digits an error?
@@ -106,7 +103,7 @@ def set_rate(self, param):
             self.device.temperature_rate = rate / 100.0
         return b""
 
-    def set_limit(self, param):
+    def set_limit(self, param) -> bytes:
         """
         Models "Limit Command" functionality of device.
 
@@ -121,7 +118,7 @@ def set_limit(self, param):
             self.device.temperature_limit = limit / 10.0
         return b""
 
-    def start(self):
+    def start(self) -> bytes:
         """
         Models "Start Command" functionality of device.
 
@@ -133,7 +130,7 @@ def start(self):
         self.device.start_commanded = True
         return b""
 
-    def stop(self):
+    def stop(self) -> bytes:
         """
         Models "Stop Command" functionality of device.
 
@@ -144,7 +141,7 @@ def stop(self):
         self.device.stop_commanded = True
         return b""
 
-    def hold(self):
+    def hold(self) -> bytes:
         """
         Models "Hold Command" functionality of device.
 
@@ -155,7 +152,7 @@ def hold(self):
         self.device.hold_commanded = True
         return b""
 
-    def heat(self):
+    def heat(self) -> bytes:
         """
         Models "Heat Command" functionality of device.
 
@@ -165,7 +162,7 @@ def heat(self):
         self.device.hold_commanded = False
         return b""
 
-    def cool(self):
+    def cool(self) -> bytes:
         """
         Models "Cool Command" functionality of device.
 
@@ -175,7 +172,7 @@ def cool(self):
         self.device.hold_commanded = False
         return b""
 
-    def pump_command(self, param):
+    def pump_command(self, param) -> bytes:
         """
         Models "LNP Pump Commands" functionality of device.
 
@@ -194,7 +191,7 @@ def pump_command(self, param):
             self.device.manual_target_speed = lookup.index(param)
         return b""
 
-    def handle_error(self, request, error):
+    def handle_error(self, request, error) -> None:
         """
         If command is not recognised print and error
 
@@ -203,6 +200,4 @@ def handle_error(self, request, error):
             error: problem
 
         """
-        self.log.error(
-            "An error occurred at request " + repr(request) + ": " + repr(error)
-        )
+        self.log.error("An error occurred at request " + repr(request) + ": " + repr(error))
diff --git a/lewis/examples/dual_device/__init__.py b/lewis/examples/dual_device/__init__.py
index ce597f99..cf82a811 100644
--- a/lewis/examples/dual_device/__init__.py
+++ b/lewis/examples/dual_device/__init__.py
@@ -34,7 +34,7 @@ def get_param(self):
         """The parameter multiplied by 2."""
         return self.param * 2
 
-    def set_param(self, new_param):
+    def set_param(self, new_param) -> None:
         self.param = int(new_param / 2)
 
     @property
@@ -44,7 +44,7 @@ def second(self):
 
     @second.setter
     @check_limits("lower_limit", "upper_limit")
-    def second(self, new_second):
+    def second(self, new_second) -> None:
         self._second = new_second
 
 
diff --git a/lewis/examples/example_motor/__init__.py b/lewis/examples/example_motor/__init__.py
index 5adc71e8..fa5b0b02 100644
--- a/lewis/examples/example_motor/__init__.py
+++ b/lewis/examples/example_motor/__init__.py
@@ -26,7 +26,7 @@
 
 
 class DefaultMovingState(State):
-    def in_state(self, dt):
+    def in_state(self, dt) -> None:
         old_position = self._context.position
         self._context.position = approaches.linear(
             old_position, self._context.target, self._context.speed, dt
@@ -41,7 +41,7 @@ def in_state(self, dt):
 
 
 class SimulatedExampleMotor(StateMachineDevice):
-    def _initialize_data(self):
+    def _initialize_data(self) -> None:
         self.position = 0.0
         self._target = 0.0
         self.speed = 2.0
@@ -49,7 +49,7 @@ def _initialize_data(self):
     def _get_state_handlers(self):
         return {"idle": State(), "moving": DefaultMovingState()}
 
-    def _get_initial_state(self):
+    def _get_initial_state(self) -> str:
         return "idle"
 
     def _get_transition_handlers(self):
@@ -69,7 +69,7 @@ def target(self):
         return self._target
 
     @target.setter
-    def target(self, new_target):
+    def target(self, new_target) -> None:
         if self.state == "moving":
             raise RuntimeError("Can not set new target while moving.")
 
diff --git a/lewis/examples/simple_device/__init__.py b/lewis/examples/simple_device/__init__.py
index 4cc805c4..0ed28b64 100644
--- a/lewis/examples/simple_device/__init__.py
+++ b/lewis/examples/simple_device/__init__.py
@@ -66,7 +66,7 @@ def get_param(self):
         """Returns the device parameter."""
         return self.device.param
 
-    def set_param(self, new_param):
+    def set_param(self, new_param) -> None:
         """Set the device parameter, does not return anything."""
         self.device.param = new_param
 
diff --git a/lewis/examples/timeout_device/__init__.py b/lewis/examples/timeout_device/__init__.py
index 2facc82d..fd52c422 100644
--- a/lewis/examples/timeout_device/__init__.py
+++ b/lewis/examples/timeout_device/__init__.py
@@ -24,10 +24,10 @@
 class TimeTerminatedDevice(Device):
     param = 10
 
-    def say_world(self):
+    def say_world(self) -> str:
         return "world!"
 
-    def say_bar(self):
+    def say_bar(self) -> str:
         return "bar!"
 
 
@@ -73,7 +73,7 @@ class TimeTerminatedInterface(StreamInterface):
     in_terminator = ""
     out_terminator = "\r\n"
 
-    # Unusually long, for easier manual entry
+    # Unusually long, for easier manual entry.
     readtimeout = 2500
 
     def handle_error(self, request, error):
diff --git a/lewis/scripts/control.py b/lewis/scripts/control.py
index a92dc465..ce7db619 100644
--- a/lewis/scripts/control.py
+++ b/lewis/scripts/control.py
@@ -27,12 +27,12 @@
 from lewis.scripts import get_usage_text
 
 
-def list_objects(remote):
+def list_objects(remote) -> None:
     for obj in remote.keys():
         print(obj)
 
 
-def show_api(remote, object_name):
+def show_api(remote, object_name) -> None:
     if object_name not in remote.keys():
         raise RuntimeError("Object '{}' is not exposed by remote.".format(object_name))
 
@@ -61,11 +61,7 @@ def show_api(remote, object_name):
     print("Methods:")
     print(
         "\n".join(
-            sorted(
-                "    {}".format(member)
-                for member in dir(obj)
-                if is_remote_method(obj, member)
-            )
+            sorted("    {}".format(member) for member in dir(obj) if is_remote_method(obj, member))
         )
     )
 
@@ -121,8 +117,7 @@ def call_method(remote, object_name, method, arguments):
 positional_args.add_argument(
     "arguments",
     nargs="*",
-    help="Arguments to method call. For setting a property, "
-    "supply the property value. ",
+    help="Arguments to method call. For setting a property, " "supply the property value. ",
 )
 
 optional_args = parser.add_argument_group("Optional arguments")
@@ -150,9 +145,7 @@ def call_method(remote, object_name, method, arguments):
 optional_args.add_argument(
     "-v", "--version", action="store_true", help="Prints the version and exits."
 )
-optional_args.add_argument(
-    "-h", "--h", action="help", help="Shows this help message and exits."
-)
+optional_args.add_argument("-h", "--h", action="help", help="Shows this help message and exits.")
 
 __doc__ = (
     "To interact with the control server of a running simulation, use this script. "
@@ -160,7 +153,7 @@ def call_method(remote, object_name, method, arguments):
 )
 
 
-def control_simulation(argument_list=None):
+def control_simulation(argument_list=None) -> None:
     args = parser.parse_args(argument_list or sys.argv[1:])
 
     if args.version:
diff --git a/lewis/scripts/run.py b/lewis/scripts/run.py
index c5867190..4e31dfbc 100644
--- a/lewis/scripts/run.py
+++ b/lewis/scripts/run.py
@@ -166,9 +166,7 @@
 other_args.add_argument(
     "-v", "--version", action="store_true", help="Prints the version and exits."
 )
-other_args.add_argument(
-    "-h", "--help", action="help", help="Shows this help message and exits."
-)
+other_args.add_argument("-h", "--help", action="help", help="Shows this help message and exits.")
 
 deprecated_args = parser.add_argument_group("Deprecated arguments")
 deprecated_args.add_argument(
@@ -181,9 +179,7 @@
 
 __doc__ = (
     "This script is the main interaction point of the user with Lewis. The usage "
-    "is as follows:\n\n.. code-block:: none\n\n{}".format(
-        get_usage_text(parser, indent=4)
-    )
+    "is as follows:\n\n.. code-block:: none\n\n{}".format(get_usage_text(parser, indent=4))
 )
 
 
@@ -215,7 +211,7 @@ def parse_adapter_options(raw_adapter_options):
     return protocols
 
 
-def run_simulation(argument_list=None):  # noqa: C901
+def run_simulation(argument_list=None) -> None:  # noqa: C901
     """
     This is effectively the main function of a typical simulation run. Arguments passed in are
     parsed and used to construct and run the simulation.
@@ -237,9 +233,7 @@ def run_simulation(argument_list=None):  # noqa: C901
 
         loglevel = "debug" if arguments.verify else arguments.output_level
         if loglevel != "none":
-            logging.basicConfig(
-                level=getattr(logging, loglevel.upper()), format=default_log_format
-            )
+            logging.basicConfig(level=getattr(logging, loglevel.upper()), format=default_log_format)
 
         if arguments.add_path is not None:
             additional_path = os.path.abspath(arguments.add_path)
@@ -249,9 +243,7 @@ def run_simulation(argument_list=None):  # noqa: C901
         simulation_factory = SimulationFactory(arguments.device_package)
 
         if not arguments.device:
-            devices = [
-                "Please specify a device to simulate. The following devices are available:"
-            ]
+            devices = ["Please specify a device to simulate. The following devices are available:"]
 
             for dev in sorted(simulation_factory.devices):
                 devices.append("    " + dev)
@@ -264,9 +256,7 @@ def run_simulation(argument_list=None):  # noqa: C901
             return
 
         protocols = (
-            parse_adapter_options(arguments.adapter_options)
-            if not arguments.no_interface
-            else {}
+            parse_adapter_options(arguments.adapter_options) if not arguments.no_interface else {}
         )
 
         simulation = simulation_factory.create(
diff --git a/lewis/utils/byte_conversions.py b/lewis/utils/byte_conversions.py
index 5b83614d..359558c0 100644
--- a/lewis/utils/byte_conversions.py
+++ b/lewis/utils/byte_conversions.py
@@ -18,11 +18,12 @@
 # *********************************************************************
 
 import struct
+from typing import Literal
 
-BYTE = 2 ** 8
+BYTE = 2**8
 
 
-def _get_byteorder_name(low_byte_first):
+def _get_byteorder_name(low_byte_first: bool) -> Literal["little", "big"]:
     """
     Get the python name for low byte first
     :param low_byte_first: True for low byte first; False for MSB first
@@ -31,7 +32,7 @@ def _get_byteorder_name(low_byte_first):
     return "little" if low_byte_first else "big"
 
 
-def int_to_raw_bytes(integer, length, low_byte_first) -> bytes:
+def int_to_raw_bytes(integer: int, length: int, low_byte_first: bool) -> bytes:
     """
     Converts an integer to an unsigned set of bytes with the specified length (represented as a string). Unless the
     integer is negative in which case it converts to a signed integer.
@@ -49,7 +50,7 @@ def int_to_raw_bytes(integer, length, low_byte_first) -> bytes:
     )
 
 
-def raw_bytes_to_int(raw_bytes, low_bytes_first=True):
+def raw_bytes_to_int(raw_bytes: bytes, low_bytes_first: bool = True) -> int:
     """
     Converts an unsigned set of bytes to an integer.
 
@@ -75,7 +76,7 @@ def float_to_raw_bytes(real_number: float, low_byte_first: bool = True) -> bytes
     return raw_bytes[::-1] if low_byte_first else raw_bytes
 
 
-def raw_bytes_to_float(raw_bytes):
+def raw_bytes_to_float(raw_bytes: bytes) -> float:
     """
     Convert a set of bytes to a floating point number
 
diff --git a/lewis/utils/command_builder.py b/lewis/utils/command_builder.py
index fce95cdd..c454af1a 100644
--- a/lewis/utils/command_builder.py
+++ b/lewis/utils/command_builder.py
@@ -23,6 +23,7 @@
 
 import re
 from functools import partial
+from typing import AnyStr
 
 from lewis.adapters.stream import Cmd, regex
 from lewis.utils.constants import ACK, ENQ, EOT, ETX, STX
@@ -51,7 +52,7 @@ class CmdBuilder(object):
     >>> CmdBuilder("set_pres").escape("pres?").enq().build()
     """
 
-    def __init__(self, target_method, arg_sep="", ignore="", ignore_case=False):
+    def __init__(self, target_method, arg_sep="", ignore="", ignore_case=False) -> None:
         """
         Create a builder. Use build to create the final object
 
@@ -72,12 +73,12 @@ def __init__(self, target_method, arg_sep="", ignore="", ignore_case=False):
 
         self._ignore_case = ignore_case
 
-    def _add_to_regex(self, regex, is_arg):
+    def _add_to_regex(self, regex, is_arg: bool) -> None:
         self._reg_ex += regex + self._ignore
         if not is_arg:
             self._current_sep = ""
 
-    def optional(self, text):
+    def optional(self, text) -> "CmdBuilder":
         """
         Add some escaped text which does not necessarily need to be there. For commands with optional parameters
         :param text: Text to add
@@ -86,7 +87,7 @@ def optional(self, text):
         self._add_to_regex("(?:" + re.escape(text) + ")?", False)
         return self
 
-    def escape(self, text):
+    def escape(self, text) -> "CmdBuilder":
         """
         Add some text to the regex which is escaped.
 
@@ -96,17 +97,17 @@ def escape(self, text):
         self._add_to_regex(re.escape(text), False)
         return self
 
-    def regex(self, regex):
+    def regex(self, new_regex: str) -> "CmdBuilder":
         """
         Add a regex to match but not as an argument.
 
-        :param regex: regex to add
+        :param new_regex: regex to add
         :return: builder
         """
-        self._add_to_regex(regex, False)
+        self._add_to_regex(new_regex, False)
         return self
 
-    def enum(self, *allowed_values):
+    def enum(self, *allowed_values: AnyStr) -> "CmdBuilder":
         """
         Matches one of a set of specified strings.
 
@@ -120,7 +121,7 @@ def enum(self, *allowed_values):
         self.argument_mappings.append(string_arg)
         return self
 
-    def spaces(self, at_least_one=False):
+    def spaces(self, at_least_one: bool = False) -> "CmdBuilder":
         """
         Add a regex for any number of spaces
 
@@ -133,7 +134,7 @@ def spaces(self, at_least_one=False):
         self._add_to_regex(" " + wildcard, False)
         return self
 
-    def arg(self, arg_regex, argument_mapping=string_arg):
+    def arg(self, arg_regex, argument_mapping: partial = string_arg) -> "CmdBuilder":
         """
         Add an argument to the command.
 
@@ -146,7 +147,7 @@ def arg(self, arg_regex, argument_mapping=string_arg):
         self.argument_mappings.append(argument_mapping)
         return self
 
-    def string(self, length=None):
+    def string(self, length: None | int = None) -> "CmdBuilder":
         """
         Add an argument which is a string of a given length (if blank string is any length)
 
@@ -159,7 +160,7 @@ def string(self, length=None):
             self.arg(".{{{}}}".format(length))
         return self
 
-    def float(self, mapping=float, ignore=False):
+    def float(self, mapping: type = float, ignore: bool = False) -> "CmdBuilder":
         """
         Add a float argument.
 
@@ -170,7 +171,7 @@ def float(self, mapping=float, ignore=False):
         regex = r"[+-]?\d+\.?\d*"
         return self.regex(regex) if ignore else self.arg(regex, mapping)
 
-    def digit(self, mapping=int, ignore=False):
+    def digit(self, mapping: type = int, ignore: bool = False) -> "CmdBuilder":
         """
         Add a single digit argument.
 
@@ -180,7 +181,7 @@ def digit(self, mapping=int, ignore=False):
         """
         return self.regex(r"\d") if ignore else self.arg(r"\d", mapping)
 
-    def char(self, not_chars=None, ignore=False):
+    def char(self, not_chars: None | list[str] | str = None, ignore=False) -> "CmdBuilder":
         """
         Add a single character argument.
 
@@ -188,10 +189,10 @@ def char(self, not_chars=None, ignore=False):
         :param ignore: True to match with a char but ignore the returned value (default: False)
         :return: builder
         """
-        regex = r"." if not_chars is None else "[^{}]".format("".join(not_chars))
-        return self.regex(regex) if ignore else self.arg(regex)
+        _regex = r"." if not_chars is None else "[^{}]".format("".join(not_chars))
+        return self.regex(_regex) if ignore else self.arg(_regex)
 
-    def int(self, mapping=int, ignore=False):
+    def int(self, mapping: type = int, ignore: bool = False) -> "CmdBuilder":
         """
         Add an integer argument.
 
@@ -199,10 +200,10 @@ def int(self, mapping=int, ignore=False):
         :param ignore: True to match with a int but ignore the returned value (default: False)
         :return:  builder
         """
-        regex = r"[+-]?\d+"
-        return self.regex(regex) if ignore else self.arg(regex, mapping)
+        _regex = r"[+-]?\d+"
+        return self.regex(_regex) if ignore else self.arg(_regex, mapping)
 
-    def any(self):
+    def any(self) -> "CmdBuilder":
         """
         Add an argument that matches anything.
 
@@ -210,7 +211,7 @@ def any(self):
         """
         return self.arg(r".*")
 
-    def any_except(self, char):
+    def any_except(self, char: str) -> "CmdBuilder":
         """
         Adds an argument that matches anything other than a specified character (useful for commands containing
         delimiters)
@@ -220,7 +221,7 @@ def any_except(self, char):
         """
         return self.arg(r"[^{}]*".format(re.escape(char)))
 
-    def build(self, *args, **kwargs):
+    def build(self, *args, **kwargs) -> Cmd:
         """
         Builds the CMd object based on the target and regular expression.
 
@@ -238,10 +239,10 @@ def build(self, *args, **kwargs):
             pattern,
             argument_mappings=self.argument_mappings,
             *args,
-            **kwargs
+            **kwargs,
         )
 
-    def add_ascii_character(self, char_number):
+    def add_ascii_character(self, char_number: int) -> "CmdBuilder":
         """
         Add a single character based on its integer value, e.g. 49 is 'a'.
 
@@ -251,7 +252,7 @@ def add_ascii_character(self, char_number):
         self._add_to_regex(chr(char_number), False)
         return self
 
-    def stx(self):
+    def stx(self) -> "CmdBuilder":
         """
         Add the STX character (0x2) to the string.
 
@@ -259,7 +260,7 @@ def stx(self):
         """
         return self.escape(STX)
 
-    def etx(self):
+    def etx(self) -> "CmdBuilder":
         """
         Add the ETX character (0x3) to the string.
 
@@ -267,7 +268,7 @@ def etx(self):
         """
         return self.escape(ETX)
 
-    def eot(self):
+    def eot(self) -> "CmdBuilder":
         """
         Add the EOT character (0x4) to the string.
 
@@ -275,7 +276,7 @@ def eot(self):
         """
         return self.escape(EOT)
 
-    def enq(self):
+    def enq(self) -> "CmdBuilder":
         """
         Add the ENQ character (0x5) to the string.
 
@@ -283,7 +284,7 @@ def enq(self):
         """
         return self.escape(ENQ)
 
-    def ack(self):
+    def ack(self) -> "CmdBuilder":
         """
         Add the ACK character (0x6) to the string.
 
@@ -291,7 +292,7 @@ def ack(self):
         """
         return self.escape(ACK)
 
-    def eos(self):
+    def eos(self) -> "CmdBuilder":
         """
         Adds the regex end-of-string character to a command.
 
@@ -300,7 +301,7 @@ def eos(self):
         self._reg_ex += "$"
         return self
 
-    def get_multicommands(self, command_separator):
+    def get_multicommands(self, command_separator: AnyStr) -> "CmdBuilder":
         """
         Allows emulator to split multiple commands separated by a defined command separator, e.g. ";".
         Must be accompanied by stream device methods. See Keithley 2700 for examples
@@ -308,7 +309,5 @@ def get_multicommands(self, command_separator):
         :param command_separator: Character(s) that separate commands
         :return: builder
         """
-        self.arg("[^" + re.escape(command_separator) + "]*").escape(
-            command_separator
-        ).arg(".*")
+        self.arg("[^" + re.escape(command_separator) + "]*").escape(command_separator).arg(".*")
         return self
diff --git a/lewis/utils/replies.py b/lewis/utils/replies.py
index 9a795ccc..22804d6f 100644
--- a/lewis/utils/replies.py
+++ b/lewis/utils/replies.py
@@ -19,11 +19,16 @@
 
 import time
 from functools import wraps
+from typing import Callable, ParamSpec, TypeVar
 
+from lewis.adapters.stream import StreamInterface
 from lewis.core.logging import has_log
 
+T = TypeVar("T")
+P = ParamSpec("P")
 
-def _get_device_from(instance):
+
+def _get_device_from(instance: StreamInterface):
     try:
         device = instance.device
     except AttributeError:
@@ -36,10 +41,12 @@ def _get_device_from(instance):
     return device
 
 
-def conditional_reply(property_name, reply=None):
+def conditional_reply(property_name: str, reply: str | None = None) -> Callable[P, T]:
     """
-    Decorator that executes the command and replies if the device has a member called 'property name' and it is True in
-    a boolean context. Example usage:
+    Decorator that executes the command and replies if the device has a member called
+    'property name' and it is True in a boolean context.
+
+    Example usage:
 
     .. sourcecode:: Python
 
@@ -50,26 +57,25 @@ def acknowledge_pressure(channel):
     :param property_name: The name of the property to look for on the device
     :param reply: Desired output reply string when condition is false
 
-    :return: The function returns as normal if property is true. The command is not executed and there is no reply if
-       property is false
+    :return: The function returns as normal if property is true.
+     The command is not executed and there is no reply if property is false
 
-    :except AttributeError if the first argument of the decorated function (self) does not contain .device or ._device
+    :except AttributeError if the first argument of the decorated function (self)
+    does not contain .device or ._device
     :except AttributeError if the device does not contain a property called property_name
     """
 
-    def decorator(func):
+    def decorator(func: Callable[P, T]) -> Callable[P, T]:
         @wraps(func)
-        def wrapper(self, *args, **kwargs):
-
+        def wrapper(self: StreamInterface, *args: P.args, **kwargs: P.kwargs) -> T:
             device = _get_device_from(self)
 
             try:
                 do_reply = getattr(device, property_name)
             except AttributeError:
                 raise AttributeError(
-                    "Expected device to contain an attribute called '{}' but it wasn't found.".format(
-                        property_name
-                    )
+                    f"Expected device to contain an attribute called '{property_name}' "
+                    f"but it wasn't found."
                 )
 
             return func(self, *args, **kwargs) if do_reply else reply
@@ -84,10 +90,14 @@ class _LastInput:
 
 
 @has_log
-def timed_reply(action, reply=None, minimum_time_delay=0):
+def timed_reply(
+    action: str, reply: str | None = None, minimum_time_delay: float = 0
+) -> Callable[P, T]:
     """
-    Decorator that inhibits a command and performs a action if call time is less than than some minimum time delay
-    between the the current and last input.  Example usage:
+    Decorator that inhibits a command and performs an action if call time is less than
+    some minimum time delay between the current and last input.
+
+    Example usage:
 
     .. sourcecode:: Python
 
@@ -99,16 +109,18 @@ def acknowledge_pressure(channel):
     :param reply: Desired output reply string when input time delay is less than the minimum
     :param minimum_time_delay: The minimum time (ms) between commands sent to the device
 
-    :return: The function returns as normal if minimum delay exceeded. The command is not executed and the action method
-     is called on the device instead
+    :return: The function returns as normal if minimum delay exceeded.
+      The command is not executed and the action method is called on the device instead
+
+    :except AttributeError if the first argument of the decorated function (self)
+      does not contain .device or ._device
 
-    :except AttributeError if the first argument of the decorated function (self) does not contain .device or ._device
     :except AttributeError if the device does not contain a property called action
     """
 
-    def decorator(func):
+    def decorator(func: Callable[P, T]) -> Callable[P, T]:
         @wraps(func)
-        def wrapper(self, *args, **kwargs):
+        def wrapper(self: StreamInterface, *args: P.args, **kwargs: P.kwargs) -> T:
             try:
                 new_input_time = int(round(time.time() * 1000))
                 time_since_last_request = new_input_time - _LastInput.last_input_time
@@ -118,9 +130,9 @@ def wrapper(self, *args, **kwargs):
                     return func(self, *args, **kwargs)
                 else:
                     self.log.info(
-                        "Violated time tolerance ({}ms) was {}ms. Calling action ({}) on device".format(
-                            minimum_time_delay, time_since_last_request, action
-                        )
+                        f"Violated time tolerance ({minimum_time_delay}ms) was"
+                        f" {time_since_last_request}ms."
+                        f" Calling action ({action}) on device"
                     )
                     device = _get_device_from(self)
                     action_function = getattr(device, action)
@@ -129,9 +141,8 @@ def wrapper(self, *args, **kwargs):
 
             except AttributeError:
                 raise AttributeError(
-                    "Expected device to contain an attribute called '{}' but it wasn't found.".format(
-                        self.action
-                    )
+                    f"Expected device to contain an attribute called '{self.action}' but it"
+                    f" wasn't found."
                 )
 
         return wrapper
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..e143af6e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,69 @@
+[project]
+name="lewis"
+description="Lewis - Let's write intricate simulators!"
+requires-python="<3.12"
+readme="README.md"
+classifiers=[
+        "Development Status :: 5 - Production/Stable",
+
+        "Environment :: Console",
+
+        "Intended Audience :: Developers",
+        "Intended Audience :: Science/Research",
+
+        "Topic :: Scientific/Engineering",
+        "Topic :: Software Development :: Libraries",
+
+        "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
+
+        "Operating System :: OS Independent",
+
+        "Programming Language :: Python :: 3",
+    ]
+license= {file = "LICENSE"}
+dynamic=["version"]
+authors=[
+    {name = "ScreamingUdder"},
+    {name = "ISIS Experiment Controls", email="ISISExperimentControls@stfc.ac.uk"}
+]
+keywords=["hardware", "controls", "epics", "simulation"]
+dependencies=[
+    "pyzmq",
+    "json-rpc",
+    "semantic_version",
+    "PyYAML",
+    "scanf"
+]
+
+[project.optional-dependencies]
+epics = ["pcaspy"]
+doc = [
+    "sphinx",
+    "sphinx_rtd_theme",
+    "myst_parser",
+    "sphinx-autobuild",
+]
+dev = [
+    "parameterized",
+    "lewis[doc]",
+    "pytest",
+    "pytest-cov",
+    "coverage",
+    "tox",
+    "approvaltests",
+    "pytest-approvaltests",
+    "ruff",
+    "pyright"
+]
+
+[project.scripts]
+lewis = "lewis.scripts.run:run_simulation"
+lewis-control = "lewis.scripts.control:control_simulation"
+
+[project.urls]
+"Homepage" = "https://github.com/ISISComputingGroup/lewis"
+"Bug Reports" = "https://github.com/ISISComputingGroup/lewis/issues"
+"Source" = "https://github.com/ISISComputingGroup/lewis"
+
+[tool.setuptools.packages.find]
+exclude=["tests", "tests.*"]
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index 376384f4..00000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,16 +0,0 @@
--r requirements.txt
-approvaltests
-isort
-mock
-parameterized
-pre-commit
-pytest
-pytest-cov
-coverage
-tox==3.27.1
-tox-pyenv
-twine
-wheel
-
-black==22.6.0
-flake8==3.8.4
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index b1d1654e..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-pyzmq
-json-rpc
-semantic_version
-PyYAML
-scanf
-
-# If you want to use EPICS based devices, uncomment the pcaspy line.
-# It requires a working EPICS installation, please refer to the
-# installation instructions: https://pcaspy.readthedocs.io/en/latest/installation.html
-#pcaspy
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 76b32aef..00000000
--- a/setup.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# *********************************************************************
-# lewis - a library for creating hardware device simulators
-# Copyright (C) 2016-2021 European Spallation Source ERIC
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-# *********************************************************************
-
-from setuptools import find_packages, setup
-
-
-# as suggested on http://python-packaging.readthedocs.io/en/latest/metadata.html
-def readme():
-    with open("README.md") as f:
-        return f.read()
-
-
-setup(
-    name="lewis",
-    version="1.3.3",
-    description="Lewis - Let's write intricate simulators!",
-    long_description=readme(),
-    url="https://github.com/ess-dmsc/lewis",
-    author="ScreamingUdder",
-    license="GPL v3",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Environment :: Console",
-        "Intended Audience :: Developers",
-        "Intended Audience :: Science/Research",
-        "Topic :: Scientific/Engineering",
-        "Topic :: Software Development :: Libraries",
-        "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
-        "Operating System :: OS Independent",
-        "Programming Language :: Python :: 3",
-    ],
-    keywords="hardware simulation controls",
-    packages=find_packages(exclude=["tests", "tests.*"]),
-    python_requires=">=3.7.0",
-    install_requires=["pyzmq", "json-rpc", "semantic_version", "PyYAML", "scanf"],
-    extras_require={
-        "epics": ["pcaspy"],
-        "dev": [
-            "flake8",
-            "mock",
-            "sphinx",
-            "sphinx_rtd_theme",
-            "pytest",
-            "pytest-cov",
-            "coverage",
-            "tox",
-            "approvaltests",
-            "pytest-approvaltests",
-        ],
-    },
-    entry_points={
-        "console_scripts": [
-            "lewis=lewis.scripts.run:run_simulation",
-            "lewis-control=lewis.scripts.control:control_simulation",
-        ],
-    },
-)
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 088b699c..00000000
--- a/tox.ini
+++ /dev/null
@@ -1,27 +0,0 @@
-[tox]
-envlist = py37, py38, py39, py310, py311, flake8, coverage
-isolated_build = true
-skipsdist=true
-
-[testenv]
-description = Run unit tests, arguments are forwarded to pytest.
-deps =
-    -r{toxinidir}/requirements-dev.txt
-commands =
-    python -m pytest tests system_tests/lewis_tests.py {posargs}
-
-[testenv:flake8]
-description = Run flake8.
-deps =
-    -r{toxinidir}/requirements-dev.txt
-commands =
-    python -m flake8 setup.py lewis scripts system_tests tests
-
-[testenv:coverage]
-description = Run unit tests with Python and collect coverage data.
-basepython =
-    python3.9
-deps =
-    -r{toxinidir}/requirements-dev.txt
-commands =
-    python -m pytest --cov=lewis.core --cov=lewis.devices tests


Test patch:
diff --git a/.github/workflows/Lint-and-test.yml b/.github/workflows/Lint-and-test.yml
new file mode 100644
index 00000000..68f87bc4
--- /dev/null
+++ b/.github/workflows/Lint-and-test.yml
@@ -0,0 +1,38 @@
+name: Lint-and-test
+on: [pull_request, workflow_call]
+jobs:
+  call-linter-workflow:
+    uses: ISISComputingGroup/reusable-workflows/.github/workflows/linters.yml@main
+    with:
+      compare-branch: origin/main
+      python-ver: '3.11'
+  tests:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        version: ['3.10','3.11']
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.version }}
+      - name: install requirements
+        run: pip install -e .[dev]
+      - name: run unit tests
+        run: python -m pytest .
+      - name: run system tests
+        working-directory: ./system_tests
+        run: python -m pytest --approvaltests-use-reporter='PythonNativeReporter' lewis_tests.py 
+  results:
+    if: ${{ always() }}
+    runs-on: ubuntu-latest
+    name: Final Results
+    needs: [call-linter-workflow, tests]
+    steps:
+    - run: exit 1
+      # see https://stackoverflow.com/a/67532120/4907315
+      if: >-
+          ${{
+               contains(needs.*.result, 'failure')
+            || contains(needs.*.result, 'cancelled')
+          }}
diff --git a/system_tests/lewis_tests.py b/system_tests/lewis_tests.py
index 30a58243..952785f1 100644
--- a/system_tests/lewis_tests.py
+++ b/system_tests/lewis_tests.py
@@ -33,9 +33,7 @@ def julabo_simulation():
 
 
 def run_control_command(mode, command, value):
-    subprocess.check_output(
-        ["python", str(LEWIS_CONTROL_PATH), mode, command, value]
-    ).decode()
+    subprocess.check_output(["python", str(LEWIS_CONTROL_PATH), mode, command, value]).decode()
 
 
 def santise_whitespace(input_str):
@@ -58,9 +56,7 @@ def test_list_available_devices(self):
         When: running Lewis without parameters
         Then: returns a list of possible simulations
         """
-        result = santise_whitespace(
-            subprocess.check_output(["python", str(LEWIS_PATH)]).decode()
-        )
+        result = santise_whitespace(subprocess.check_output(["python", str(LEWIS_PATH)]).decode())
 
         verify(result, self.reporter)
 
diff --git a/tests/test_CanProcess.py b/tests/test_CanProcess.py
index b4865ae9..95c56cbe 100644
--- a/tests/test_CanProcess.py
+++ b/tests/test_CanProcess.py
@@ -18,8 +18,7 @@
 # *********************************************************************
 
 import unittest
-
-from mock import call, patch
+from unittest.mock import call, patch
 
 from lewis.core.processor import CanProcess, CanProcessComposite
 
@@ -28,77 +27,73 @@ class TestCanProcess(unittest.TestCase):
     def test_process_calls_doProcess(self):
         processor = CanProcess()
 
-        with patch.object(processor, "doProcess", create=True) as doProcessMock:
+        with patch.object(processor, "doProcess", create=True) as do_process_mock:
             processor.process(1.0)
 
-        doProcessMock.assert_called_once_with(1.0)
+        do_process_mock.assert_called_once_with(1.0)
 
     def test_process_calls_doBeforeProcess_only_if_doProcess_is_present(self):
         processor = CanProcess()
 
-        with patch.object(
-            processor, "doBeforeProcess", create=True
-        ) as doBeforeProcessMock:
+        with patch.object(processor, "doBeforeProcess", create=True) as do_before_process_mock:
             processor.process(1.0)
 
-            doBeforeProcessMock.assert_not_called()
+            do_before_process_mock.assert_not_called()
 
             with patch.object(processor, "doProcess", create=True):
                 processor.process(2.0)
 
-            doBeforeProcessMock.assert_called_once_with(2.0)
+            do_before_process_mock.assert_called_once_with(2.0)
 
     def test_process_calls_doAfterProcess_only_if_doProcess_is_present(self):
         processor = CanProcess()
 
-        with patch.object(processor, "doAfterProcess", create=True) as doAfterProcess:
+        with patch.object(processor, "doAfterProcess", create=True) as do_after_process:
             processor.process(1.0)
 
-            doAfterProcess.assert_not_called()
+            do_after_process.assert_not_called()
 
             with patch.object(processor, "doProcess", create=True):
                 processor.process(2.0)
 
-            doAfterProcess.assert_called_once_with(2.0)
+            do_after_process.assert_called_once_with(2.0)
 
     @patch.object(CanProcess, "process")
-    def test_call_invokes_process(self, processMock):
+    def test_call_invokes_process(self, process_mock):
         processor = CanProcess()
 
         processor(45.0)
 
-        processMock.assert_called_once_with(45.0)
+        process_mock.assert_called_once_with(45.0)
 
 
 class TestCanProcessComposite(unittest.TestCase):
     def test_process_calls_doBeforeProcess_if_present(self):
         composite = CanProcessComposite()
 
-        with patch.object(
-            composite, "doBeforeProcess", create=True
-        ) as doBeforeProcessMock:
+        with patch.object(composite, "doBeforeProcess", create=True) as do_before_process_mock:
             composite.process(3.0)
 
-        doBeforeProcessMock.assert_called_once_with(3.0)
+        do_before_process_mock.assert_called_once_with(3.0)
 
     def test_addProcessor_if_argument_CanProcess(self):
         composite = CanProcessComposite()
 
-        with patch.object(composite, "_append_processor") as appendProcessorMock:
+        with patch.object(composite, "_append_processor") as append_processor_mock:
             composite.add_processor(CanProcess())
 
-        self.assertEqual(appendProcessorMock.call_count, 1)
+        self.assertEqual(append_processor_mock.call_count, 1)
 
     def test_addProcessor_if_argument_not_CanProcess(self):
         composite = CanProcessComposite()
 
-        with patch.object(composite, "_append_processor") as appendProcessorMock:
+        with patch.object(composite, "_append_processor") as append_processor_mock:
             composite.add_processor(None)
 
-        appendProcessorMock.assert_not_called()
+        append_processor_mock.assert_not_called()
 
     def test_init_from_iterable(self):
-        with patch.object(CanProcess, "doProcess", create=True) as mockProcessMethod:
+        with patch.object(CanProcess, "doProcess", create=True) as mock_process_method:
             devices = (
                 CanProcess(),
                 CanProcess(),
@@ -107,4 +102,4 @@ def test_init_from_iterable(self):
             composite = CanProcessComposite(devices)
             composite(4.0)
 
-            mockProcessMethod.assert_has_calls([call(4.0), call(4.0)])
+            mock_process_method.assert_has_calls([call(4.0), call(4.0)])
diff --git a/tests/test_SimulatedChopper.py b/tests/test_SimulatedChopper.py
index 99021cb4..0090c49f 100644
--- a/tests/test_SimulatedChopper.py
+++ b/tests/test_SimulatedChopper.py
@@ -44,8 +44,6 @@ def test_invalid_transition_override_fails(self):
         )
 
     def test_valid_transition_override_does_not_fail(self):
-        chopper = SimulatedChopper(
-            override_transitions={("idle", "stopping"): lambda: True}
-        )
+        chopper = SimulatedChopper(override_transitions={("idle", "stopping"): lambda: True})
 
         self.assertIsInstance(chopper, SimulatedChopper)
diff --git a/tests/test_SimulatedLinkamT95.py b/tests/test_SimulatedLinkamT95.py
index 63ddf3d1..6e965392 100644
--- a/tests/test_SimulatedLinkamT95.py
+++ b/tests/test_SimulatedLinkamT95.py
@@ -53,12 +53,8 @@ def test_default_status(self):
         self.assertEqual(len(status_bytes), 10, "Byte array should always be 10 bytes")
         self.assertFalse(b"\x00" in status_bytes, "Byte array may not contain zeroes")
         self.assertEqual(0x01, status_bytes[0], "Status byte should be 1 on startup")
-        self.assertEqual(
-            0x80, status_bytes[1], "No error flags should be set on startup"
-        )
-        self.assertEqual(
-            0x80, status_bytes[2], "The pump should not be active on startup"
-        )
+        self.assertEqual(0x80, status_bytes[1], "No error flags should be set on startup")
+        self.assertEqual(0x80, status_bytes[2], "The pump should not be active on startup")
         self.assertEqual(b"00f0", status_bytes[6:10], "Starting temperature 24C")
 
     def test_simple_heat(self):
diff --git a/tests/test_StateMachine.py b/tests/test_StateMachine.py
index 588cbe83..1829bfa8 100644
--- a/tests/test_StateMachine.py
+++ b/tests/test_StateMachine.py
@@ -18,8 +18,7 @@
 # *********************************************************************
 
 import unittest
-
-from mock import Mock, patch
+from unittest.mock import Mock, patch
 
 from lewis.core.statemachine import (
     State,
@@ -50,9 +49,7 @@ def test_first_cycle_transitions_to_initial(self):
         )
 
     def test_can_transition_with_lambda(self):
-        sm = StateMachine(
-            {"initial": "foo", "transitions": {("foo", "bar"): lambda: True}}
-        )
+        sm = StateMachine({"initial": "foo", "transitions": {("foo", "bar"): lambda: True}})
 
         self.assertIsNone(sm.state)
         sm.process(0.1)
@@ -62,9 +59,7 @@ def test_can_transition_with_lambda(self):
 
     def test_can_transition_with_callable(self):
         transition = Mock(return_value=True)
-        sm = StateMachine(
-            {"initial": "foo", "transitions": {("foo", "bar"): transition}}
-        )
+        sm = StateMachine({"initial": "foo", "transitions": {("foo", "bar"): transition}})
 
         transition.assert_not_called()
         self.assertIsNone(sm.state)
@@ -78,9 +73,7 @@ def test_can_transition_with_callable(self):
     @patch.object(Transition, "__call__", return_value=True)
     def test_can_transition_with_Transition(self, tr_call):
         transition = Transition()
-        sm = StateMachine(
-            {"initial": "foo", "transitions": {("foo", "bar"): transition}}
-        )
+        sm = StateMachine({"initial": "foo", "transitions": {("foo", "bar"): transition}})
 
         tr_call.assert_not_called()
         self.assertIsNone(sm.state)
@@ -223,9 +216,7 @@ def test_State_receives_Context(self):
 
     def test_bind_handlers_by_name_default_behaviour(self):
         target = Mock()
-        sm = StateMachine(
-            {"initial": "foo", "transitions": {("foo", "bar"): lambda: True}}
-        )
+        sm = StateMachine({"initial": "foo", "transitions": {("foo", "bar"): lambda: True}})
         sm.bind_handlers_by_name(target)
 
         # First cycle enters and executes initial state, but forces delta T to zero
@@ -254,9 +245,7 @@ def test_bind_handlers_by_name_default_behaviour(self):
 
     def test_bind_handlers_by_name_custom_prefix(self):
         target = Mock()
-        sm = StateMachine(
-            {"initial": "foo", "transitions": {("foo", "bar"): lambda: True}}
-        )
+        sm = StateMachine({"initial": "foo", "transitions": {("foo", "bar"): lambda: True}})
         sm.bind_handlers_by_name(
             target,
             prefix={
@@ -297,9 +286,7 @@ def test_bind_handlers_by_name_override(self):
         # second target will 'override' the events for bar
         second = Mock(spec=["_on_entry_bar", "_in_state_bar", "_on_exit_bar"])
 
-        sm = StateMachine(
-            {"initial": "foo", "transitions": {("foo", "bar"): lambda: True}}
-        )
+        sm = StateMachine({"initial": "foo", "transitions": {("foo", "bar"): lambda: True}})
         sm.bind_handlers_by_name(first)
         sm.bind_handlers_by_name(second, override=True)
 
@@ -333,9 +320,7 @@ def test_bind_handlers_by_name_override(self):
         first._on_exit_bar.assert_not_called()
 
     def test_reset(self):
-        sm = StateMachine(
-            {"initial": "foo", "transitions": {("foo", "bar"): lambda: True}}
-        )
+        sm = StateMachine({"initial": "foo", "transitions": {("foo", "bar"): lambda: True}})
 
         self.assertIsNone(sm.state)
         sm.reset()
diff --git a/tests/test_StateMachineDevice.py b/tests/test_StateMachineDevice.py
index 73a4b7cf..48c608fd 100644
--- a/tests/test_StateMachineDevice.py
+++ b/tests/test_StateMachineDevice.py
@@ -19,8 +19,7 @@
 
 import itertools
 import unittest
-
-from mock import MagicMock, Mock, call, patch
+from unittest.mock import MagicMock, Mock, call, patch
 
 from lewis.devices import StateMachineDevice
 
@@ -71,9 +70,7 @@ def test_invalid_initial_override_fails(self):
         assertRaisesNothing(self, MockStateMachineDevice, override_initial_state="init")
         assertRaisesNothing(self, MockStateMachineDevice, override_initial_state="test")
 
-        self.assertRaises(
-            RuntimeError, MockStateMachineDevice, override_initial_state="invalid"
-        )
+        self.assertRaises(RuntimeError, MockStateMachineDevice, override_initial_state="invalid")
 
     def test_overriding_undefined_data_fails(self):
         assertRaisesNothing(
diff --git a/tests/test_control_client.py b/tests/test_control_client.py
index b9cc6ba5..6516f11c 100644
--- a/tests/test_control_client.py
+++ b/tests/test_control_client.py
@@ -18,9 +18,9 @@
 # *********************************************************************
 
 import unittest
+from unittest.mock import Mock, call, patch
 
 import zmq
-from mock import Mock, call, patch
 
 from lewis.core.control_client import (
     ControlClient,
@@ -72,9 +72,7 @@ def test_json_rpc(self, mock_socket, mock_uuid):
             [
                 call(),
                 call().connect("tcp://127.0.0.1:10001"),
-                call().send_json(
-                    {"method": "foo", "params": (), "jsonrpc": "2.0", "id": "2"}
-                ),
+                call().send_json({"method": "foo", "params": (), "jsonrpc": "2.0", "id": "2"}),
                 call().recv_json(),
             ]
         )
@@ -139,27 +137,21 @@ def test_get_remote_object_collection(self, mock_socket):
 class TestObjectProxy(unittest.TestCase):
     def test_init_adds_members(self):
         mock_connection = Mock()
-        obj = type("TestType", (ObjectProxy,), {})(
-            mock_connection, ["a:get", "a:set", "setTest"]
-        )
+        obj = type("TestType", (ObjectProxy,), {})(mock_connection, ["a:get", "a:set", "setTest"])
         self.assertTrue(hasattr(type(obj), "a"))
         self.assertTrue(hasattr(obj, "setTest"))
 
         mock_connection.assert_not_called()
 
     def test_member_access_calls_make_request(self):
-        obj = type("TestType", (ObjectProxy,), {})(
-            Mock(), ["a:get", "a:set", "setTest"]
-        )
+        obj = type("TestType", (ObjectProxy,), {})(Mock(), ["a:get", "a:set", "setTest"])
 
         with patch.object(obj, "_make_request") as request_mock:
             obj.a
             obj.a = 4
             obj.setTest()
 
-        request_mock.assert_has_calls(
-            [call("a:get"), call("a:set", 4), call("setTest")]
-        )
+        request_mock.assert_has_calls([call("a:get"), call("a:set", 4), call("setTest")])
 
     def test_response_without_id_raises_exception(self):
         mock_connection = Mock(ControlClient)
@@ -189,9 +181,7 @@ def test_make_request_with_known_exception(self):
         mock_connection = Mock(ControlClient)
         mock_connection.json_rpc.return_value = (
             {
-                "error": {
-                    "data": {"type": "AttributeError", "message": "Some message"}
-                },
+                "error": {"data": {"type": "AttributeError", "message": "Some message"}},
                 "id": 2,
             },
             2,
@@ -206,9 +196,7 @@ def test_make_request_with_unknown_exception(self):
         mock_connection = Mock(ControlClient)
         mock_connection.json_rpc.return_value = (
             {
-                "error": {
-                    "data": {"type": "NonExistingException", "message": "Some message"}
-                },
+                "error": {"data": {"type": "NonExistingException", "message": "Some message"}},
                 "id": 2,
             },
             2,
diff --git a/tests/test_control_server.py b/tests/test_control_server.py
index c2e065e7..c35f3354 100644
--- a/tests/test_control_server.py
+++ b/tests/test_control_server.py
@@ -19,9 +19,9 @@
 
 import socket
 import unittest
+from unittest.mock import Mock, call, patch
 
 import zmq
-from mock import Mock, call, patch
 
 from lewis.core.control_server import (
     ControlServer,
@@ -83,9 +83,7 @@ def test_excluded_methods_not_exposed(self):
             self.assertTrue(method in rpc_object)
 
     def test_selected_and_excluded_methods(self):
-        rpc_object = ExposedObject(
-            DummyObject(), members=("a", "getTest"), exclude=("a")
-        )
+        rpc_object = ExposedObject(DummyObject(), members=("a", "getTest"), exclude=("a"))
 
         expected_methods = [":api", "getTest"]
         self.assertEqual(len(rpc_object), len(expected_methods))
@@ -94,9 +92,7 @@ def test_selected_and_excluded_methods(self):
             self.assertTrue(method in rpc_object)
 
     def test_inherited_not_exposed(self):
-        rpc_object = ExposedObject(
-            DummyObjectChild(), members=("a", "c"), exclude_inherited=True
-        )
+        rpc_object = ExposedObject(DummyObjectChild(), members=("a", "c"), exclude_inherited=True)
 
         expected_methods = [":api", "c:get", "c:set"]
         self.assertEqual(len(rpc_object), len(expected_methods))
@@ -114,9 +110,7 @@ def test_inherited_exposed(self):
             self.assertTrue(method in rpc_object)
 
     def test_invalid_method_raises(self):
-        self.assertRaises(
-            AttributeError, ExposedObject, DummyObject(), ("nonExisting",)
-        )
+        self.assertRaises(AttributeError, ExposedObject, DummyObject(), ("nonExisting",))
 
     def test_attribute_wrapper_gets_value(self):
         obj = DummyObject()
@@ -181,9 +175,7 @@ def test_empty_initialization(self):
         self.assertEqual(set(exposed_objects), {":api", "get_objects"})
 
         self.assertEqual(len(exposed_objects.get_objects()), 0)
-        self.assertEqual(
-            exposed_objects["get_objects"](), exposed_objects.get_objects()
-        )
+        self.assertEqual(exposed_objects["get_objects"](), exposed_objects.get_objects())
 
     def test_api(self):
         exposed_objects = ExposedObjectCollection(named_objects={})
@@ -233,9 +225,7 @@ def test_duplicate_name_raises(self):
         exposed_objects = ExposedObjectCollection({})
         exposed_objects.add_object(DummyObject(), "testObject")
 
-        self.assertRaises(
-            RuntimeError, exposed_objects.add_object, DummyObject(), "testObject"
-        )
+        self.assertRaises(RuntimeError, exposed_objects.add_object, DummyObject(), "testObject")
 
     def test_remove_object(self):
         exposed_objects = ExposedObjectCollection({})
@@ -299,9 +289,7 @@ def test_process_does_not_block(self):
     def test_exposed_object_is_exposed_directly(self):
         mock_collection = Mock(spec=ExposedObject)
 
-        server = ControlServer(
-            object_map=mock_collection, connection_string="127.0.0.1:10000"
-        )
+        server = ControlServer(object_map=mock_collection, connection_string="127.0.0.1:10000")
         self.assertEqual(server.exposed_object, mock_collection)
 
     @patch("lewis.core.control_server.ExposedObjectCollection")
diff --git a/tests/test_core_adapters.py b/tests/test_core_adapters.py
index 1df4a47d..8be50a34 100644
--- a/tests/test_core_adapters.py
+++ b/tests/test_core_adapters.py
@@ -1,7 +1,6 @@
 import inspect
 import unittest
-
-from mock import MagicMock, Mock
+from unittest.mock import MagicMock, Mock
 
 from lewis.core.adapters import Adapter, AdapterCollection, NoLock
 from lewis.core.exceptions import LewisException
@@ -85,12 +84,8 @@ def test_protocol_is_forwarded_from_interface(self):
         self.assertEqual(adapter.protocol, "foo")
 
     def test_options(self):
-        assertRaisesNothing(
-            self, DummyAdapter, "protocol", options={"bar": 2, "foo": 3}
-        )
-        self.assertRaises(
-            LewisException, DummyAdapter, "protocol", options={"invalid": False}
-        )
+        assertRaisesNothing(self, DummyAdapter, "protocol", options={"bar": 2, "foo": 3})
+        self.assertRaises(LewisException, DummyAdapter, "protocol", options={"invalid": False})
 
 
 class TestAdapterCollection(unittest.TestCase):
diff --git a/tests/test_core_devices.py b/tests/test_core_devices.py
index 649b7466..16a09726 100644
--- a/tests/test_core_devices.py
+++ b/tests/test_core_devices.py
@@ -112,9 +112,7 @@ def test_create_interface(self):
         builder = DeviceBuilder(self.module)
 
         self.assertIsInstance(builder.create_interface(), self.module.DummyAdapter)
-        self.assertIsInstance(
-            builder.create_interface("dummy"), self.module.DummyAdapter
-        )
+        self.assertIsInstance(builder.create_interface("dummy"), self.module.DummyAdapter)
 
         self.assertRaises(LewisException, builder.create_interface, "invalid_protocol")
 
@@ -216,9 +214,7 @@ def test_create_device(self):
 
         self.assertIsInstance(builder.create_device(), self.module.DummyDevice)
         self.assertIsInstance(builder.create_device("default"), self.module.DummyDevice)
-        self.assertIsInstance(
-            builder.create_device("other"), self.module.OtherDummyDevice
-        )
+        self.assertIsInstance(builder.create_device("other"), self.module.OtherDummyDevice)
 
 
 class TestDeviceBuilderWithDuplicateProtocols(unittest.TestCase):
diff --git a/tests/test_simulation.py b/tests/test_simulation.py
index 68c621b4..23a4d547 100644
--- a/tests/test_simulation.py
+++ b/tests/test_simulation.py
@@ -18,8 +18,7 @@
 # *********************************************************************
 
 import unittest
-
-from mock import ANY, MagicMock, Mock, call, patch
+from unittest.mock import ANY, MagicMock, Mock, call, patch
 
 from lewis.core.simulation import Simulation
 
@@ -149,13 +148,9 @@ def test_invalid_control_server_fails(self):
 
     @patch("lewis.core.simulation.ExposedObject")
     @patch("lewis.core.simulation.ControlServer")
-    def test_construct_control_server(
-        self, mock_control_server_type, exposed_object_mock
-    ):
+    def test_construct_control_server(self, mock_control_server_type, exposed_object_mock):
         exposed_object_mock.return_value = "test"
-        assertRaisesNothing(
-            self, Simulation, device=Mock(), control_server="localhost:10000"
-        )
+        assertRaisesNothing(self, Simulation, device=Mock(), control_server="localhost:10000")
 
         mock_control_server_type.assert_called_once_with(
             {"device": "test", "simulation": "test", "interface": "test"},
@@ -207,9 +202,7 @@ def test_cycle_delay_range(self):
     def test_start_stop(self):
         env = Simulation(device=Mock())
 
-        with patch.object(
-            env, "_process_cycle", side_effect=lambda x: env.stop()
-        ) as mock_cycle:
+        with patch.object(env, "_process_cycle", side_effect=lambda x: env.stop()) as mock_cycle:
             env.start()
 
             mock_cycle.assert_has_calls([call(0.0)])
@@ -249,9 +242,7 @@ def test_control_server_setter(self, control_server_mock, exposed_object_mock):
         control_server_mock.return_value.assert_has_calls([call.start_server()])
 
         # Can not replace control server when simulation is running
-        self.assertRaises(
-            RuntimeError, setattr, env, "control_server", "127.0.0.1:10003"
-        )
+        self.assertRaises(RuntimeError, setattr, env, "control_server", "127.0.0.1:10003")
 
     def test_set_parameters(self):
         class TestDevice:
@@ -297,9 +288,7 @@ def create_device(self, setup):
                 raise RuntimeError("Error")
 
         adapter_mock = MagicMock()
-        sim = Simulation(
-            device=Mock(), adapters=adapter_mock, device_builder=MockBuilder()
-        )
+        sim = Simulation(device=Mock(), adapters=adapter_mock, device_builder=MockBuilder())
 
         sim.switch_setup("foo")
 
diff --git a/tests/test_stream.py b/tests/test_stream.py
index eeb55897..72f50859 100644
--- a/tests/test_stream.py
+++ b/tests/test_stream.py
@@ -44,9 +44,7 @@ def setUp(self):
     def test_argument_variations_of_valid_Func_without_return_mapping_is_instance_of_Func(
         self, _, target_member, write_pattern, argument_mapping
     ):
-        self.assertIsInstance(
-            Func(target_member, write_pattern, argument_mapping), Func
-        )
+        self.assertIsInstance(Func(target_member, write_pattern, argument_mapping), Func)
 
     @parameterized.expand(
         [
diff --git a/tests/test_stream_adapter.py b/tests/test_stream_adapter.py
index d3e6e481..6b1704d7 100644
--- a/tests/test_stream_adapter.py
+++ b/tests/test_stream_adapter.py
@@ -1,6 +1,6 @@
 import unittest
+from unittest.mock import MagicMock, patch
 
-from mock import MagicMock, patch
 from parameterized import parameterized
 
 from lewis.adapters.stream import StreamHandler
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4cec141d..2d1f1236 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -20,8 +20,7 @@
 import importlib
 import unittest
 from datetime import datetime
-
-from mock import patch
+from unittest.mock import patch
 
 from lewis.core.exceptions import LewisException, LimitViolationException
 from lewis.core.utils import (
@@ -147,23 +146,23 @@ def test_non_existing_members_in_module_dont_work(self):
         )
 
     def test_non_existing_module_works(self):
-        A, B = FromOptionalDependency("invalid_module").do_import("A", "B")
+        a, b = FromOptionalDependency("invalid_module").do_import("A", "B")
 
-        self.assertEqual(A.__name__, "A")
-        self.assertEqual(B.__name__, "B")
+        self.assertEqual(a.__name__, "A")
+        self.assertEqual(b.__name__, "B")
 
-        self.assertRaises(LewisException, A, "argument_one")
-        self.assertRaises(LewisException, B, "argument_one", "argument_two")
+        self.assertRaises(LewisException, a, "argument_one")
+        self.assertRaises(LewisException, b, "argument_one", "argument_two")
 
     def test_string_exception_is_raised(self):
-        A = FromOptionalDependency("invalid_module", "test").do_import("A")
+        a = FromOptionalDependency("invalid_module", "test").do_import("A")
 
-        self.assertRaises(LewisException, A)
+        self.assertRaises(LewisException, a)
 
     def test_custom_exception_is_raised(self):
-        A = FromOptionalDependency("invalid_module", ValueError("test")).do_import("A")
+        a = FromOptionalDependency("invalid_module", ValueError("test")).do_import("A")
 
-        self.assertRaises(ValueError, A)
+        self.assertRaises(ValueError, a)
 
     def test_exception_does_not_accept_arbitrary_type(self):
         self.assertRaises(RuntimeError, FromOptionalDependency, "invalid_module", 6.0)
@@ -182,9 +181,7 @@ def test_indented_lines_are_cleaned_up(self):
 
     def test_long_lines_are_broken(self):
         text = " ".join(["ab"] * 44)
-        expected = (
-            "    " + " ".join(["ab"] * 32) + "\n" + "    " + " ".join(["ab"] * 12)
-        )
+        expected = "    " + " ".join(["ab"] * 32) + "\n" + "    " + " ".join(["ab"] * 12)
 
         self.assertEqual(format_doc_text(text), expected)