diff --git a/pints/_mcmc/__init__.py b/pints/_mcmc/__init__.py
index c6090196..fa5c8853 100644
--- a/pints/_mcmc/__init__.py
+++ b/pints/_mcmc/__init__.py
@@ -418,6 +418,9 @@ class MCMCController(object):
         self._n_workers = 1
         self.set_parallel()
 
+        # :meth:`run` can only be called once
+        self._has_run = False
+
         #
         # Stopping criteria
         #
@@ -507,6 +510,12 @@ class MCMCController(object):
         If storing chains to memory has been disabled with
         :meth:`set_chain_storage`, then ``None`` is returned instead.
         """
+
+        # Can only run once for each controller instance
+        if self._has_run:
+            raise RuntimeError("Controller is valid for single use only")
+        self._has_run = True
+
         # Check stopping criteria
         has_stopping_criterion = False
         has_stopping_criterion |= (self._max_iterations is not None)
diff --git a/pints/_nested/__init__.py b/pints/_nested/__init__.py
index f19315e6..d06a7ccf 100644
--- a/pints/_nested/__init__.py
+++ b/pints/_nested/__init__.py
@@ -315,6 +315,9 @@ class NestedController(object):
         # Performance metrics
         self._time = None
 
+        # :meth:`run` can only be called once
+        self._has_run = False
+
     def active_points(self):
         """
         Returns the active points from nested sampling.
@@ -522,6 +525,11 @@ class NestedController(object):
         samples and an estimate of the marginal likelihood.
         """
 
+        # Can only run once for each controller instance
+        if self._has_run:
+            raise RuntimeError("Controller is valid for single use only")
+        self._has_run = True
+
         # Choose method to evaluate
         f = self._initialise_callable()
 
diff --git a/pints/_optimisers/__init__.py b/pints/_optimisers/__init__.py
index 66dc177f..6b72063e 100644
--- a/pints/_optimisers/__init__.py
+++ b/pints/_optimisers/__init__.py
@@ -382,6 +382,9 @@ class OptimisationController(object):
         self._n_workers = 1
         self.set_parallel()
 
+        # :meth:`run` can only be called once
+        self._has_run = False
+
         #
         # Stopping criteria
         #
@@ -452,6 +455,11 @@ class OptimisationController(object):
         """
         Runs the optimisation, returns a tuple ``(xbest, fbest)``.
         """
+        # Can only run once for each controller instance
+        if self._has_run:
+            raise RuntimeError("Controller is valid for single use only")
+        self._has_run = True
+
         # Check stopping criteria
         has_stopping_criterion = False
         has_stopping_criterion |= (self._max_iterations is not None)

Make controllers raise exception if used twice
Now that we've decided they're officially single use objects. See: #640
diff --git a/pints/tests/test_mcmc_controller.py b/pints/tests/test_mcmc_controller.py
index 963a6a35..8657a3ac 100755
--- a/pints/tests/test_mcmc_controller.py
+++ b/pints/tests/test_mcmc_controller.py
@@ -379,6 +379,11 @@ class TestMCMCController(unittest.TestCase):
         self.assertEqual(chains.shape[2], nparameters)
 
         # Test with fixed number of worker processes
+        mcmc = pints.MCMCController(
+            self.log_posterior, nchains, xs,
+            method=pints.HaarioBardenetACMC)
+        mcmc.set_max_iterations(niterations)
+        mcmc.set_log_to_screen(debug)
         mcmc.set_parallel(5)
         mcmc.set_log_to_screen(True)
         self.assertIs(mcmc._parallel, True)
@@ -614,6 +619,22 @@ class TestMCMCController(unittest.TestCase):
             self.log_posterior, 1, [self.real_parameters])
         self.assertIsInstance(mcmc, pints.MCMCController)
 
+    def test_exception_on_multi_use(self):
+        # Controller should raise an exception if use multiple times
+
+        # Test simple run
+        n_chains = 1
+        n_iterations = 10
+        x0 = np.array(self.real_parameters) * 1.1
+        xs = [x0]
+        mcmc = pints.MCMCController(self.log_posterior, n_chains, xs)
+        mcmc.set_max_iterations(n_iterations)
+        mcmc.set_log_to_screen(False)
+        mcmc.run()
+        with self.assertRaisesRegex(
+                RuntimeError, 'Controller is valid for single use only'):
+            mcmc.run()
+
     def test_post_run_statistics(self):
         # Test method to obtain post-run statistics
 
diff --git a/pints/tests/test_nested_controller.py b/pints/tests/test_nested_controller.py
index ed8b1505..491abb0b 100755
--- a/pints/tests/test_nested_controller.py
+++ b/pints/tests/test_nested_controller.py
@@ -131,6 +131,8 @@ class TestNestedController(unittest.TestCase):
         sampler.run()
 
         # Test with fixed number of worker processes
+        sampler = pints.NestedController(
+            self.log_likelihood, self.log_prior)
         sampler.set_parallel(4)
         sampler.set_log_to_screen(False)
         self.assertEqual(sampler.parallel(), 4)
@@ -248,6 +250,8 @@ class TestNestedController(unittest.TestCase):
         logLikelihood1 = sampler.log_likelihood_vector()
         self.assertEqual(len(logLikelihood1), 400 + 100)
         self.assertTrue(ess1 > 0)
+        sampler = pints.NestedController(
+            self.log_likelihood, self.log_prior)
         iter = 2000
         sampler.set_iterations(iter)
         sampler.set_n_posterior_samples(100)
@@ -277,6 +281,8 @@ class TestNestedController(unittest.TestCase):
             self.assertTrue(elem <= 1)
 
         # Acive points
+        sampler = pints.NestedController(
+            self.log_likelihood, self.log_prior)
         sampler.set_iterations(100)
         sampler.set_log_to_screen(False)
         sampler.set_parallel(2)
@@ -324,6 +330,19 @@ class TestNestedController(unittest.TestCase):
         m_inactive = sampler.inactive_points()
         self.assertTrue(m_inactive.shape[0] < 200)
 
+    def test_exception_on_multi_use(self):
+        # Controller should raise an exception if use multiple times
+
+        sampler = pints.NestedController(
+            self.log_likelihood, self.log_prior)
+        sampler.set_n_posterior_samples(2)
+        sampler.set_iterations(10)
+        sampler.set_log_to_screen(False)
+        sampler.run()
+        with self.assertRaisesRegex(
+                RuntimeError, 'Controller is valid for single use only'):
+            sampler.run()
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pints/tests/test_opt_optimisation_controller.py b/pints/tests/test_opt_optimisation_controller.py
index e5a78041..05739797 100755
--- a/pints/tests/test_opt_optimisation_controller.py
+++ b/pints/tests/test_opt_optimisation_controller.py
@@ -302,6 +302,22 @@ class TestOptimisationController(unittest.TestCase):
         self.assertGreater(opt.time(), 0)
         self.assertGreater(t_upper, opt.time())
 
+    def test_exception_on_multi_use(self):
+        # Controller should raise an exception if use multiple times
+
+        r = pints.toy.TwistedGaussianLogPDF(2, 0.01)
+        x = np.array([0, 1.01])
+        b = pints.RectangularBoundaries([-0.01, 0.95], [0.01, 1.05])
+        s = 0.01
+        opt = pints.OptimisationController(r, x, s, b, method=method)
+        opt.set_log_to_screen(False)
+        opt.set_max_unchanged_iterations(None)
+        opt.set_max_iterations(10)
+        opt.run()
+        with self.assertRaisesRegex(RuntimeError,
+                                    "Controller is valid for single use only"):
+            opt.run()
+
 
 if __name__ == '__main__':
     print('Add -v for more debug output')

pip install -e .[dev,docs]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
anyio==4.9.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==3.0.0
async-lru==2.0.5
attrs==25.3.0
babel==2.17.0
beautifulsoup4==4.13.3
bleach==6.2.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
cma==4.0.0
comm==0.2.2
contourpy==1.3.0
cycler==0.12.1
debugpy==1.8.13
decorator==5.2.1
defusedxml==0.7.1
docutils==0.21.2
exceptiongroup==1.2.2
executing==2.2.0
fastjsonschema==2.21.1
flake8==7.2.0
fonttools==4.56.0
fqdn==1.5.1
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
importlib_resources==6.5.2
iniconfig==2.1.0
ipykernel==6.29.5
ipython==8.18.1
ipywidgets==8.1.5
isoduration==20.11.0
jedi==0.19.2
Jinja2==3.1.6
json5==0.10.0
jsonpointer==3.0.0
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
jupyter==1.1.1
jupyter-console==6.6.3
jupyter-events==0.12.0
jupyter-lsp==2.2.5
jupyter_client==8.6.3
jupyter_core==5.7.2
jupyter_server==2.15.0
jupyter_server_terminals==0.5.3
jupyterlab==4.3.6
jupyterlab_pygments==0.3.0
jupyterlab_server==2.27.3
jupyterlab_widgets==3.0.13
kiwisolver==1.4.7
MarkupSafe==3.0.2
matplotlib==3.9.4
matplotlib-inline==0.1.7
mccabe==0.7.0
mistune==3.1.3
nbclient==0.10.2
nbconvert==7.16.6
nbformat==5.10.4
nest-asyncio==1.6.0
notebook==7.3.3
notebook_shim==0.2.4
numpy==2.0.2
overrides==7.7.0
packaging==24.2
pandocfilters==1.5.1
parso==0.8.4
pexpect==4.9.0
pillow==11.1.0
-e git+https://github.com/pints-team/pints.git@ad9f037ac8a62095825a887fbe87d49d978e9830#egg=pints
platformdirs==4.3.7
pluggy==1.5.0
prometheus_client==0.21.1
prompt_toolkit==3.0.50
psutil==7.0.0
ptyprocess==0.7.0
pure_eval==0.2.3
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.1
Pygments==2.19.1
pyparsing==3.2.3
pytest==8.3.5
python-dateutil==2.9.0.post0
python-json-logger==3.3.0
PyYAML==6.0.2
pyzmq==26.3.0
referencing==0.36.2
requests==2.32.3
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rpds-py==0.24.0
scipy==1.13.1
Send2Trash==1.8.3
six==1.17.0
sniffio==1.3.1
snowballstemmer==2.2.0
soupsieve==2.6
Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
stack-data==0.6.3
tabulate==0.9.0
terminado==0.18.1
tinycss2==1.4.0
tomli==2.2.1
tornado==6.4.2
traitlets==5.14.3
types-python-dateutil==2.9.0.20241206
typing_extensions==4.13.0
uri-template==1.3.0
urllib3==2.3.0
wcwidth==0.2.13
webcolors==24.11.1
webencodings==0.5.1
websocket-client==1.8.0
widgetsnbextension==4.0.13
zipp==3.21.0

name: pints
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - anyio==4.9.0
      - argon2-cffi==23.1.0
      - argon2-cffi-bindings==21.2.0
      - arrow==1.3.0
      - asttokens==3.0.0
      - async-lru==2.0.5
      - attrs==25.3.0
      - babel==2.17.0
      - beautifulsoup4==4.13.3
      - bleach==6.2.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - cma==4.0.0
      - comm==0.2.2
      - contourpy==1.3.0
      - cycler==0.12.1
      - debugpy==1.8.13
      - decorator==5.2.1
      - defusedxml==0.7.1
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - executing==2.2.0
      - fastjsonschema==2.21.1
      - flake8==7.2.0
      - fonttools==4.56.0
      - fqdn==1.5.1
      - h11==0.14.0
      - httpcore==1.0.7
      - httpx==0.28.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - importlib-resources==6.5.2
      - iniconfig==2.1.0
      - ipykernel==6.29.5
      - ipython==8.18.1
      - ipywidgets==8.1.5
      - isoduration==20.11.0
      - jedi==0.19.2
      - jinja2==3.1.6
      - json5==0.10.0
      - jsonpointer==3.0.0
      - jsonschema==4.23.0
      - jsonschema-specifications==2024.10.1
      - jupyter==1.1.1
      - jupyter-client==8.6.3
      - jupyter-console==6.6.3
      - jupyter-core==5.7.2
      - jupyter-events==0.12.0
      - jupyter-lsp==2.2.5
      - jupyter-server==2.15.0
      - jupyter-server-terminals==0.5.3
      - jupyterlab==4.3.6
      - jupyterlab-pygments==0.3.0
      - jupyterlab-server==2.27.3
      - jupyterlab-widgets==3.0.13
      - kiwisolver==1.4.7
      - markupsafe==3.0.2
      - matplotlib==3.9.4
      - matplotlib-inline==0.1.7
      - mccabe==0.7.0
      - mistune==3.1.3
      - nbclient==0.10.2
      - nbconvert==7.16.6
      - nbformat==5.10.4
      - nest-asyncio==1.6.0
      - notebook==7.3.3
      - notebook-shim==0.2.4
      - numpy==2.0.2
      - overrides==7.7.0
      - packaging==24.2
      - pandocfilters==1.5.1
      - parso==0.8.4
      - pexpect==4.9.0
      - pillow==11.1.0
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - prometheus-client==0.21.1
      - prompt-toolkit==3.0.50
      - psutil==7.0.0
      - ptyprocess==0.7.0
      - pure-eval==0.2.3
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pyparsing==3.2.3
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - python-json-logger==3.3.0
      - pyyaml==6.0.2
      - pyzmq==26.3.0
      - referencing==0.36.2
      - requests==2.32.3
      - rfc3339-validator==0.1.4
      - rfc3986-validator==0.1.1
      - rpds-py==0.24.0
      - scipy==1.13.1
      - send2trash==1.8.3
      - six==1.17.0
      - sniffio==1.3.1
      - snowballstemmer==2.2.0
      - soupsieve==2.6
      - sphinx==7.4.7
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - stack-data==0.6.3
      - tabulate==0.9.0
      - terminado==0.18.1
      - tinycss2==1.4.0
      - tomli==2.2.1
      - tornado==6.4.2
      - traitlets==5.14.3
      - types-python-dateutil==2.9.0.20241206
      - typing-extensions==4.13.0
      - uri-template==1.3.0
      - urllib3==2.3.0
      - wcwidth==0.2.13
      - webcolors==24.11.1
      - webencodings==0.5.1
      - websocket-client==1.8.0
      - widgetsnbextension==4.0.13
      - zipp==3.21.0
prefix: /opt/conda/envs/pints

pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_exception_on_multi_use
pints/tests/test_mcmc_controller.py::TestMCMCControllerSingleChainStorage::test_multiple_samplers_mixed_index_nones pints/tests/test_mcmc_controller.py::TestMCMCControllerSingleChainStorage::test_multiple_samplers_no_nones pints/tests/test_mcmc_controller.py::TestMCMCControllerSingleChainStorage::test_multiple_samplers_same_index_nones pints/tests/test_mcmc_controller.py::TestMCMCControllerSingleChainStorage::test_one_sampler_no_nones pints/tests/test_mcmc_controller.py::TestMCMCControllerSingleChainStorage::test_one_sampler_with_nones pints/tests/test_mcmc_controller.py::TestMCMCControllerMultiChainStorage::test_multi_chain_no_nones pints/tests/test_mcmc_controller.py::TestMCMCControllerMultiChainStorage::test_multi_chain_with_nones pints/tests/test_mcmc_controller.py::TestMCMCControllerMultiChainStorage::test_single_chain_no_nones pints/tests/test_mcmc_controller.py::TestMCMCControllerMultiChainStorage::test_single_chain_with_nones pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_deprecated_alias pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_logging pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_optimise pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_parallel pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_post_run_statistics pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_set_population_size pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_stopping_max_iterations pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_stopping_max_unchanged pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_stopping_no_criterion pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_stopping_threshold pints/tests/test_opt_optimisation_controller.py::TestOptimisationController::test_transform
pints/_optimisers/__init__.py pints/_mcmc/__init__.py pints/_nested/__init__.py
pints/_optimisers/__init__.py pints/_mcmc/__init__.py pints/_nested/__init__.py