Repository: rigetti/pyquil (language: python)

Issue:
Drop support for Python 3.8, update dependencies
Python 3.8 is approaching end-of-life, and many of the packages we use have already dropped support. We should also drop support for Python 3.8 and upgrade dependencies to newer versions (e.g scipy, ipython, python-rapidjson).
Compatibility Layer: Replace flake8 and black with ruff
We should use ruff for all of our linting and formatting needs.
Add check for security vulnerabilities to CI 
We should use a tool like [osv](https://pypi.org/project/osv/) to check if any dependencies pyQuil requires has a vulnerability.

`pyquil` is incompatible with `pyzx` because it depends on an old version of `lark`
Pre-Report Checklist
--------------------

- [x] I am running the latest versions of pyQuil and the Forest SDK
- [x] I checked to make sure that this bug has not already been reported

Issue Description
-----------------

It is not possible to install `pyquil` and `pyzx` together in the same environment. I tried `pyquil` versions 3.2.0 to 4.8.0.


How to Reproduce
----------------

### Code Snippet

```bash
pip install pyzx==0.8.0 pyquil==4.8.0
```

### Error Output

```
ERROR: Cannot install pyquil==4.8.0 and pyzx==0.8.0 because these package versions have conflicting dependencies.

The conflict is caused by:
    pyzx 0.8.0 depends on lark~=1.1.7
    pyquil 4.8.0 depends on lark<0.12.0 and >=0.11.1
```

Environment Context
-------------------

Operating System: Linux

Python Version (`python -V`): Python 3.10.12

Quilc Version (`quilc --version`): N/A

QVM Version (`qvm --version`): N/A

Python Environment Details (`pip freeze` or `conda list`):

```
[empty: new virtual environment]
```



Discussion / hints:

Fix patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ed7533734..0dac5bd5a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -107,18 +107,14 @@ poetry install
 
 ### Style Guidelines
 
-We use [Black](https://black.readthedocs.io/en/stable/index.html) and `flake8` to automatically
-lint the code and enforce style requirements as part of the CI pipeline. You can run these style
-tests yourself locally by running `make check-style` (to check for violations of the `flake8` rules)
-and `make check-format` (to see if `black` would reformat the code) in the top-level directory of
-the repository. If you aren't presented with any errors, then that means your code is good enough
-for the linter (`flake8`) and formatter (`black`). If `make check-format` fails, it will present
-you with a diff, which you can resolve by running `make format`. Black is very opinionated, but
-saves a lot of time by removing the need for style nitpicks in PR review. We only deviate from its
-default behavior in one category: we choose to use a line length of 100 rather than the Black
-default of 88 (this is configured in the [`pyproject.toml`](pyproject.toml) file). As for `flake8`,
-we ignore a couple of its rules (all for good reasons), and the specific configuration can be
-found in the [`.flake8`](.flake8) file.
+We use [ruff](https://docs.astral.sh/ruff/) to enforce lint and formatting requirements as part of CI. You can run these
+tests yourself locally by running `make check-style` (to check for violations of the linting rules)
+and `make check-format` (to see if `ruff` would reformat the code) in the top-level directory of
+the repository. If you aren't presented with any errors, then that means your code satisfies all
+the linting and formatting requirements. If `make check-format` fails, it will present you with a
+diff, which you can resolve by running `make format`. The ruff formatter is opinionated, but
+saves a lot of time by removing the need for style nitpicks in PR review. The configuration for
+`ruff` can be found in `pyproject.toml`.
 
 In addition to linting and formatting, we use type hints for all parameters and return values,
 following the [PEP 484 syntax][pep-484]. This is enforced as part of the CI via the command
diff --git a/Dockerfile b/Dockerfile
index a8ba41fde..56a61d4ad 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
 # use multi-stage builds to independently pull dependency versions
 ARG quilc_version=1.20.0
 ARG qvm_version=1.17.1
-ARG python_version=3.8
+ARG python_version=3.9
 
 # use multi-stage builds to independently pull dependency versions
 FROM rigetti/quilc:$quilc_version as quilc
diff --git a/Makefile b/Makefile
index 08a2f245c..126182399 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ check-all: check-format check-types check-style
 
 .PHONY: check-format
 check-format:
-	black --check --diff pyquil
+	ruff format --check --diff pyquil
 
 .PHONY: check-types
 check-types:
@@ -19,7 +19,7 @@ check-types:
 
 .PHONY: check-style
 check-style:
-	flake8 pyquil
+	ruff check pyquil
 
 .PHONY: clean
 clean:
@@ -55,7 +55,7 @@ docker: Dockerfile
 
 .PHONY: format
 format:
-	black $(if $(format_file), $(format_file), pyquil)
+	ruff format $(if $(format_file), $(format_file), pyquil)
 
 .PHONY: info
 info:
diff --git a/docs/Makefile b/docs/Makefile
index 5b0c5b844..e0f8d244f 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -2,7 +2,7 @@
 #
 
 # You can set these variables from the command line.
-SPHINXOPTS    = -W # treat warnings as errors
+SPHINXOPTS    = 
 SPHINXBUILD   = sphinx-build
 SOURCEDIR     = source
 BUILDDIR      = build
diff --git a/docs/source/conf.py b/docs/source/conf.py
index f097ebe90..a7f59b714 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -6,12 +6,13 @@
 # -- Project information -----------------------------------------------------
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
 
+import os
+import subprocess
+
 import sphinx_rtd_theme
 from sphinx.ext.doctest import doctest
-from pyquil import __version__
 
-import subprocess
-import os
+from pyquil import __version__
 
 project = "pyQuil"
 copyright = "2021, Rigetti Computing"
@@ -37,7 +38,7 @@
     "sphinx.ext.viewcode",
     "sphinxcontrib.jquery",
     "nbsphinx",
-    "recommonmark",
+    "myst_parser",
 ]
 
 templates_path = ["_templates"]
@@ -119,7 +120,6 @@
 
 dirname = os.path.dirname(__file__)
 
-
 def builder_inited_handler(app):
     import pandoc
 
@@ -129,16 +129,26 @@ def builder_inited_handler(app):
     input = pandoc.read(source=None, file=infile, format="markdown")
     pandoc.write(input, file=outfile, format="rst")
 
-    subprocess.call(
+    result = subprocess.run(
         [
             "sphinx-apidoc",
             "--module-first",
             "--force",
+            "--append-syspath",
             "--separate",
             f"--output-dir={dirname}/apidocs",
             f"{dirname}/../../pyquil",
-        ]
+        ],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        text=True
     )
+    if result.returncode == 0:
+        print("sphinx-apidoc ran successfully.")
+    else:
+        print(f"sphinx-apidoc failed with return code {result.returncode}.")
+        print("STDOUT:", result.stdout)
+        print("STDERR:", result.stderr)
 
 
 def setup(app):
diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst
index 857540540..ceb22506e 100644
--- a/docs/source/getting_started.rst
+++ b/docs/source/getting_started.rst
@@ -40,7 +40,7 @@ If you would like to stay up to date with the latest changes and bug fixes, you
 
 .. note::
 
-    pyQuil requires Python 3.8 or later.
+    pyQuil requires Python 3.9 or later.
 
 .. testcode:: verify-min-version
     :hide:
@@ -58,7 +58,7 @@ If you would like to stay up to date with the latest changes and bug fixes, you
 .. testoutput:: verify-min-version
     :hide:
 
-    ^3.8...
+    ^3.9...
 
 .. note::
 
diff --git a/mypy.ini b/mypy.ini
index 53e9e033e..9fce5efec 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -37,8 +37,3 @@ ignore_errors = True
 # Ignore errors in the conftest.py file
 [mypy-conftest]
 ignore_errors = True
-
-# Ignore errors in the pyquil/parser.py file
-# TODO(notmgsk): Add types
-[mypy-pyquil.parser]
-ignore_errors = True
diff --git a/poetry.lock b/poetry.lock
index 1d44e9368..80ff4fdad 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,13 +2,13 @@
 
 [[package]]
 name = "alabaster"
-version = "0.7.13"
-description = "A configurable sidebar-enabled Sphinx theme"
+version = "0.7.16"
+description = "A light, configurable Sphinx theme"
 optional = true
-python-versions = ">=3.6"
+python-versions = ">=3.9"
 files = [
-    {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"},
-    {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"},
+    {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"},
+    {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"},
 ]
 
 [[package]]
@@ -34,16 +34,23 @@ test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypo
 trio = ["trio (>=0.23)"]
 
 [[package]]
-name = "appnope"
-version = "0.1.4"
-description = "Disable App Nap on macOS >= 10.9"
+name = "asttokens"
+version = "2.4.1"
+description = "Annotate AST trees with source code positions"
 optional = true
-python-versions = ">=3.6"
+python-versions = "*"
 files = [
-    {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"},
-    {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"},
+    {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"},
+    {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"},
 ]
 
+[package.dependencies]
+six = ">=1.12.0"
+
+[package.extras]
+astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"]
+test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
+
 [[package]]
 name = "attrs"
 version = "23.2.0"
@@ -74,23 +81,9 @@ files = [
     {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"},
 ]
 
-[package.dependencies]
-pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
-
 [package.extras]
 dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
 
-[[package]]
-name = "backcall"
-version = "0.2.0"
-description = "Specifications for callback functions passed in to an API"
-optional = true
-python-versions = "*"
-files = [
-    {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
-    {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
-]
-
 [[package]]
 name = "beautifulsoup4"
 version = "4.12.3"
@@ -112,41 +105,6 @@ charset-normalizer = ["charset-normalizer"]
 html5lib = ["html5lib"]
 lxml = ["lxml"]
 
-[[package]]
-name = "black"
-version = "22.12.0"
-description = "The uncompromising code formatter."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
-    {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
-    {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
-    {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
-    {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
-    {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
-    {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
-    {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
-    {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
-    {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
-    {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
-    {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
-]
-
-[package.dependencies]
-click = ">=8.0.0"
-mypy-extensions = ">=0.4.3"
-pathspec = ">=0.9.0"
-platformdirs = ">=2"
-tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
-typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
-
-[package.extras]
-colorama = ["colorama (>=0.4.3)"]
-d = ["aiohttp (>=3.7.4)"]
-jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
-uvloop = ["uvloop (>=0.15.2)"]
-
 [[package]]
 name = "bleach"
 version = "6.1.0"
@@ -339,20 +297,6 @@ files = [
     {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
 ]
 
-[[package]]
-name = "click"
-version = "8.1.7"
-description = "Composable command line interface toolkit"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
-    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
-]
-
-[package.dependencies]
-colorama = {version = "*", markers = "platform_system == \"Windows\""}
-
 [[package]]
 name = "colorama"
 version = "0.4.6"
@@ -364,92 +308,6 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
-[[package]]
-name = "colored"
-version = "1.4.4"
-description = "Simple library for color and formatting to terminal"
-optional = false
-python-versions = "*"
-files = [
-    {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"},
-]
-
-[[package]]
-name = "commonmark"
-version = "0.9.1"
-description = "Python parser for the CommonMark Markdown spec"
-optional = true
-python-versions = "*"
-files = [
-    {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
-    {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
-]
-
-[package.extras]
-test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
-
-[[package]]
-name = "contourpy"
-version = "1.1.0"
-description = "Python library for calculating contours of 2D quadrilateral grids"
-optional = true
-python-versions = ">=3.8"
-files = [
-    {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"},
-    {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"},
-    {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"},
-    {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"},
-    {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"},
-    {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"},
-    {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"},
-    {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"},
-    {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"},
-    {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"},
-    {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"},
-    {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"},
-    {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"},
-    {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"},
-    {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"},
-    {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"},
-    {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"},
-    {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"},
-    {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"},
-    {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"},
-    {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"},
-    {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"},
-    {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"},
-    {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"},
-    {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"},
-    {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"},
-    {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"},
-    {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"},
-    {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"},
-    {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"},
-    {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"},
-    {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"},
-    {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"},
-    {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"},
-    {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"},
-    {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"},
-    {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"},
-    {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"},
-    {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"},
-    {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"},
-    {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"},
-    {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"},
-    {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"},
-]
-
-[package.dependencies]
-numpy = ">=1.16"
-
-[package.extras]
-bokeh = ["bokeh", "selenium"]
-docs = ["furo", "sphinx-copybutton"]
-mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"]
-test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
-test-no-images = ["pytest", "pytest-cov", "wurlitzer"]
-
 [[package]]
 name = "contourpy"
 version = "1.1.1"
@@ -512,7 +370,10 @@ files = [
 ]
 
 [package.dependencies]
-numpy = {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""}
+numpy = [
+    {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""},
+    {version = ">=1.26.0rc1,<2.0", markers = "python_version >= \"3.12\""},
+]
 
 [package.extras]
 bokeh = ["bokeh", "selenium"]
@@ -669,18 +530,32 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "execnet"
-version = "2.0.2"
+version = "2.1.1"
 description = "execnet: rapid multi-Python deployment"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"},
-    {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"},
+    {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"},
+    {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"},
 ]
 
 [package.extras]
 testing = ["hatch", "pre-commit", "pytest", "tox"]
 
+[[package]]
+name = "executing"
+version = "2.0.1"
+description = "Get the currently executing AST node of a frame, and other information"
+optional = true
+python-versions = ">=3.5"
+files = [
+    {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"},
+    {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"},
+]
+
+[package.extras]
+tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
+
 [[package]]
 name = "fastjsonschema"
 version = "2.19.1"
@@ -695,22 +570,6 @@ files = [
 [package.extras]
 devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
 
-[[package]]
-name = "flake8"
-version = "3.9.2"
-description = "the modular source code checker: pep8 pyflakes and co"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
-files = [
-    {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
-    {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
-]
-
-[package.dependencies]
-mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.7.0,<2.8.0"
-pyflakes = ">=2.3.0,<2.4.0"
-
 [[package]]
 name = "fonttools"
 version = "4.48.1"
@@ -834,13 +693,13 @@ socks = ["socksio (==1.*)"]
 
 [[package]]
 name = "idna"
-version = "3.6"
+version = "3.7"
 description = "Internationalized Domain Names in Applications (IDNA)"
 optional = false
 python-versions = ">=3.5"
 files = [
-    {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
-    {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
+    {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+    {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
 ]
 
 [[package]]
@@ -904,39 +763,40 @@ files = [
 
 [[package]]
 name = "ipython"
-version = "7.34.0"
+version = "8.18.1"
 description = "IPython: Productive Interactive Computing"
 optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.9"
 files = [
-    {file = "ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e"},
-    {file = "ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6"},
+    {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"},
+    {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"},
 ]
 
 [package.dependencies]
-appnope = {version = "*", markers = "sys_platform == \"darwin\""}
-backcall = "*"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 decorator = "*"
+exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
 jedi = ">=0.16"
 matplotlib-inline = "*"
 pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
-pickleshare = "*"
-prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
-pygments = "*"
-setuptools = ">=18.5"
-traitlets = ">=4.2"
+prompt-toolkit = ">=3.0.41,<3.1.0"
+pygments = ">=2.4.0"
+stack-data = "*"
+traitlets = ">=5"
+typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
 
 [package.extras]
-all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"]
-doc = ["Sphinx (>=1.3)"]
+all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
+black = ["black"]
+doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
 kernel = ["ipykernel"]
 nbconvert = ["nbconvert"]
 nbformat = ["nbformat"]
 notebook = ["ipywidgets", "notebook"]
 parallel = ["ipyparallel"]
 qtconsole = ["qtconsole"]
-test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"]
+test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"]
+test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"]
 
 [[package]]
 name = "jedi"
@@ -959,13 +819,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
 
 [[package]]
 name = "jinja2"
-version = "3.1.3"
+version = "3.1.4"
 description = "A very fast and expressive template engine."
 optional = true
 python-versions = ">=3.7"
 files = [
-    {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
-    {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
+    {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
+    {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
 ]
 
 [package.dependencies]
@@ -987,9 +847,7 @@ files = [
 
 [package.dependencies]
 attrs = ">=22.2.0"
-importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""}
 jsonschema-specifications = ">=2023.03.6"
-pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""}
 referencing = ">=0.28.4"
 rpds-py = ">=0.7.1"
 
@@ -1009,7 +867,6 @@ files = [
 ]
 
 [package.dependencies]
-importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""}
 referencing = ">=0.31.0"
 
 [[package]]
@@ -1180,19 +1037,28 @@ files = [
 ]
 
 [[package]]
-name = "lark"
-version = "0.11.3"
-description = "a modern parsing library"
-optional = false
-python-versions = "*"
+name = "markdown-it-py"
+version = "3.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = true
+python-versions = ">=3.8"
 files = [
-    {file = "lark-0.11.3.tar.gz", hash = "sha256:3100d9749b5a85735ec428b83100876a5da664804579e729c23a36341f961e7e"},
+    {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+    {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
 ]
 
+[package.dependencies]
+mdurl = ">=0.1,<1.0"
+
 [package.extras]
-atomic-cache = ["atomicwrites"]
-nearley = ["js2py"]
-regex = ["regex"]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 
 [[package]]
 name = "markupsafe"
@@ -1265,58 +1131,40 @@ files = [
 
 [[package]]
 name = "matplotlib"
-version = "3.7.4"
+version = "3.9.0"
 description = "Python plotting package"
 optional = true
-python-versions = ">=3.8"
-files = [
-    {file = "matplotlib-3.7.4-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:b71079239bd866bf56df023e5146de159cb0c7294e508830901f4d79e2d89385"},
-    {file = "matplotlib-3.7.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bf91a42f6274a64cb41189120b620c02e574535ff6671fa836cade7701b06fbd"},
-    {file = "matplotlib-3.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f757e8b42841d6add0cb69b42497667f0d25a404dcd50bd923ec9904e38414c4"},
-    {file = "matplotlib-3.7.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dfee00aa4bd291e08bb9461831c26ce0da85ca9781bb8794f2025c6e925281"},
-    {file = "matplotlib-3.7.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3640f33632beb3993b698b1be9d1c262b742761d6101f3c27b87b2185d25c875"},
-    {file = "matplotlib-3.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff539c4a17ecdf076ed808ee271ffae4a30dcb7e157b99ccae2c837262c07db6"},
-    {file = "matplotlib-3.7.4-cp310-cp310-win32.whl", hash = "sha256:24b8f28af3e766195c09b780b15aa9f6710192b415ae7866b9c03dee7ec86370"},
-    {file = "matplotlib-3.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fa193286712c3b6c3cfa5fe8a6bb563f8c52cc750006c782296e0807ce5e799"},
-    {file = "matplotlib-3.7.4-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:b167f54cb4654b210c9624ec7b54e2b3b8de68c93a14668937e7e53df60770ec"},
-    {file = "matplotlib-3.7.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7dfe6821f1944cb35603ff22e21510941bbcce7ccf96095beffaac890d39ce77"},
-    {file = "matplotlib-3.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3c557d9165320dff3c5f2bb99bfa0b6813d3e626423ff71c40d6bc23b83c3339"},
-    {file = "matplotlib-3.7.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08372696b3bb45c563472a552a705bfa0942f0a8ffe084db8a4e8f9153fbdf9d"},
-    {file = "matplotlib-3.7.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81e1a7ac818000e8ac3ca696c3fdc501bc2d3adc89005e7b4e22ee5e9d51de98"},
-    {file = "matplotlib-3.7.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390920a3949906bc4b0216198d378f2a640c36c622e3584dd0c79a7c59ae9f50"},
-    {file = "matplotlib-3.7.4-cp311-cp311-win32.whl", hash = "sha256:62e094d8da26294634da9e7f1856beee3978752b1b530c8e1763d2faed60cc10"},
-    {file = "matplotlib-3.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:f8fc2df756105784e650605e024d36dc2d048d68e5c1b26df97ee25d1bd41f9f"},
-    {file = "matplotlib-3.7.4-cp312-cp312-macosx_10_12_universal2.whl", hash = "sha256:568574756127791903604e315c11aef9f255151e4cfe20ec603a70f9dda8e259"},
-    {file = "matplotlib-3.7.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7d479aac338195e2199a8cfc03c4f2f55914e6a120177edae79e0340a6406457"},
-    {file = "matplotlib-3.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32183d4be84189a4c52b4b8861434d427d9118db2cec32986f98ed6c02dcfbb6"},
-    {file = "matplotlib-3.7.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0037d066cca1f4bda626c507cddeb6f7da8283bc6a214da2db13ff2162933c52"},
-    {file = "matplotlib-3.7.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44856632ebce88abd8efdc0a0dceec600418dcac06b72ae77af0019d260aa243"},
-    {file = "matplotlib-3.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:632fc938c22117d4241411191cfb88ac264a4c0a9ac702244641ddf30f0d739c"},
-    {file = "matplotlib-3.7.4-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:ce163be048613b9d1962273708cc97e09ca05d37312e670d166cf332b80bbaff"},
-    {file = "matplotlib-3.7.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e680f49bb8052ba3b2698e370155d2b4afb49f9af1cc611a26579d5981e2852a"},
-    {file = "matplotlib-3.7.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0604880e4327114054199108b7390f987f4f40ee5ce728985836889e11a780ba"},
-    {file = "matplotlib-3.7.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1e6abcde6fc52475f9d6a12b9f1792aee171ce7818ef6df5d61cb0b82816e6e8"},
-    {file = "matplotlib-3.7.4-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f59a70e2ec3212033ef6633ed07682da03f5249379722512a3a2a26a7d9a738e"},
-    {file = "matplotlib-3.7.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a9981b2a2dd9da06eca4ab5855d09b54b8ce7377c3e0e3957767b83219d652d"},
-    {file = "matplotlib-3.7.4-cp38-cp38-win32.whl", hash = "sha256:83859ac26839660ecd164ee8311272074250b915ac300f9b2eccc84410f8953b"},
-    {file = "matplotlib-3.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:7a7709796ac59fe8debde68272388be6ed449c8971362eb5b60d280eac8dadde"},
-    {file = "matplotlib-3.7.4-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:b1d70bc1ea1bf110bec64f4578de3e14947909a8887df4c1fd44492eca487955"},
-    {file = "matplotlib-3.7.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c83f49e795a5de6c168876eea723f5b88355202f9603c55977f5356213aa8280"},
-    {file = "matplotlib-3.7.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c9133f230945fe10652eb33e43642e933896194ef6a4f8d5e79bb722bdb2000"},
-    {file = "matplotlib-3.7.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798ff59022eeb276380ce9a73ba35d13c3d1499ab9b73d194fd07f1b0a41c304"},
-    {file = "matplotlib-3.7.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1707b20b25e90538c2ce8d4409e30f0ef1df4017cc65ad0439633492a973635b"},
-    {file = "matplotlib-3.7.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e6227ca8492baeef873cdd8e169a318efb5c3a25ce94e69727e7f964995b0b1"},
-    {file = "matplotlib-3.7.4-cp39-cp39-win32.whl", hash = "sha256:5661c8639aded7d1bbf781373a359011cb1dd09199dee49043e9e68dd16f07ba"},
-    {file = "matplotlib-3.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:55eec941a4743f0bd3e5b8ee180e36b7ea8e62f867bf2613937c9f01b9ac06a2"},
-    {file = "matplotlib-3.7.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab16868714e5cc90ec8f7ff5d83d23bcd6559224d8e9cb5227c9f58748889fe8"},
-    {file = "matplotlib-3.7.4-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c698b33f9a3f0b127a8e614c8fb4087563bb3caa9c9d95298722fa2400cdd3f"},
-    {file = "matplotlib-3.7.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be3493bbcb4d255cb71de1f9050ac71682fce21a56089eadbcc8e21784cb12ee"},
-    {file = "matplotlib-3.7.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f8c725d1dd2901b2e7ec6cd64165e00da2978cc23d4143cb9ef745bec88e6b04"},
-    {file = "matplotlib-3.7.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:286332f8f45f8ffde2d2119b9fdd42153dccd5025fa9f451b4a3b5c086e26da5"},
-    {file = "matplotlib-3.7.4-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:116ef0b43aa00ff69260b4cce39c571e4b8c6f893795b708303fa27d9b9d7548"},
-    {file = "matplotlib-3.7.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c90590d4b46458677d80bc3218f3f1ac11fc122baa9134e0cb5b3e8fc3714052"},
-    {file = "matplotlib-3.7.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de7c07069687be64fd9d119da3122ba13a8d399eccd3f844815f0dc78a870b2c"},
-    {file = "matplotlib-3.7.4.tar.gz", hash = "sha256:7cd4fef8187d1dd0d9dcfdbaa06ac326d396fb8c71c647129f0bf56835d77026"},
+python-versions = ">=3.9"
+files = [
+    {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"},
+    {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"},
+    {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"},
+    {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"},
+    {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"},
+    {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"},
+    {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"},
+    {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"},
+    {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"},
+    {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"},
+    {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"},
+    {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"},
+    {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"},
+    {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"},
+    {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"},
+    {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"},
+    {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"},
+    {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"},
+    {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"},
+    {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"},
+    {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"},
+    {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"},
+    {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"},
+    {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"},
+    {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"},
+    {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"},
+    {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"},
+    {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"},
+    {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"},
 ]
 
 [package.dependencies]
@@ -1324,36 +1172,58 @@ contourpy = ">=1.0.1"
 cycler = ">=0.10"
 fonttools = ">=4.22.0"
 importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""}
-kiwisolver = ">=1.0.1"
-numpy = ">=1.20,<2"
+kiwisolver = ">=1.3.1"
+numpy = ">=1.23"
 packaging = ">=20.0"
-pillow = ">=6.2.0"
+pillow = ">=8"
 pyparsing = ">=2.3.1"
 python-dateutil = ">=2.7"
 
+[package.extras]
+dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"]
+
 [[package]]
 name = "matplotlib-inline"
-version = "0.1.6"
+version = "0.1.7"
 description = "Inline Matplotlib backend for Jupyter"
 optional = true
-python-versions = ">=3.5"
+python-versions = ">=3.8"
 files = [
-    {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
-    {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
+    {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
+    {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
 ]
 
 [package.dependencies]
 traitlets = "*"
 
 [[package]]
-name = "mccabe"
-version = "0.6.1"
-description = "McCabe checker, plugin for flake8"
-optional = false
-python-versions = "*"
+name = "mdit-py-plugins"
+version = "0.4.1"
+description = "Collection of plugins for markdown-it-py"
+optional = true
+python-versions = ">=3.8"
+files = [
+    {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"},
+    {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=1.0.0,<4.0.0"
+
+[package.extras]
+code-style = ["pre-commit"]
+rtd = ["myst-parser", "sphinx-book-theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = true
+python-versions = ">=3.7"
 files = [
-    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
-    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+    {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+    {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
 ]
 
 [[package]]
@@ -1434,38 +1304,38 @@ files = [
 
 [[package]]
 name = "mypy"
-version = "1.8.0"
+version = "1.10.0"
 description = "Optional static typing for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"},
-    {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"},
-    {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"},
-    {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"},
-    {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"},
-    {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"},
-    {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"},
-    {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"},
-    {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"},
-    {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"},
-    {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"},
-    {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"},
-    {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"},
-    {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"},
-    {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"},
-    {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"},
-    {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"},
-    {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"},
-    {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"},
-    {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"},
-    {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"},
-    {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"},
-    {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"},
-    {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"},
-    {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"},
-    {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"},
-    {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"},
+    {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"},
+    {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"},
+    {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"},
+    {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"},
+    {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"},
+    {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
+    {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
+    {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
+    {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
+    {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
+    {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
+    {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
+    {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
+    {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
+    {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
+    {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"},
+    {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"},
+    {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"},
+    {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"},
+    {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"},
+    {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"},
+    {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"},
+    {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"},
+    {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"},
+    {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"},
+    {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
+    {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
 ]
 
 [package.dependencies]
@@ -1490,6 +1360,32 @@ files = [
     {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
 ]
 
+[[package]]
+name = "myst-parser"
+version = "3.0.1"
+description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser,"
+optional = true
+python-versions = ">=3.8"
+files = [
+    {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"},
+    {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"},
+]
+
+[package.dependencies]
+docutils = ">=0.18,<0.22"
+jinja2 = "*"
+markdown-it-py = ">=3.0,<4.0"
+mdit-py-plugins = ">=0.4,<1.0"
+pyyaml = "*"
+sphinx = ">=6,<8"
+
+[package.extras]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+linkify = ["linkify-it-py (>=2.0,<3.0)"]
+rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"]
+testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"]
+testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"]
+
 [[package]]
 name = "nbclient"
 version = "0.9.0"
@@ -1590,17 +1486,6 @@ nbformat = "*"
 sphinx = ">=1.8"
 traitlets = ">=5"
 
-[[package]]
-name = "nest-asyncio"
-version = "1.6.0"
-description = "Patch asyncio to allow nested event loops"
-optional = false
-python-versions = ">=3.5"
-files = [
-    {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"},
-    {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
-]
-
 [[package]]
 name = "networkx"
 version = "3.1"
@@ -1621,39 +1506,47 @@ test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"]
 
 [[package]]
 name = "numpy"
-version = "1.24.4"
+version = "1.26.4"
 description = "Fundamental package for array computing in Python"
 optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"},
-    {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"},
-    {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"},
-    {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"},
-    {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"},
-    {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"},
-    {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"},
-    {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"},
-    {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"},
-    {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"},
-    {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"},
-    {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"},
-    {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"},
-    {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"},
-    {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"},
-    {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"},
-    {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"},
-    {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"},
-    {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"},
-    {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"},
-    {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"},
-    {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"},
-    {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"},
-    {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"},
-    {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"},
-    {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"},
-    {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"},
-    {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"},
+python-versions = ">=3.9"
+files = [
+    {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
+    {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
+    {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"},
+    {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"},
+    {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"},
+    {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"},
+    {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"},
+    {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"},
+    {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"},
+    {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"},
+    {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"},
+    {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"},
+    {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"},
+    {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"},
+    {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"},
+    {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"},
+    {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"},
+    {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"},
+    {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"},
+    {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"},
+    {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"},
+    {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"},
+    {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"},
+    {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"},
+    {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"},
+    {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"},
+    {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"},
+    {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"},
+    {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"},
+    {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"},
+    {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"},
+    {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"},
+    {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"},
+    {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"},
+    {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"},
+    {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"},
 ]
 
 [[package]]
@@ -1775,17 +1668,6 @@ files = [
 qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
 testing = ["docopt", "pytest (<6.0.0)"]
 
-[[package]]
-name = "pathspec"
-version = "0.12.1"
-description = "Utility library for gitignore style pattern matching of file paths."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
-    {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
-]
-
 [[package]]
 name = "pexpect"
 version = "4.9.0"
@@ -1800,92 +1682,82 @@ files = [
 [package.dependencies]
 ptyprocess = ">=0.5"
 
-[[package]]
-name = "pickleshare"
-version = "0.7.5"
-description = "Tiny 'shelve'-like database with concurrency support"
-optional = true
-python-versions = "*"
-files = [
-    {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
-    {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
-]
-
 [[package]]
 name = "pillow"
-version = "10.2.0"
+version = "10.3.0"
 description = "Python Imaging Library (Fork)"
 optional = true
 python-versions = ">=3.8"
 files = [
-    {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
-    {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
-    {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
-    {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
-    {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
-    {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
-    {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
-    {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
-    {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
-    {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
-    {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
-    {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
-    {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
-    {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
-    {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
-    {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
-    {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
-    {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
-    {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
-    {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
-    {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
-    {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
-    {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
-    {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
-    {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
-    {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
-    {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
-    {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
-    {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
-    {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
-    {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
-    {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
-    {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
-    {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
-    {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
-    {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
+    {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
+    {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
+    {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
+    {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
+    {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
+    {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
+    {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
+    {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
+    {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
+    {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
+    {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
+    {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
+    {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
+    {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
+    {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
+    {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
+    {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
+    {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
+    {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
+    {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
+    {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
+    {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
+    {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
+    {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
+    {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
+    {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
+    {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
+    {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
+    {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
+    {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
+    {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
+    {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
+    {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
+    {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
+    {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
 ]
 
 [package.extras]
@@ -1896,22 +1768,11 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
 typing = ["typing-extensions"]
 xmp = ["defusedxml"]
 
-[[package]]
-name = "pkgutil-resolve-name"
-version = "1.3.10"
-description = "Resolve a name to an object."
-optional = true
-python-versions = ">=3.6"
-files = [
-    {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"},
-    {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"},
-]
-
 [[package]]
 name = "platformdirs"
 version = "4.2.0"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-optional = false
+optional = true
 python-versions = ">=3.8"
 files = [
     {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
@@ -1924,13 +1785,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-
 
 [[package]]
 name = "pluggy"
-version = "1.4.0"
+version = "1.5.0"
 description = "plugin and hook calling mechanisms for python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
-    {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
 ]
 
 [package.extras]
@@ -1993,16 +1854,19 @@ files = [
 ]
 
 [[package]]
-name = "pycodestyle"
-version = "2.7.0"
-description = "Python style guide checker"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+name = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
+optional = true
+python-versions = "*"
 files = [
-    {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
-    {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
+    {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
+    {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
 ]
 
+[package.extras]
+tests = ["pytest"]
+
 [[package]]
 name = "pycparser"
 version = "2.21"
@@ -2014,17 +1878,6 @@ files = [
     {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
 ]
 
-[[package]]
-name = "pyflakes"
-version = "2.3.1"
-description = "passive checker of Python programs"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
-    {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
-    {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
-]
-
 [[package]]
 name = "pygments"
 version = "2.17.2"
@@ -2056,13 +1909,13 @@ diagrams = ["jinja2", "railroad-diagrams"]
 
 [[package]]
 name = "pytest"
-version = "7.4.4"
+version = "8.2.1"
 description = "pytest: simple powerful testing with Python"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
-    {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+    {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
+    {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
 ]
 
 [package.dependencies]
@@ -2070,21 +1923,21 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
 exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=0.12,<2.0"
-tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+pluggy = ">=1.5,<2.0"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
 
 [package.extras]
-testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
 [[package]]
 name = "pytest-cov"
-version = "4.1.0"
+version = "5.0.0"
 description = "Pytest plugin for measuring coverage."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
-    {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+    {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
+    {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
 ]
 
 [package.dependencies]
@@ -2092,68 +1945,68 @@ coverage = {version = ">=5.2.1", extras = ["toml"]}
 pytest = ">=4.6"
 
 [package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
 
 [[package]]
 name = "pytest-mock"
-version = "3.12.0"
+version = "3.14.0"
 description = "Thin-wrapper around the mock package for easier use with pytest"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"},
-    {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"},
+    {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
+    {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
 ]
 
 [package.dependencies]
-pytest = ">=5.0"
+pytest = ">=6.2.5"
 
 [package.extras]
 dev = ["pre-commit", "pytest-asyncio", "tox"]
 
 [[package]]
 name = "pytest-rerunfailures"
-version = "12.0"
+version = "14.0"
 description = "pytest plugin to re-run tests to eliminate flaky failures"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytest-rerunfailures-12.0.tar.gz", hash = "sha256:784f462fa87fe9bdf781d0027d856b47a4bfe6c12af108f6bd887057a917b48e"},
-    {file = "pytest_rerunfailures-12.0-py3-none-any.whl", hash = "sha256:9a1afd04e21b8177faf08a9bbbf44de7a0fe3fc29f8ddbe83b9684bd5f8f92a9"},
+    {file = "pytest-rerunfailures-14.0.tar.gz", hash = "sha256:4a400bcbcd3c7a4ad151ab8afac123d90eca3abe27f98725dc4d9702887d2e92"},
+    {file = "pytest_rerunfailures-14.0-py3-none-any.whl", hash = "sha256:4197bdd2eaeffdbf50b5ea6e7236f47ff0e44d1def8dae08e409f536d84e7b32"},
 ]
 
 [package.dependencies]
 packaging = ">=17.1"
-pytest = ">=6.2"
+pytest = ">=7.2"
 
 [[package]]
 name = "pytest-timeout"
-version = "1.4.2"
-description = "py.test plugin to abort hanging tests"
+version = "2.3.1"
+description = "pytest plugin to abort hanging tests"
 optional = false
-python-versions = "*"
+python-versions = ">=3.7"
 files = [
-    {file = "pytest-timeout-1.4.2.tar.gz", hash = "sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76"},
-    {file = "pytest_timeout-1.4.2-py2.py3-none-any.whl", hash = "sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063"},
+    {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"},
+    {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"},
 ]
 
 [package.dependencies]
-pytest = ">=3.6.0"
+pytest = ">=7.0.0"
 
 [[package]]
 name = "pytest-xdist"
-version = "3.5.0"
+version = "3.6.1"
 description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"},
-    {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"},
+    {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"},
+    {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"},
 ]
 
 [package.dependencies]
-execnet = ">=1.1"
-pytest = ">=6.2.0"
+execnet = ">=2.1"
+pytest = ">=7.0.0"
 
 [package.extras]
 psutil = ["psutil (>=3.0)"]
@@ -2278,6 +2131,66 @@ files = [
     {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"},
 ]
 
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = true
+python-versions = ">=3.6"
+files = [
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
 [[package]]
 name = "pyzmq"
 version = "25.1.2"
@@ -2451,22 +2364,6 @@ files = [
     {file = "quil-0.10.0.tar.gz", hash = "sha256:381d6551366dd0403ba5bc2c18d851dc53688da938aaec768e93a8f87e4b4cde"},
 ]
 
-[[package]]
-name = "recommonmark"
-version = "0.7.1"
-description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects."
-optional = true
-python-versions = "*"
-files = [
-    {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"},
-    {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"},
-]
-
-[package.dependencies]
-commonmark = ">=0.8.1"
-docutils = ">=0.11"
-sphinx = ">=1.3.1"
-
 [[package]]
 name = "referencing"
 version = "0.33.0"
@@ -2484,13 +2381,13 @@ rpds-py = ">=0.7.0"
 
 [[package]]
 name = "requests"
-version = "2.31.0"
+version = "2.32.3"
 description = "Python HTTP for Humans."
 optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
-    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+    {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+    {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
 ]
 
 [package.dependencies]
@@ -2505,13 +2402,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "respx"
-version = "0.20.2"
+version = "0.21.1"
 description = "A utility for mocking out the Python HTTPX and HTTP Core libraries."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "respx-0.20.2-py2.py3-none-any.whl", hash = "sha256:ab8e1cf6da28a5b2dd883ea617f8130f77f676736e6e9e4a25817ad116a172c9"},
-    {file = "respx-0.20.2.tar.gz", hash = "sha256:07cf4108b1c88b82010f67d3c831dae33a375c7b436e54d87737c7f9f99be643"},
+    {file = "respx-0.21.1-py2.py3-none-any.whl", hash = "sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20"},
+    {file = "respx-0.21.1.tar.gz", hash = "sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af"},
 ]
 
 [package.dependencies]
@@ -2718,64 +2615,94 @@ files = [
     {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"},
 ]
 
+[[package]]
+name = "ruff"
+version = "0.4.6"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ruff-0.4.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef995583a038cd4a7edf1422c9e19118e2511b8ba0b015861b4abd26ec5367c5"},
+    {file = "ruff-0.4.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:602ebd7ad909eab6e7da65d3c091547781bb06f5f826974a53dbe563d357e53c"},
+    {file = "ruff-0.4.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f9ced5cbb7510fd7525448eeb204e0a22cabb6e99a3cb160272262817d49786"},
+    {file = "ruff-0.4.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04a80acfc862e0e1630c8b738e70dcca03f350bad9e106968a8108379e12b31f"},
+    {file = "ruff-0.4.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be47700ecb004dfa3fd4dcdddf7322d4e632de3c06cd05329d69c45c0280e618"},
+    {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1ff930d6e05f444090a0139e4e13e1e2e1f02bd51bb4547734823c760c621e79"},
+    {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f13410aabd3b5776f9c5699f42b37a3a348d65498c4310589bc6e5c548dc8a2f"},
+    {file = "ruff-0.4.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cf5cc02d3ae52dfb0c8a946eb7a1d6ffe4d91846ffc8ce388baa8f627e3bd50"},
+    {file = "ruff-0.4.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea3424793c29906407e3cf417f28fc33f689dacbbadfb52b7e9a809dd535dcef"},
+    {file = "ruff-0.4.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fa8561489fadf483ffbb091ea94b9c39a00ed63efacd426aae2f197a45e67fc"},
+    {file = "ruff-0.4.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d5b914818d8047270308fe3e85d9d7f4a31ec86c6475c9f418fbd1624d198e0"},
+    {file = "ruff-0.4.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4f02284335c766678778475e7698b7ab83abaf2f9ff0554a07b6f28df3b5c259"},
+    {file = "ruff-0.4.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3a6a0a4f4b5f54fff7c860010ab3dd81425445e37d35701a965c0248819dde7a"},
+    {file = "ruff-0.4.6-py3-none-win32.whl", hash = "sha256:9018bf59b3aa8ad4fba2b1dc0299a6e4e60a4c3bc62bbeaea222679865453062"},
+    {file = "ruff-0.4.6-py3-none-win_amd64.whl", hash = "sha256:a769ae07ac74ff1a019d6bd529426427c3e30d75bdf1e08bb3d46ac8f417326a"},
+    {file = "ruff-0.4.6-py3-none-win_arm64.whl", hash = "sha256:735a16407a1a8f58e4c5b913ad6102722e80b562dd17acb88887685ff6f20cf6"},
+    {file = "ruff-0.4.6.tar.gz", hash = "sha256:a797a87da50603f71e6d0765282098245aca6e3b94b7c17473115167d8dfb0b7"},
+]
+
 [[package]]
 name = "scipy"
-version = "1.9.3"
+version = "1.13.1"
 description = "Fundamental algorithms for scientific computing in Python"
 optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"},
-    {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"},
-    {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"},
-    {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"},
-    {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"},
-    {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"},
-    {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"},
-    {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"},
-    {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"},
-    {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"},
-    {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"},
-    {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"},
-    {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"},
-    {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"},
-    {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"},
-    {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"},
-    {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"},
-    {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"},
-    {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"},
-    {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"},
-    {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"},
+python-versions = ">=3.9"
+files = [
+    {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"},
+    {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"},
+    {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"},
+    {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"},
+    {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"},
+    {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"},
+    {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"},
+    {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"},
+    {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"},
+    {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"},
+    {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"},
+    {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"},
+    {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"},
+    {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"},
+    {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"},
+    {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"},
+    {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"},
+    {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"},
+    {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"},
+    {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"},
+    {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"},
+    {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"},
+    {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"},
+    {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"},
+    {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"},
 ]
 
 [package.dependencies]
-numpy = ">=1.18.5,<1.26.0"
+numpy = ">=1.22.4,<2.3"
 
 [package.extras]
-dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"]
-doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"]
-test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
+dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"]
+doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"]
+test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
 
 [[package]]
 name = "seaborn"
-version = "0.12.2"
+version = "0.13.2"
 description = "Statistical data visualization"
 optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "seaborn-0.12.2-py3-none-any.whl", hash = "sha256:ebf15355a4dba46037dfd65b7350f014ceb1f13c05e814eda2c9f5fd731afc08"},
-    {file = "seaborn-0.12.2.tar.gz", hash = "sha256:374645f36509d0dcab895cba5b47daf0586f77bfe3b36c97c607db7da5be0139"},
+    {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"},
+    {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"},
 ]
 
 [package.dependencies]
-matplotlib = ">=3.1,<3.6.1 || >3.6.1"
-numpy = ">=1.17,<1.24.0 || >1.24.0"
-pandas = ">=0.25"
+matplotlib = ">=3.4,<3.6.1 || >3.6.1"
+numpy = ">=1.20,<1.24.0 || >1.24.0"
+pandas = ">=1.2"
 
 [package.extras]
 dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"]
-docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx-copybutton", "sphinx-design", "sphinx-issues"]
-stats = ["scipy (>=1.3)", "statsmodels (>=0.10)"]
+docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"]
+stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
 
 [[package]]
 name = "setuptools"
@@ -2839,25 +2766,25 @@ files = [
 
 [[package]]
 name = "sphinx"
-version = "7.1.2"
+version = "7.3.7"
 description = "Python documentation generator"
 optional = true
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"},
-    {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"},
+    {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"},
+    {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"},
 ]
 
 [package.dependencies]
-alabaster = ">=0.7,<0.8"
+alabaster = ">=0.7.14,<0.8.0"
 babel = ">=2.9"
 colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
-docutils = ">=0.18.1,<0.21"
+docutils = ">=0.18.1,<0.22"
 imagesize = ">=1.3"
 importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
 Jinja2 = ">=3.0"
 packaging = ">=21.0"
-Pygments = ">=2.13"
+Pygments = ">=2.14"
 requests = ">=2.25.0"
 snowballstemmer = ">=2.0"
 sphinxcontrib-applehelp = "*"
@@ -2865,12 +2792,13 @@ sphinxcontrib-devhelp = "*"
 sphinxcontrib-htmlhelp = ">=2.0.0"
 sphinxcontrib-jsmath = "*"
 sphinxcontrib-qthelp = "*"
-sphinxcontrib-serializinghtml = ">=1.1.5"
+sphinxcontrib-serializinghtml = ">=1.1.9"
+tomli = {version = ">=2", markers = "python_version < \"3.11\""}
 
 [package.extras]
 docs = ["sphinxcontrib-websupport"]
-lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
-test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
+lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"]
+test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"]
 
 [[package]]
 name = "sphinx-rtd-theme"
@@ -2981,47 +2909,52 @@ test = ["pytest"]
 
 [[package]]
 name = "sphinxcontrib-serializinghtml"
-version = "1.1.5"
-description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
+version = "1.1.10"
+description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)"
 optional = true
-python-versions = ">=3.5"
+python-versions = ">=3.9"
 files = [
-    {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
-    {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
+    {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"},
+    {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"},
 ]
 
 [package.extras]
 lint = ["docutils-stubs", "flake8", "mypy"]
+standalone = ["Sphinx (>=5)"]
 test = ["pytest"]
 
 [[package]]
-name = "syrupy"
-version = "3.0.6"
-description = "Pytest Snapshot Test Utility"
-optional = false
-python-versions = ">=3.7,<4"
+name = "stack-data"
+version = "0.6.3"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+optional = true
+python-versions = "*"
 files = [
-    {file = "syrupy-3.0.6-py3-none-any.whl", hash = "sha256:9c18e22264026b34239bcc87ab7cc8d893eb17236ea7dae634217ea4f22a848d"},
-    {file = "syrupy-3.0.6.tar.gz", hash = "sha256:583aa5ca691305c27902c3e29a1ce9da50ff9ab5f184c54b1dc124a16e4a6cf4"},
+    {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
+    {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
 ]
 
 [package.dependencies]
-colored = ">=1.3.92,<2.0.0"
-pytest = ">=5.1.0,<8.0.0"
+asttokens = ">=2.1.0"
+executing = ">=1.2.0"
+pure-eval = "*"
+
+[package.extras]
+tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
 
 [[package]]
-name = "tenacity"
-version = "8.2.3"
-description = "Retry code until it succeeds"
+name = "syrupy"
+version = "4.6.1"
+description = "Pytest Snapshot Test Utility"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8.1,<4"
 files = [
-    {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"},
-    {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"},
+    {file = "syrupy-4.6.1-py3-none-any.whl", hash = "sha256:203e52f9cb9fa749cf683f29bd68f02c16c3bc7e7e5fe8f2fc59bdfe488ce133"},
+    {file = "syrupy-4.6.1.tar.gz", hash = "sha256:37a835c9ce7857eeef86d62145885e10b3cb9615bc6abeb4ce404b3f18e1bb36"},
 ]
 
-[package.extras]
-doc = ["reno", "sphinx", "tornado (>=4.5)"]
+[package.dependencies]
+pytest = ">=7.0.0,<9.0.0"
 
 [[package]]
 name = "tinycss2"
@@ -3109,37 +3042,15 @@ files = [
     {file = "types_Deprecated-1.2.9.20240106-py3-none-any.whl", hash = "sha256:9dcb258493b5be407574ee21e50ddac9e429072d39b576126bf1ac00764fb9a8"},
 ]
 
-[[package]]
-name = "types-python-dateutil"
-version = "2.8.19.20240106"
-description = "Typing stubs for python-dateutil"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "types-python-dateutil-2.8.19.20240106.tar.gz", hash = "sha256:1f8db221c3b98e6ca02ea83a58371b22c374f42ae5bbdf186db9c9a76581459f"},
-    {file = "types_python_dateutil-2.8.19.20240106-py3-none-any.whl", hash = "sha256:efbbdc54590d0f16152fa103c9879c7d4a00e82078f6e2cf01769042165acaa2"},
-]
-
-[[package]]
-name = "types-retry"
-version = "0.9.9.4"
-description = "Typing stubs for retry"
-optional = false
-python-versions = "*"
-files = [
-    {file = "types-retry-0.9.9.4.tar.gz", hash = "sha256:e4731dc684b56b875d9746459ad665d3bc281a56b530acdf1c97730167799941"},
-    {file = "types_retry-0.9.9.4-py3-none-any.whl", hash = "sha256:f29760a9fe8b1fefe253e5fe6be7e4c0eba243932c600e0eccffb42a21d17765"},
-]
-
 [[package]]
 name = "typing-extensions"
-version = "4.9.0"
+version = "4.12.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
-    {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
+    {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"},
+    {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"},
 ]
 
 [[package]]
@@ -3287,10 +3198,10 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link
 testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
 
 [extras]
-docs = ["Sphinx", "matplotlib", "nbsphinx", "pandoc", "recommonmark", "seaborn", "sphinx-rtd-theme"]
+docs = ["Sphinx", "matplotlib", "myst-parser", "nbsphinx", "pandoc", "seaborn", "sphinx-rtd-theme"]
 latex = ["ipython"]
 
 [metadata]
 lock-version = "2.0"
-python-versions = "^3.8,<=3.12"
-content-hash = "e5bd071d0ca5944e2c68535eea6bb93e3fd9ffadb92b485a2de780afcbeeeff2"
+python-versions = "^3.9,<=3.14"
+content-hash = "aa08be6cc81654a552ea596c6c9e355c3df38fe7165a6164299d70e540a095dd"
diff --git a/pyproject.toml b/pyproject.toml
index cf4871d77..c36a8ff11 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,85 +19,102 @@ packages = [{ include = "pyquil" }]
 exclude = ["pyquil/conftest.py"]
 
 [tool.poetry.dependencies]
-python = "^3.8,<=3.12"
-numpy = "^1.22"
-scipy = "^1.7.3"
-lark = "^0.11.1"
-rpcq = "^3.10.0"
+python = "^3.9,<=3.14"
+numpy = "^1.25"
+scipy = "^1.11"
+rpcq = "^3.11.0"
 networkx = ">=2.5"
-importlib-metadata = { version = ">=3.7.3,<5", python = "<3.8" }
 qcs-sdk-python = "0.17.9"
-tenacity = "^8.2.2"
-types-python-dateutil = "^2.8.19"
-types-retry = "^0.9.9"
 packaging = "^23.1"
+deprecated = "^1.2.14"
+types-deprecated = "^1.2.9.3"
 
 # latex extra
-ipython = { version = "^7.21.0", optional = true }
+ipython = { version = "^8.16.0", optional = true }
 
 # docs extra
-Sphinx = { version = "^7.1.2", optional = true }
+Sphinx = { version = "^7.3.7", optional = true }
 sphinx-rtd-theme = { version = "^1.3.0,<=2.0.0", optional = true }
-nbsphinx = { version = "^0.9.1", optional = true }
-recommonmark = { version = "^0.7.1", optional = true }
+nbsphinx = { version = "^0.9.3", optional = true }
+myst-parser = { version = "^3.0.1", optional = true }
 pandoc = {version = "2.4b0", optional = true}
-matplotlib = {version = "^3.7.1", optional = true}
-matplotlib-inline = {version = "^0.1.6", optional = true}
-seaborn = {version = "^0.12.2", optional = true}
-deprecated = "^1.2.13"
-types-deprecated = "^1.2.9.2"
-
-[tool.poetry.dev-dependencies]
-black = "^22.8.0"
-flake8 = "^3.8.1"
-pytest = "^7.4.0"
-pytest-cov = "^4.1.0"
-mypy = "^1.5.0"
-toml = "^0.10.2"
-pytest-xdist = "^3.3.1"
-pytest-rerunfailures = "^12.0.0"
-pytest-timeout = "^1.4.2"
-pytest-mock = "^3.11.1"
-respx = "^0.20"
-nest-asyncio = "^1.5.6"
-mock = { version = "^4.0", python = "<3.8" }
-syrupy = "^3.0.6"
+matplotlib = {version = "^3.9.0", optional = true}
+matplotlib-inline = {version = "^0.1.7", optional = true}
+seaborn = {version = "^0.13.2", optional = true}
 
 [tool.poetry.extras]
 latex = ["ipython"]
-docs = ["Sphinx", "sphinx-rtd-theme", "nbsphinx", "recommonmark", "pandoc", "matplotlib", "seaborn", "toml"]
+docs = ["Sphinx", "sphinx-rtd-theme", "nbsphinx", "myst-parser", "pandoc", "matplotlib", "matlotlib-inline", "seaborn", "toml"]
 
 [tool.poetry.group.dev.dependencies]
+typing-extensions = "^4.12.0"
 setuptools = {version = "^69.0.2", python = ">=3.12"}
-mypy = "^1.8.0"
+ruff = "^0.4.6"
+pytest = "^8.2.0"
+pytest-cov = "^5.0.0"
+mypy = "^1.10.0"
+toml = "^0.10.2"
+pytest-xdist = "^3.6.1"
+pytest-rerunfailures = "^14.0.0"
+pytest-timeout = "^2.3.1"
+pytest-mock = "^3.14.0"
+respx = "^0.21.1"
+syrupy = "^4.6.1"
 
-[tool.ruff]
-line-length = 120
 
-[tool.black]
+[tool.ruff]
+exclude = [
+    ".bzr",
+    ".direnv",
+    ".eggs",
+    ".git",
+    ".git-rewrite",
+    ".hg",
+    ".ipynb_checkpoints",
+    ".mypy_cache",
+    ".nox",
+    ".pants.d",
+    ".pyenv",
+    ".pytest_cache",
+    ".pytype",
+    ".ruff_cache",
+    ".svn",
+    ".tox",
+    ".venv",
+    ".vscode",
+    "__pypackages__",
+    "_build",
+    "buck-out",
+    "build",
+    "dist",
+    "node_modules",
+    "site-packages",
+    "venv",
+]
 line-length = 120
-target-version = ['py38']
-include = '\.pyi?$'
-exclude = '''
+indent-width = 4
+target-version = "py39"
 
-(
-  /(
-      \.eggs         # black-default
-    | \.git
-    | \.hg
-    | \.mypy_cache
-    | \.tox
-    | \.venv
-    | _build
-    | buck-out
-    | build
-    | dist
-  )/
-  | external         # pyquil-specific
-  | gen3
+[tool.ruff.lint]
+select = ["D", "E4", "E7", "E9", "F", "I", "B", "S", "UP", "W"]
+ignore = [
+  "D105",  # Allow missing documentation in dunder method.
+  "D203",  # This conflicts with D211.
+  "D213",  # This conflicts with D212.
+]
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
 
-)
-'''
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+skip-magic-trailing-comma = false
+line-ending = "auto"
+docstring-code-format = true
+docstring-code-line-length = "dynamic"
 
 [tool.pytest.ini_options]
 filterwarnings = ["ignore::DeprecationWarning:pyquil.*:", "ignore::DeprecationWarning:test.unit.*:"]
diff --git a/pyquil/__init__.py b/pyquil/__init__.py
index 18127d0e3..e2dbcdebd 100644
--- a/pyquil/__init__.py
+++ b/pyquil/__init__.py
@@ -1,5 +1,12 @@
+"""pyQuil is a Python library for quantum programming using Quil. pyQuil enables users to construct, manipulate, and execute quantum programs on a quantum virtual machine (QVM) and Rigetti quantum processing units (QPUs).
+
+Refer to the `pyQuil Documentation <https://pyquil-docs.rigetti.com/en/stable/>`_ for more information.
+"""
+
 from pyquil._version import pyquil_version
+from pyquil.api import get_qc, list_quantum_computers
 from pyquil.quil import Program
-from pyquil.api import list_quantum_computers, get_qc
 
 __version__ = pyquil_version
+
+__all__ = ["get_qc", "list_quantum_computers", "Program", "__version__"]
diff --git a/pyquil/_version.py b/pyquil/_version.py
index ed11c6460..2991d29f5 100644
--- a/pyquil/_version.py
+++ b/pyquil/_version.py
@@ -14,6 +14,7 @@
 #    limitations under the License.
 ##############################################################################
 from importlib.metadata import version
+
 from packaging.version import parse
 
 pyquil_version = version(__package__)
diff --git a/pyquil/api/__init__.py b/pyquil/api/__init__.py
index 7ee97f6a9..dc6675587 100644
--- a/pyquil/api/__init__.py
+++ b/pyquil/api/__init__.py
@@ -13,55 +13,58 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-"""
-Sub-package for facilitating connections to the QVM / QPU.
-"""
-
-__all__ = [
-    "AbstractCompiler",
-    "BenchmarkConnection",
-    "EncryptedProgram",
-    "ExecutionOptions",
-    "get_qc",
-    "list_quantum_computers",
-    "local_forest_runtime",
-    "MemoryMap",
-    "QAM",
-    "QAMExecutionResult",
-    "QCSClient",
-    "QCSQuantumProcessor",
-    "QPU",
-    "QPUCompiler",
-    "QPUCompilerAPIOptions",
-    "QuantumComputer",
-    "QuantumExecutable",
-    "QVM",
-    "QVMCompiler",
-    "WavefunctionSimulator",
-]
+"""Sub-package for facilitating connections to the QVM / QPU."""
 
 from qcs_sdk import QCSClient, RegisterMatrixConversionError
-from qcs_sdk.qpu.api import ExecutionOptions, ExecutionOptionsBuilder, ConnectionStrategy
 from qcs_sdk.qpu import RawQPUReadoutData
+from qcs_sdk.qpu.api import ConnectionStrategy, ExecutionOptions, ExecutionOptionsBuilder
 from qcs_sdk.qvm import RawQVMReadoutData
 
 from pyquil.api._benchmark import BenchmarkConnection
 from pyquil.api._compiler import (
-    QVMCompiler,
-    QPUCompiler,
-    QuantumExecutable,
-    EncryptedProgram,
     AbstractCompiler,
+    EncryptedProgram,
+    QPUCompiler,
     QPUCompilerAPIOptions,
+    QuantumExecutable,
+    QVMCompiler,
 )
-from pyquil.api._qam import QAM, QAMExecutionResult, MemoryMap
+from pyquil.api._qam import QAM, MemoryMap, QAMExecutionResult
 from pyquil.api._qpu import QPU
 from pyquil.api._quantum_computer import (
     QuantumComputer,
-    list_quantum_computers,
     get_qc,
+    list_quantum_computers,
     local_forest_runtime,
 )
 from pyquil.api._qvm import QVM
 from pyquil.api._wavefunction_simulator import WavefunctionSimulator
 from pyquil.quantum_processor import QCSQuantumProcessor
+
+__all__ = [
+    "QCSClient",
+    "RegisterMatrixConversionError",
+    "RawQPUReadoutData",
+    "RawQVMReadoutData",
+    "ConnectionStrategy",
+    "ExecutionOptions",
+    "BenchmarkConnection",
+    "AbstractCompiler",
+    "EncryptedProgram",
+    "QPUCompiler",
+    "QPUCompilerAPIOptions",
+    "QuantumExecutable",
+    "QVMCompiler",
+    "QAM",
+    "MemoryMap",
+    "QAMExecutionResult",
+    "QPU",
+    "QuantumComputer",
+    "get_qc",
+    "list_quantum_computers",
+    "local_forest_runtime",
+    "QVM",
+    "WavefunctionSimulator",
+    "QCSQuantumProcessor",
+    "ExecutionOptionsBuilder",
+]
diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py
index 5c3f17e00..a39969eeb 100644
--- a/pyquil/api/_abstract_compiler.py
+++ b/pyquil/api/_abstract_compiler.py
@@ -13,14 +13,16 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from abc import ABC, abstractmethod
-from dataclasses import dataclass
 import dataclasses
-from typing import Any, Dict, List, Optional, Sequence, Union
 import json
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from dataclasses import dataclass
+from typing import Any, Optional, Union
 
 from qcs_sdk import QCSClient
-from qcs_sdk.compiler.quilc import compile_program, TargetDevice, CompilerOpts, QuilcClient, CompilationResult
+from qcs_sdk.compiler.quilc import CompilationResult, CompilerOpts, QuilcClient, TargetDevice, compile_program
+from rpcq.messages import ParameterSpec
 
 from pyquil._version import pyquil_version
 from pyquil.api._compiler_client import CompilerClient
@@ -30,7 +32,6 @@
 from pyquil.quil import Program
 from pyquil.quilatom import MemoryReference
 from pyquil.quilbase import Gate
-from rpcq.messages import ParameterSpec
 
 
 class QuilcVersionMismatch(Exception):
@@ -43,26 +44,22 @@ class QuilcNotRunning(Exception):
 
 @dataclass
 class EncryptedProgram:
-    """
-    Encrypted binary, executable on a QPU.
-    """
+    """Encrypted binary, executable on a QPU."""
 
     program: str
     """String representation of an encrypted Quil program."""
 
-    memory_descriptors: Dict[str, ParameterSpec]
+    memory_descriptors: dict[str, ParameterSpec]
     """Descriptors for memory executable's regions, mapped by name."""
 
-    ro_sources: Dict[MemoryReference, str]
+    ro_sources: dict[MemoryReference, str]
     """Readout sources, mapped by memory reference."""
 
-    recalculation_table: List[str]
+    recalculation_table: list[str]
     """A mapping from memory references to the original gate arithmetic."""
 
     def copy(self) -> "EncryptedProgram":
-        """
-        Return a deep copy of this EncryptedProgram.
-        """
+        """Return a deep copy of this EncryptedProgram."""
         return dataclasses.replace(self)
 
 
@@ -91,18 +88,15 @@ def __init__(
             quilc_client=quilc_client,
         )
 
-    def get_version_info(self) -> Dict[str, Any]:
-        """
-        Return version information for this compiler and its dependencies.
+    def get_version_info(self) -> dict[str, Any]:
+        """Return version information for this compiler and its dependencies.
 
         :return: Dictionary of version information.
         """
         return {"quilc": self._compiler_client.get_version()}
 
     def quil_to_native_quil(self, program: Program, *, protoquil: Optional[bool] = None) -> Program:
-        """
-        Convert a Quil program into native Quil, which is supported for execution on a QPU.
-        """
+        """Convert a Quil program into native Quil, which is supported for execution on a QPU."""
         result = self._compile_with_quilc(
             program.out(calibrations=False),
             options=CompilerOpts(protoquil=protoquil, timeout=self._compiler_client.timeout),
@@ -132,38 +126,34 @@ def _compile_with_quilc(self, input: str, options: Optional[CompilerOpts] = None
     def _connect(self) -> None:
         try:
             _check_quilc_version(self._compiler_client.get_version())
-        except TimeoutError:
+        except TimeoutError as e:
             raise QuilcNotRunning(
                 f"Request to quilc at {self._compiler_client.base_url} timed out. "
                 "This could mean that quilc is not running, is not reachable, or is "
                 "responding slowly. See the Troubleshooting Guide: "
                 "{DOCS_URL}/troubleshooting.html"
-            )
+            ) from e
 
     def transpile_qasm_2(self, qasm: str) -> Program:
-        """Transpile a QASM 2.0 program string to Quil, returning the result as a :py:class:~`pyquil.quil.Program`"""
+        """Transpile a QASM 2.0 program string to Quil, returning the result as a :py:class:~`pyquil.quil.Program`."""
         result = self._compile_with_quilc(qasm, options=CompilerOpts(timeout=self._compiler_client.timeout))
         return Program(result.program)
 
     @abstractmethod
     def native_quil_to_executable(self, nq_program: Program, **kwargs: Any) -> QuantumExecutable:
-        """
-        Compile a native quil program to a binary executable.
+        """Compile a native quil program to a binary executable.
 
         :param nq_program: Native quil to compile
         :return: An (opaque) binary executable
         """
 
+    @abstractmethod
     def reset(self) -> None:
-        """
-        Reset the state of the this compiler.
-        """
-        pass
+        """Reset the state of the this compiler."""
 
 
 def _check_quilc_version(version: str) -> None:
-    """
-    Verify that there is no mismatch between pyquil and quilc versions.
+    """Verify that there is no mismatch between pyquil and quilc versions.
 
     :param version: quilc version.
     """
@@ -177,9 +167,7 @@ def _check_quilc_version(version: str) -> None:
 class AbstractBenchmarker(ABC):
     @abstractmethod
     def apply_clifford_to_pauli(self, clifford: Program, pauli_in: PauliTerm) -> PauliTerm:
-        r"""
-        Given a circuit that consists only of elements of the Clifford group,
-        return its action on a PauliTerm.
+        r"""Given a circuit that consists only of elements of the Clifford group, return its action on a PauliTerm.
 
         In particular, for Clifford C, and Pauli P, this returns the PauliTerm
         representing PCP^{\dagger}.
@@ -196,10 +184,10 @@ def generate_rb_sequence(
         gateset: Sequence[Gate],
         seed: Optional[int] = None,
         interleaver: Optional[Program] = None,
-    ) -> List[Program]:
-        """
-        Construct a randomized benchmarking experiment on the given qubits, decomposing into
-        gateset. If interleaver is not provided, the returned sequence will have the form
+    ) -> list[Program]:
+        r"""Construct a randomized benchmarking experiment on the given qubits, decomposing into gateset.
+
+        If interleaver is not provided, the returned sequence will have the form
 
             C_1 C_2 ... C_(depth-1) C_inv ,
 
diff --git a/pyquil/api/_benchmark.py b/pyquil/api/_benchmark.py
index 19e7dc85b..253bf7a51 100644
--- a/pyquil/api/_benchmark.py
+++ b/pyquil/api/_benchmark.py
@@ -13,45 +13,41 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from typing import List, Optional, Sequence, cast
+from collections.abc import Sequence
+from typing import Optional, cast
 
 from qcs_sdk import QCSClient
 from qcs_sdk.compiler.quilc import (
-    PauliTerm as QuilcPauliTerm,
-    RandomizedBenchmarkingRequest,
     ConjugateByCliffordRequest,
+    RandomizedBenchmarkingRequest,
+)
+from qcs_sdk.compiler.quilc import (
+    PauliTerm as QuilcPauliTerm,
 )
 
 from pyquil.api._abstract_compiler import AbstractBenchmarker
 from pyquil.api._compiler_client import CompilerClient
-
 from pyquil.paulis import PauliTerm, is_identity
-from pyquil.quil import address_qubits, Program
+from pyquil.quil import Program, address_qubits
 from pyquil.quilbase import Gate
 
 
 class BenchmarkConnection(AbstractBenchmarker):
-    """
-    Represents a connection to a server that generates benchmarking data.
-    """
+    """Represents a connection to a server that generates benchmarking data."""
 
     def __init__(self, *, timeout: float = 10.0, client_configuration: Optional[QCSClient] = None):
-        """
-        Client to communicate with the benchmarking data endpoint.
+        """Client to communicate with the benchmarking data endpoint.
 
         :param timeout: Time limit for requests, in seconds.
         :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded.
         """
-
         self._compiler_client = CompilerClient(
             client_configuration=client_configuration or QCSClient.load(),
             request_timeout=timeout,
         )
 
     def apply_clifford_to_pauli(self, clifford: Program, pauli_in: PauliTerm) -> PauliTerm:
-        r"""
-        Given a circuit that consists only of elements of the Clifford group,
-        return its action on a PauliTerm.
+        r"""Given a circuit that consists only of elements of the Clifford group, return its action on a PauliTerm.
 
         In particular, for Clifford C, and Pauli P, this returns the PauliTerm
         representing CPC^{\dagger}.
@@ -80,7 +76,7 @@ def apply_clifford_to_pauli(self, clifford: Program, pauli_in: PauliTerm) -> Pau
         pauli_out = PauliTerm("I", 0, 1.0j**phase_factor)
         clifford_qubits = clifford.get_qubits()
         pauli_qubits = pauli_in.get_qubits()
-        all_qubits = sorted(set(cast(List[int], pauli_qubits)).union(set(cast(List[int], clifford_qubits))))
+        all_qubits = sorted(set(cast(list[int], pauli_qubits)).union(set(cast(list[int], clifford_qubits))))
         # The returned pauli will have specified its value on all_qubits, sorted by index.
         #  This is maximal set of qubits that can be affected by this conjugation.
         for i, pauli in enumerate(paulis):
@@ -93,10 +89,10 @@ def generate_rb_sequence(
         gateset: Sequence[Gate],
         seed: Optional[int] = None,
         interleaver: Optional[Program] = None,
-    ) -> List[Program]:
-        """
-        Construct a randomized benchmarking experiment on the given qubits, decomposing into
-        gateset. If interleaver is not provided, the returned sequence will have the form
+    ) -> list[Program]:
+        """Construct a randomized benchmarking experiment on the given qubits, decomposing into gateset.
+
+        If interleaver is not provided, the returned sequence will have the form
 
             C_1 C_2 ... C_(depth-1) C_inv ,
 
@@ -123,7 +119,6 @@ def generate_rb_sequence(
          programs are called cliffords then `sum(cliffords, Program())` will give the randomized
          benchmarking experiment, which will compose to the identity program.
         """
-
         # Support QubitPlaceholders: we temporarily index to arbitrary integers.
         # `generate_rb_sequence` handles mapping back to the original gateset gates.
         gateset_as_program = address_qubits(sum(gateset, Program()))  # type: ignore
@@ -131,7 +126,8 @@ def generate_rb_sequence(
         gateset_for_api = gateset_as_program.out().splitlines()
         interleaver_out: Optional[str] = None
         if interleaver:
-            assert isinstance(interleaver, Program)
+            if not isinstance(interleaver, Program):
+                raise ValueError("interleaver must be a Program")
             interleaver_out = interleaver.out(calibrations=False)
 
         depth = int(depth)  # needs to be jsonable, no np.int64 please!
diff --git a/pyquil/api/_compiler.py b/pyquil/api/_compiler.py
index 8accdefdd..bc23802d8 100644
--- a/pyquil/api/_compiler.py
+++ b/pyquil/api/_compiler.py
@@ -13,20 +13,22 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from typing import Any, Dict, Optional
-from typing_extensions import TypeAlias
+from typing import Any, Optional
 from warnings import warn
 
 from qcs_sdk import QCSClient
+from qcs_sdk.compiler.quilc import QuilcClient
 from qcs_sdk.qpu.rewrite_arithmetic import rewrite_arithmetic
 from qcs_sdk.qpu.translation import (
+    TranslationBackend,
     get_quilt_calibrations,
     translate,
+)
+from qcs_sdk.qpu.translation import (
     TranslationOptions as _TranslationOptions,
-    TranslationBackend,
 )
-from qcs_sdk.compiler.quilc import QuilcClient
 from rpcq.messages import ParameterSpec
+from typing_extensions import TypeAlias
 
 from pyquil.api._abstract_compiler import AbstractCompiler, EncryptedProgram, QuantumExecutable
 from pyquil.quantum_processor import AbstractQuantumProcessor
@@ -55,16 +57,15 @@ def parse_mref(val: str) -> MemoryReference:
             return MemoryReference(name, int(offset[:-1]))
         else:
             return MemoryReference(val)
-    except Exception:
-        raise ValueError(f"Unable to parse memory reference {val}.")
+    except Exception as e:
+        raise ValueError(f"Unable to parse memory reference {val}.") from e
 
 
-def _collect_memory_descriptors(program: Program) -> Dict[str, ParameterSpec]:
+def _collect_memory_descriptors(program: Program) -> dict[str, ParameterSpec]:
     """Collect Declare instructions that are important for building the patch table.
 
     :return: A dictionary of variable names to specs about the declared region.
     """
-
     return {
         instr.name: ParameterSpec(type=instr.memory_type, length=instr.memory_size)
         for instr in program
@@ -73,9 +74,7 @@ def _collect_memory_descriptors(program: Program) -> Dict[str, ParameterSpec]:
 
 
 class QPUCompiler(AbstractCompiler):
-    """
-    Client to communicate with the compiler and translation service.
-    """
+    """Client to communicate with the compiler and translation service."""
 
     api_options: Optional[QPUCompilerAPIOptions]
 
@@ -89,8 +88,7 @@ def __init__(
         api_options: Optional[QPUCompilerAPIOptions] = None,
         quilc_client: Optional[QuilcClient] = None,
     ) -> None:
-        """
-        Instantiate a new QPU compiler client.
+        """Instantiate a new QPU compiler client.
 
         :param quantum_processor_id: Processor to target.
         :param quantum_processor: Quantum processor to use as compilation target.
@@ -112,8 +110,7 @@ def __init__(
     def native_quil_to_executable(
         self, nq_program: Program, *, api_options: Optional[QPUCompilerAPIOptions] = None, **kwargs: Any
     ) -> QuantumExecutable:
-        """
-        Convert a native Quil program into an executable binary which can be executed by a QPU.
+        """Convert a native Quil program into an executable binary which can be executed by a QPU.
 
         If `api_options` is provided, it overrides the options set on `self`.
         """
@@ -149,8 +146,7 @@ def _fetch_calibration_program(self) -> Program:
         return Program(response)
 
     def get_calibration_program(self, force_refresh: bool = False) -> Program:
-        """
-        Get the Quil-T calibration program associated with the underlying QPU.
+        """Get the Quil-T calibration program associated with the underlying QPU.
 
         This will return a cached reference of the calibration program if present.
         Otherwise (or if forcing a refresh), it will fetch and store the
@@ -168,28 +164,23 @@ def get_calibration_program(self, force_refresh: bool = False) -> Program:
         hardware.
 
         :param force_refresh: Whether or not to fetch a new calibration program before returning.
-        :returns: A Program object containing the calibration definitions."""
+        :returns: A Program object containing the calibration definitions.
+        """
         if force_refresh or self._calibration_program is None:
             try:
                 self._calibration_program = self._fetch_calibration_program()
             except Exception as ex:
                 raise RuntimeError("Could not fetch calibrations") from ex
 
-        assert self._calibration_program is not None
         return self._calibration_program
 
     def reset(self) -> None:
-        """
-        Reset the state of the QPUCompiler.
-        """
-        super().reset()
+        """Reset the state of the QPUCompiler."""
         self._calibration_program = None
 
 
 class QVMCompiler(AbstractCompiler):
-    """
-    Client to communicate with the compiler.
-    """
+    """Client to communicate with the compiler."""
 
     def __init__(
         self,
@@ -199,8 +190,7 @@ def __init__(
         client_configuration: Optional[QCSClient] = None,
         quilc_client: Optional[QuilcClient] = None,
     ) -> None:
-        """
-        Client to communicate with compiler.
+        """Client to communicate with compiler.
 
         :param quantum_processor: Quantum processor to use as compilation target.
         :param timeout: Number of seconds to wait for a response from the client.
@@ -216,6 +206,9 @@ def __init__(
     def native_quil_to_executable(self, nq_program: Program, **kwargs: Any) -> QuantumExecutable:
         return nq_program
 
+    def reset(self) -> None:
+        pass
+
 
 class IncompatibleBackendForQuantumProcessorIDWarning(Warning):
     pass
@@ -224,8 +217,7 @@ class IncompatibleBackendForQuantumProcessorIDWarning(Warning):
 def select_backend_for_quantum_processor_id(
     quantum_processor_id: str, backend: Optional[TranslationBackend]
 ) -> TranslationBackend:
-    """
-    Check that the translation backend is supported for the quantum processor.
+    """Check that the translation backend is supported for the quantum processor.
 
     If the translation backend is not supported by the quantum processor, a supported
     translation backend is returned.
@@ -245,6 +237,7 @@ def select_backend_for_quantum_processor_id(
         warn(
             "Backend V2 is not supported for Aspen processors. Backend has been changed to V1.",
             IncompatibleBackendForQuantumProcessorIDWarning,
+            stacklevel=2,
         )
         return TranslationBackend.V1
 
@@ -252,6 +245,7 @@ def select_backend_for_quantum_processor_id(
         warn(
             "Backend V1 is only supported for Aspen processors. Backend has been changed to V2.",
             IncompatibleBackendForQuantumProcessorIDWarning,
+            stacklevel=2,
         )
         return TranslationBackend.V2
 
diff --git a/pyquil/api/_compiler_client.py b/pyquil/api/_compiler_client.py
index f0c105b7e..b8fa87644 100644
--- a/pyquil/api/_compiler_client.py
+++ b/pyquil/api/_compiler_client.py
@@ -13,33 +13,31 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from dataclasses import dataclass
 import json
-from typing import List, Optional
+from dataclasses import dataclass
+from typing import Optional
 
 from qcs_sdk import QCSClient
 from qcs_sdk.compiler.quilc import (
-    get_version_info,
-    compile_program,
     CompilerOpts,
-    TargetDevice,
-    conjugate_pauli_by_clifford,
-    generate_randomized_benchmarking_sequence,
     ConjugateByCliffordRequest,
     ConjugatePauliByCliffordResponse,
-    RandomizedBenchmarkingRequest,
     GenerateRandomizedBenchmarkingSequenceResponse,
     NativeQuilMetadata,
     QuilcClient,
+    RandomizedBenchmarkingRequest,
+    TargetDevice,
+    compile_program,
+    conjugate_pauli_by_clifford,
+    generate_randomized_benchmarking_sequence,
+    get_version_info,
 )
 from rpcq.messages import TargetDevice as TargetQuantumProcessor
 
 
 @dataclass
 class CompileToNativeQuilRequest:
-    """
-    Request to compile to native Quil.
-    """
+    """Request to compile to native Quil."""
 
     program: str
     """Program to compile."""
@@ -53,11 +51,9 @@ class CompileToNativeQuilRequest:
 
 @dataclass
 class NativeQuilMetadataResponse:
-    """
-    Metadata for a native Quil program.
-    """
+    """Metadata for a native Quil program."""
 
-    final_rewiring: List[int]
+    final_rewiring: list[int]
     """Output qubit index relabeling due to SWAP insertion."""
 
     gate_depth: Optional[int]
@@ -87,9 +83,7 @@ class NativeQuilMetadataResponse:
 
 @dataclass
 class CompileToNativeQuilResponse:
-    """
-    Compile to native Quil response.
-    """
+    """Compile to native Quil response."""
 
     native_program: str
     """Native Quil program."""
@@ -99,9 +93,7 @@ class CompileToNativeQuilResponse:
 
 
 class CompilerClient:
-    """
-    Client for making requests to a Quil compiler.
-    """
+    """Client for making requests to a Quil compiler."""
 
     def __init__(
         self,
@@ -110,8 +102,7 @@ def __init__(
         request_timeout: float = 10.0,
         quilc_client: Optional[QuilcClient] = None,
     ) -> None:
-        """
-        Instantiate a new compiler client.
+        """Instantiate a new compiler client.
 
         :param client_configuration: Configuration for client.
         :param request_timeout: Timeout for requests, in seconds.
@@ -132,16 +123,11 @@ def __init__(
         self.timeout = request_timeout
 
     def get_version(self) -> str:
-        """
-        Get version info for compiler server.
-        """
-
+        """Get version info for compiler server."""
         return get_version_info(client=self.quilc_client)
 
     def compile_to_native_quil(self, request: CompileToNativeQuilRequest) -> CompileToNativeQuilResponse:
-        """
-        Compile Quil program to native Quil.
-        """
+        """Compile Quil program to native Quil."""
         target_device_json = json.dumps(request.target_quantum_processor.asdict())  # type: ignore
         target_device = TargetDevice.from_json(target_device_json)
 
@@ -154,15 +140,11 @@ def compile_to_native_quil(self, request: CompileToNativeQuilRequest) -> Compile
         return CompileToNativeQuilResponse(native_program=result.program, metadata=result.native_quil_metadata)
 
     def conjugate_pauli_by_clifford(self, request: ConjugateByCliffordRequest) -> ConjugatePauliByCliffordResponse:
-        """
-        Conjugate a Pauli element by a Clifford element.
-        """
+        """Conjugate a Pauli element by a Clifford element."""
         return conjugate_pauli_by_clifford(request=request, client=self.quilc_client)
 
     def generate_randomized_benchmarking_sequence(
         self, request: RandomizedBenchmarkingRequest
     ) -> GenerateRandomizedBenchmarkingSequenceResponse:
-        """
-        Generate a randomized benchmarking sequence.
-        """
+        """Generate a randomized benchmarking sequence."""
         return generate_randomized_benchmarking_sequence(request=request, client=self.quilc_client)
diff --git a/pyquil/api/_errors.py b/pyquil/api/_errors.py
deleted file mode 100644
index 4e52fe4c6..000000000
--- a/pyquil/api/_errors.py
+++ /dev/null
@@ -1,123 +0,0 @@
-##############################################################################
-# Copyright 2018 Rigetti Computing
-#
-#    Licensed under the Apache License, Version 2.0 (the "License");
-#    you may not use this file except in compliance with the License.
-#    You may obtain a copy of the License at
-#
-#        http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS,
-#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#    See the License for the specific language governing permissions and
-#    limitations under the License.
-##############################################################################
-
-import logging
-import sys
-from types import TracebackType
-from typing import Callable, Type
-
-from pyquil.api._logger import logger
-
-
-def exception_handler(
-    exception_type: Type[BaseException],
-    exception: BaseException,
-    traceback: TracebackType,
-    debug_hook: Callable[..., None] = sys.excepthook,
-) -> None:
-    """
-    This allows us to suppress tracebacks for UserMessageError outside of debug mode
-      by overriding the default exception handler.
-    """
-    if logger.level > logging.DEBUG and exception_type is UserMessageError:
-        exception.__traceback__ = None
-    debug_hook(exception_type, exception, traceback)
-
-
-sys.excepthook = exception_handler  # type: ignore
-
-
-class ApiError(RuntimeError):
-    def __init__(self, server_status: str, explanation: str):
-        super(ApiError, self).__init__(self, server_status)
-        self.server_status = server_status
-        self.explanation = explanation
-
-    def __repr__(self) -> str:
-        return repr(str(self))
-
-    def __str__(self) -> str:
-        return "{}\n{}".format(self.server_status, self.explanation)
-
-
-class QVMError(ApiError):
-    def __init__(self, server_status: str):
-        explanation = """
-The QVM returned the above error. This could be due to a bug in the server or a
-bug in your code. If you suspect this to be a bug in pyQuil or Rigetti Forest,
-then please describe the problem in a GitHub issue at:
-    https://github.com/rigetti/pyquil/issues"""
-        super(QVMError, self).__init__(server_status, explanation)
-
-
-class QUILCError(ApiError):
-    def __init__(self, server_status: str):
-        explanation = """
-QUILC returned the above error. This could be due to a bug in the server or a
-bug in your code. If you suspect this to be a bug in pyQuil or Rigetti Forest,
-then please describe the problem in a GitHub issue at:
-    https://github.com/rigetti/pyquil/issues"""
-        super(QUILCError, self).__init__(server_status, explanation)
-
-
-class TooManyQubitsError(ApiError):
-    def __init__(self, server_status: str):
-        explanation = """
-You requested too many qubits on the QVM."""
-        super(TooManyQubitsError, self).__init__(server_status, explanation)
-
-
-class UserMessageError(Exception):
-    """
-    A special class of error which only displays its traceback when the program
-      is run in debug mode (eg, with `LOG_LEVEL=DEBUG`).
-
-    The purpose of this is to improve the user experience, reducing noise in
-      the case of errors for which the cause is known.
-    """
-
-    def __init__(self, message: str):
-        if logger.level <= logging.DEBUG:
-            super().__init__(message)
-        else:
-            self.message = message
-
-    def __str__(self) -> str:
-        if logger.level <= logging.DEBUG:
-            return super(UserMessageError, self).__str__()
-        else:
-            return f"ERROR: {self.message}"
-
-    def __repr__(self) -> str:
-        if logger.level <= logging.DEBUG:
-            return super(UserMessageError, self).__repr__()
-        else:
-            return f"UserMessageError: {str(self)}"
-
-
-class UnknownApiError(ApiError):
-    def __init__(self, server_status: str):
-        explanation = """
-The server has failed to return a proper response. Please describe the problem
-and copy the above message into a GitHub issue at:
-    https://github.com/rigetti/pyquil/issues"""
-        super(UnknownApiError, self).__init__(server_status, explanation)
-
-
-error_mapping = {
-    "quilc_error": QUILCError,
-    "qvm_error": QVMError,
-}
diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py
index 51974839c..e49c83589 100644
--- a/pyquil/api/_qam.py
+++ b/pyquil/api/_qam.py
@@ -14,14 +14,15 @@
 #    limitations under the License.
 ##############################################################################
 from abc import ABC, abstractmethod
+from collections.abc import Iterable, Mapping, Sequence
 from dataclasses import dataclass
-from typing import Any, Generic, Mapping, Optional, TypeVar, Sequence, Union, Dict, List, Iterable
 from datetime import timedelta
+from typing import Any, Generic, Optional, TypeVar, Union
 
-from deprecated import deprecated
 import numpy as np
+from deprecated import deprecated
 from qcs_sdk import ExecutionData
-from qcs_sdk.qpu import RawQPUReadoutData, MemoryValues
+from qcs_sdk.qpu import MemoryValues, RawQPUReadoutData
 from qcs_sdk.qvm import RawQVMReadoutData
 
 from pyquil.api._abstract_compiler import QuantumExecutable
@@ -50,10 +51,8 @@ class QAMExecutionResult:
     to get at the data in a more convenient format.
     """
 
-    def get_register_map(self) -> Dict[str, Optional[np.ndarray]]:
-        """
-        A mapping of a register name (ie. "ro") to a ``np.ndarray`` containing the values for the
-        register.
+    def get_register_map(self) -> dict[str, Optional[np.ndarray]]:
+        """Map a register name (ie. "ro") to a ``np.ndarray`` containing the values for the register.
 
         Raises a ``RegisterMatrixConversionError`` if the inner execution data for any of the
         registers would result in a jagged matrix. QPU result data is captured per measure,
@@ -76,8 +75,9 @@ def get_register_map(self) -> Dict[str, Optional[np.ndarray]]:
         return {key: matrix.to_ndarray() for key, matrix in register_map.items()}
 
     def get_raw_readout_data(self) -> Union[RawQVMReadoutData, RawQPUReadoutData]:
-        """
-        Get the raw result data. This will be a flattened structure derived
+        """Get the raw result data.
+
+        This will be a flattened structure derived
         from :class:`qcs_sdk.qvm.QVMResultData` or :class:`qcs_sdk.qpu.QPUResultData`
         depending on where the job was run. See their respective documentation
         for more information on the data format.
@@ -91,12 +91,10 @@ def get_raw_readout_data(self) -> Union[RawQVMReadoutData, RawQPUReadoutData]:
         return self.data.result_data.to_raw_readout_data()
 
     def get_memory_values(self) -> Mapping[str, MemoryValues]:
-        """
-        Get the final memory values for any memory region that was both read from
-        and written to during execution. This method will only return the final
-        value in memory after the job has completed. Because of this, memory
-        values should not be used to get readout data. Instead, use `get_register_map()`
-        or `get_raw_readout_data()`.
+        """Get the final memory values for any memory region that was both read from and written to during execution.
+
+        This method will only return the final value in memory after the job has completed. Because of this, memory
+        values should not be used to get readout data. Instead, use `get_register_map()` or `get_raw_readout_data()`.
         """
         if self.data.result_data.is_qpu():
             return self.data.result_data.to_qpu().memory_values
@@ -123,10 +121,7 @@ def execution_duration_microseconds(self) -> Optional[float]:
 
 
 class QAM(ABC, Generic[T]):
-    """
-    Quantum Abstract Machine: This class acts as a generic interface describing how a classical
-    computer interacts with a live quantum computer.
-    """
+    """Quantum Abstract Machine: An interface describing how a classical computer interacts with a quantum computer."""
 
     @abstractmethod
     def execute(
@@ -135,9 +130,7 @@ def execute(
         memory_map: Optional[MemoryMap] = None,
         **kwargs: Any,
     ) -> T:
-        """
-        Run an executable on a QAM, returning a handle to be used to retrieve
-        results.
+        """Run an executable on a QAM, returning a handle to be used to retrieve results.
 
         :param executable: The executable program to be executed by the QAM.
         :param memory_map: A mapping of memory regions to a list containing the values to be written into that memory
@@ -150,9 +143,8 @@ def execute_with_memory_map_batch(
         executable: QuantumExecutable,
         memory_maps: Iterable[MemoryMap],
         **kwargs: Any,
-    ) -> List[T]:
-        """
-        Execute a QuantumExecutable with one or more memory_maps, returning handles to be used to retrieve results.
+    ) -> list[T]:
+        """Execute a QuantumExecutable with one or more memory_maps, returning handles to be used to retrieve results.
 
         How these programs are batched and executed is determined by the executor. See their respective documentation
         for details.
@@ -162,8 +154,7 @@ def execute_with_memory_map_batch(
 
     @abstractmethod
     def get_result(self, execute_response: T) -> QAMExecutionResult:
-        """
-        Retrieve the results associated with a previous call to ``QAM#execute``.
+        """Retrieve the results associated with a previous call to ``QAM#execute``.
 
         :param execute_response: The return value from a call to ``execute``.
         """
@@ -171,7 +162,5 @@ def get_result(self, execute_response: T) -> QAMExecutionResult:
     def run(
         self, executable: QuantumExecutable, memory_map: Optional[MemoryMap] = None, **kwargs: Any
     ) -> QAMExecutionResult:
-        """
-        Run an executable to completion on the QAM.
-        """
+        """Run an executable to completion on the QAM."""
         return self.get_result(self.execute(executable, memory_map, **kwargs))
diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py
index 1607e963d..b82290aa0 100644
--- a/pyquil/api/_qpu.py
+++ b/pyquil/api/_qpu.py
@@ -13,38 +13,37 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from dataclasses import dataclass
 from collections import defaultdict
+from collections.abc import Iterable
+from dataclasses import dataclass
 from datetime import timedelta
-from typing import Any, Dict, Optional, Union, List, Iterable
+from typing import Any, Optional, Union
 
 import numpy as np
 from numpy.typing import NDArray
-from qcs_sdk.qpu import ReadoutValues, QPUResultData
-from rpcq.messages import ParameterSpec
-
-from pyquil.api import QuantumExecutable, EncryptedProgram
-
-from pyquil.api._qam import MemoryMap, QAM, QAMExecutionResult
-from pyquil.quilatom import (
-    MemoryReference,
-)
-from qcs_sdk import QCSClient, ResultData, ExecutionData
+from qcs_sdk import ExecutionData, QCSClient, ResultData
+from qcs_sdk.qpu import QPUResultData, ReadoutValues
 from qcs_sdk.qpu.api import (
-    retrieve_results,
-    cancel_job,
     ConnectionStrategy,
-    ExecutionResult,
     ExecutionOptions,
     ExecutionOptionsBuilder,
+    ExecutionResult,
+    cancel_job,
+    retrieve_results,
     submit_with_parameter_batch,
 )
 from qcs_sdk.qpu.rewrite_arithmetic import build_patch_values
+from rpcq.messages import ParameterSpec
+
+from pyquil.api import EncryptedProgram, QuantumExecutable
+from pyquil.api._qam import QAM, MemoryMap, QAMExecutionResult
+from pyquil.quilatom import (
+    MemoryReference,
+)
 
 
 def decode_buffer(buffer: ExecutionResult) -> Union[NDArray[np.complex64], NDArray[np.int32]]:
-    """
-    Translate a DataBuffer into a numpy array.
+    """Translate a DataBuffer into a numpy array.
 
     :param buffer: Dictionary with 'data' byte array, 'dtype', and 'shape' fields
     :return: NumPy array of decoded data
@@ -57,10 +56,10 @@ def decode_buffer(buffer: ExecutionResult) -> Union[NDArray[np.complex64], NDArr
 
 
 def _extract_memory_regions(
-    memory_descriptors: Dict[str, ParameterSpec],
-    ro_sources: Dict[MemoryReference, str],
-    buffers: Dict[str, np.ndarray],
-) -> Dict[str, np.ndarray]:
+    memory_descriptors: dict[str, ParameterSpec],
+    ro_sources: dict[MemoryReference, str],
+    buffers: dict[str, np.ndarray],
+) -> dict[str, np.ndarray]:
     # hack to extract num_shots indirectly from the shape of the returned data
     first, *rest = buffers.values()
     num_shots = first.shape[0]
@@ -77,7 +76,7 @@ def alloc(spec: ParameterSpec) -> np.ndarray:
         except KeyError as e:
             raise ValueError(f"Unexpected memory type {spec.type}.") from e
 
-    regions: Dict[str, np.ndarray] = {}
+    regions: dict[str, np.ndarray] = {}
 
     for mref, key in ro_sources.items():
         # Translation sometimes introduces ro_sources that the user didn't ask for.
@@ -126,8 +125,7 @@ def __init__(
         endpoint_id: Optional[str] = None,
         execution_options: Optional[ExecutionOptions] = None,
     ) -> None:
-        """
-        A connection to the QPU.
+        """Connect to the QPU.
 
         :param quantum_processor_id: Processor to run against.
         :param priority: The priority with which to insert jobs into the QPU queue. Lower integers
@@ -143,8 +141,8 @@ def __init__(
         self.priority = priority
 
         self._client_configuration = client_configuration or QCSClient.load()
-        self._last_results: Dict[str, np.ndarray] = {}
-        self._memory_results: Dict[str, Optional[np.ndarray]] = defaultdict(lambda: None)
+        self._last_results: dict[str, np.ndarray] = {}
+        self._memory_results: dict[str, Optional[np.ndarray]] = defaultdict(lambda: None)
         self._quantum_processor_id = quantum_processor_id
         if execution_options is None:
             execution_options_builder = ExecutionOptionsBuilder.default()
@@ -167,10 +165,10 @@ def execute(
         execution_options: Optional[ExecutionOptions] = None,
         **__: Any,
     ) -> QPUExecuteResponse:
-        """
-        Enqueue a job for execution on the QPU. Returns a ``QPUExecuteResponse``, a
-        job descriptor which should be passed directly to ``QPU.get_result`` to retrieve
-        results.
+        """Enqueue a job for execution on the QPU.
+
+        Returns a ``QPUExecuteResponse``, a job descriptor which should be passed directly to ``QPU.get_result`` to
+        retrieve results.
 
         :param:
             execution_options: An optional `ExecutionOptions` enum that can be used
@@ -179,7 +177,10 @@ def execute(
         """
         memory_map = memory_map or {}
         responses = self.execute_with_memory_map_batch(executable, [memory_map], execution_options)
-        assert len(responses) == 1, "Request to execute job with a single memory map returned multiple responses"
+
+        if len(responses) != 1:
+            raise RuntimeError("Request to execute a single job returned 0, or more than 1 responses.")
+
         return responses[0]
 
     def execute_with_memory_map_batch(
@@ -188,9 +189,8 @@ def execute_with_memory_map_batch(
         memory_maps: Iterable[MemoryMap],
         execution_options: Optional[ExecutionOptions] = None,
         **__: Any,
-    ) -> List[QPUExecuteResponse]:
-        """
-        Execute a compiled program on a QPU with multiple sets of `memory_maps`.
+    ) -> list[QPUExecuteResponse]:
+        """Execute a compiled program on a QPU with multiple sets of `memory_maps`.
 
         See the documentation of `qcs_sdk.qpu.api.submit_with_parameter_batch` for more information.
 
@@ -202,13 +202,8 @@ def execute_with_memory_map_batch(
         """
         executable = executable.copy()
 
-        assert isinstance(
-            executable, EncryptedProgram
-        ), "QPU#execute requires an rpcq.EncryptedProgram. Create one with QuantumComputer#compile"
-
-        assert (
-            executable.ro_sources is not None
-        ), "To run on a QPU, a program must include ``MEASURE``, ``CAPTURE``, and/or ``RAW-CAPTURE`` instructions"
+        if not isinstance(executable, EncryptedProgram):
+            raise ValueError("QPU#execute requires an rpcq.EncryptedProgram. Create one with QuantumComputer#compile")
 
         patch_values = []
         for memory_map in memory_maps:
@@ -234,8 +229,7 @@ def execute_with_memory_map_batch(
         return responses
 
     def cancel(self, execute_response: QPUExecuteResponse) -> None:
-        """
-        Cancel a job that has yet to begin executing.
+        """Cancel a job that has yet to begin executing.
 
         This action is *not* atomic, and will attempt to cancel a job even if it cannot be cancelled. A
         job can be cancelled only if it has not yet started executing.
@@ -254,10 +248,7 @@ def cancel(self, execute_response: QPUExecuteResponse) -> None:
         )
 
     def get_result(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult:
-        """
-        Retrieve results from execution on the QPU.
-        """
-
+        """Retrieve results from execution on the QPU."""
         results = retrieve_results(
             job_id=execute_response.job_id,
             quantum_processor_id=self.quantum_processor_id,
diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py
index bf52ed1c8..63926f60e 100644
--- a/pyquil/api/_quantum_computer.py
+++ b/pyquil/api/_quantum_computer.py
@@ -18,32 +18,26 @@
 import socket
 import subprocess
 import warnings
+from collections.abc import Iterable, Iterator, Sequence
 from contextlib import contextmanager
-from math import pi, log
+from math import log, pi
 from typing import (
     Any,
-    Tuple,
-    Iterator,
-    Iterable,
     Optional,
-    Set,
     Union,
     cast,
-    List,
 )
 
 import networkx as nx
 import numpy as np
-
 from qcs_sdk import QCSClient
-from qcs_sdk.qpu import list_quantum_processors
 from qcs_sdk.compiler.quilc import QuilcClient
+from qcs_sdk.qpu import list_quantum_processors
 from qcs_sdk.qvm import QVMClient
 
 from pyquil.api._abstract_compiler import AbstractCompiler, QuantumExecutable
 from pyquil.api._compiler import QPUCompiler, QVMCompiler
-
-from pyquil.api._qam import QAM, QAMExecutionResult, MemoryMap
+from pyquil.api._qam import QAM, MemoryMap, QAMExecutionResult
 from pyquil.api._qpu import QPU
 from pyquil.api._qvm import QVM
 from pyquil.experiment._main import Experiment
@@ -51,17 +45,18 @@
 from pyquil.experiment._result import ExperimentResult, bitstrings_to_expectations
 from pyquil.experiment._setting import ExperimentSetting
 from pyquil.external.rpcq import CompilerISA
-from pyquil.gates import RX, MEASURE
-from pyquil.noise import decoherence_noise_with_asymmetric_ro, NoiseModel
+from pyquil.gates import MEASURE, RX
+from pyquil.noise import NoiseModel, decoherence_noise_with_asymmetric_ro
 from pyquil.paulis import PauliTerm
 from pyquil.pyqvm import PyQVM
 from pyquil.quantum_processor import (
     AbstractQuantumProcessor,
-    QCSQuantumProcessor,
     NxQuantumProcessor,
+    QCSQuantumProcessor,
     get_qcs_quantum_processor,
 )
 from pyquil.quil import Program
+from pyquil.quilatom import QubitDesignator
 
 
 class QuantumComputer:
@@ -73,8 +68,7 @@ def __init__(
         compiler: AbstractCompiler,
         symmetrize_readout: bool = False,
     ) -> None:
-        """
-        A quantum computer for running quantum programs.
+        """Use a quantum computer to run quantum programs.
 
         A quantum computer has various characteristics like supported gates, qubits, qubit
         topologies, gate fidelities, and more. A quantum computer also has the ability to
@@ -98,31 +92,25 @@ def __init__(
 
     @property
     def quantum_processor(self) -> AbstractQuantumProcessor:
-        """
-        The quantum processor associated with this quantum computer.
-        """
+        """The quantum processor associated with this quantum computer."""
         return self.compiler.quantum_processor
 
-    def qubits(self) -> List[int]:
-        """
-        Return a sorted list of this QuantumComputer's quantum_processor's qubits
+    def qubits(self) -> list[int]:
+        """Return a sorted list of the quantum_processor's qubits.
 
         See :py:func:`AbstractQuantumProcessor.qubits` for more.
         """
         return self.compiler.quantum_processor.qubits()
 
     def qubit_topology(self) -> nx.graph:
-        """
-        Return a NetworkX graph representation of this QuantumComputer's quantum_processor's qubit
-        connectivity.
+        """Return a NetworkX graph representation of this QuantumComputer's quantum_processor's qubit connectivity.
 
         See :py:func:`AbstractQuantumProcessor.qubit_topology` for more.
         """
         return self.compiler.quantum_processor.qubit_topology()
 
     def to_compiler_isa(self) -> CompilerISA:
-        """
-        Return a ``CompilerISA`` for this QuantumComputer's quantum_processor.
+        """Return a ``CompilerISA`` for this QuantumComputer's quantum_processor.
 
         See :py:func:`AbstractQuantumProcessor.to_compiler_isa` for more.
         """
@@ -131,9 +119,7 @@ def to_compiler_isa(self) -> CompilerISA:
     def run(
         self, executable: QuantumExecutable, memory_map: Optional[MemoryMap] = None, **kwargs: Any
     ) -> QAMExecutionResult:
-        """
-        Run a quil executable. All parameters in the executable must have values applied using
-        ``Program#write_memory``.
+        """Run a quil executable.
 
         :param executable: The program to run, previously compiled as needed for its target QAM.
         :param memory_map: A mapping of memory regions to a list containing the values to be written into that memory
@@ -144,10 +130,10 @@ def run(
 
     def run_with_memory_map_batch(
         self, executable: QuantumExecutable, memory_maps: Iterable[MemoryMap], **kwargs: Any
-    ) -> List[QAMExecutionResult]:
-        """
-        Run a QuantumExecutable with one or more memory_maps, returning a list of results corresponding to the length
-        and order of the given MemoryMaps.
+    ) -> list[QAMExecutionResult]:
+        """Run a QuantumExecutable with one or more memory_map.
+
+        Returns a list of results corresponding to the length and order of the given MemoryMaps.
 
         How these programs are batched and executed is determined by the executor. See their respective documentation
         for details.
@@ -158,10 +144,8 @@ def run_with_memory_map_batch(
         handles = self.qam.execute_with_memory_map_batch(executable, memory_maps, **kwargs)
         return [self.qam.get_result(handle) for handle in handles]
 
-    def calibrate(self, experiment: Experiment) -> List[ExperimentResult]:
-        """
-        Perform readout calibration on the various multi-qubit observables involved in the provided
-        ``Experiment``.
+    def calibrate(self, experiment: Experiment) -> list[ExperimentResult]:
+        """Perform readout calibration on the various multi-qubit observables involved in the provided ``Experiment``.
 
         :param experiment: The ``Experiment`` to calibrate readout error for.
         :return: A list of ``ExperimentResult`` objects that contain the expectation values that
@@ -174,9 +158,10 @@ def run_experiment(
         self,
         experiment: Experiment,
         memory_map: Optional[MemoryMap] = None,
-    ) -> List[ExperimentResult]:
-        """
-        Run an ``Experiment`` on a QVM or QPU backend. An ``Experiment`` is composed of:
+    ) -> list[ExperimentResult]:
+        """Run an ``Experiment`` on a QVM or QPU backend.
+
+        An ``Experiment`` is composed of:
 
             - A main ``Program`` body (or ansatz).
             - A collection of ``ExperimentSetting`` objects, each of which encodes a particular
@@ -222,7 +207,6 @@ def run_experiment(
         :return: A list of ``ExperimentResult`` objects containing the statistics gathered
             according to the specifications of the ``Experiment``.
         """
-
         experiment_program = experiment.generate_experiment_program()
         executable = self.compile(experiment_program)
 
@@ -236,7 +220,7 @@ def run_experiment(
                 raise ValueError("We only support length-1 settings for now.")
             setting = settings[0]
 
-            qubits = cast(List[int], setting.out_operator.get_qubits())
+            qubits = cast(list[int], setting.out_operator.get_qubits())
             experiment_setting_memory_map = experiment.build_setting_memory_map(setting)
             symmetrization_memory_maps = experiment.build_symmetrization_memory_maps(qubits)
             merged_memory_maps = merge_memory_map_lists([experiment_setting_memory_map], symmetrization_memory_maps)
@@ -246,7 +230,8 @@ def run_experiment(
                 final_memory_map = {**memory_map, **merged_memory_map}
                 executable_copy = executable.copy()
                 bitstrings = self.run(executable_copy, memory_map=final_memory_map).readout_data.get("ro")
-                assert bitstrings is not None
+                if bitstrings is None:
+                    raise ValueError("No readout data returned.")
 
                 if "symmetrization" in final_memory_map:
                     bitmask = np.array(np.array(final_memory_map["symmetrization"]) / np.pi, dtype=int)
@@ -289,38 +274,36 @@ def run_symmetrized_readout(
         program: Program,
         trials: int,
         symm_type: int = 3,
-        meas_qubits: Optional[List[int]] = None,
+        meas_qubits: Optional[Sequence[QubitDesignator]] = None,
     ) -> np.ndarray:
-        r"""
-        Run a quil program in such a way that the readout error is made symmetric. Enforcing
-        symmetric readout error is useful in simplifying the assumptions in some near
-        term error mitigation strategies, see ``measure_observables`` for more information.
-
-        The simplest example is for one qubit. In a noisy quantum_processor, the probability of accurately
-        reading the 0 state might be higher than that of the 1 state; due to e.g. amplitude
-        damping. This makes correcting for readout more difficult. In the simplest case, this
-        function runs the program normally ``(trials//2)`` times. The other half of the time,
-        it will insert an ``X`` gate prior to any ``MEASURE`` instruction and then flip the
+        r"""Run a quil program in such a way that the readout error is made symmetric.
+
+        Enforcing symmetric readout error is useful in simplifying the assumptions in some near term error mitigation
+        strategies, see ``measure_observables`` for more information.
+
+        The simplest example is for one qubit. In a noisy quantum_processor, the probability of accurately reading the
+        0 state might be higher than that of the 1 state; due to e.g. amplitude damping. This makes correcting for
+        readout more difficult. In the simplest case, this function runs the program normally ``(trials//2)`` times.
+        The other half of the time, it will insert an ``X`` gate prior to any ``MEASURE`` instruction and then flip th
         measured classical bit back. Overall this has the effect of symmetrizing the readout error.
 
-        The details. Consider preparing the input bitstring ``|i>`` (in the computational basis) and
-        measuring in the Z basis. Then the Confusion matrix for the readout error is specified by
-        the probabilities
+        The details. Consider preparing the input bitstring ``|i>`` (in the computational basis) and measuring in the
+        Z basis. Then the Confusion matrix for the readout error is specified by the probabilities
 
              p(j|i) := Pr(measured = j | prepared = i ).
 
         In the case of a single qubit i,j \in [0,1] then:
-        there is no readout error if p(0|0) = p(1|1) = 1.
-        the readout error is symmetric if p(0|0) = p(1|1) = 1 - epsilon.
-        the readout error is asymmetric if p(0|0) != p(1|1).
 
-        If your quantum computer has this kind of asymmetric readout error then
-        ``qc.run_symmetrized_readout`` will symmetrize the readout error.
+        * there is no readout error if p(0|0) = p(1|1) = 1.
+        * the readout error is symmetric if p(0|0) = p(1|1) = 1 - epsilon.
+        * the readout error is asymmetric if p(0|0) != p(1|1).
+
+        If your quantum computer has this kind of asymmetric readout error then ``qc.run_symmetrized_readout`` will
+        symmetrize the readout error.
 
-        The readout error above is only asymmetric on a single bit. In practice the confusion
-        matrix on n bits need not be symmetric, e.g. for two qubits p(ij|ij) != 1 - epsilon for
-        all i,j. In these situations a more sophisticated means of symmetrization is needed; and
-        we use orthogonal arrays (OA) built from Hadamard matrices.
+        The readout error above is only asymmetric on a single bit. In practice the confusion matrix on n bits need not
+        be symmetric, e.g. for two qubits p(ij|ij) != 1 - epsilon for all i,j. In these situations a more sophisticated
+        means of symmetrization is needed; and we use orthogonal arrays (OA) built from Hadamard matrices.
 
         The symmetrization types are specified by an int; the types available are:
         -1 -- exhaustive symmetrization uses every possible combination of flips
@@ -331,11 +314,10 @@ def run_symmetrized_readout(
         In the context of readout symmetrization the strength of the orthogonal array enforces
         the symmetry of the marginal confusion matrices.
 
-        By default a strength 3 OA is used; this ensures expectations of the form
-        ``<b_k . b_j . b_i>`` for bits any bits i,j,k will have symmetric readout errors. Here
-        expectation of a random variable x as is denote ``<x> = sum_i Pr(i) x_i``. It turns out that
-        a strength 3 OA is also a strength 2 and strength 1 OA it also ensures ``<b_j . b_i>`` and
-        ``<b_i>`` have symmetric readout errors for any bits b_j and b_i.
+        By default a strength 3 OA is used; this ensures expectations of the form ``<b_k . b_j . b_i>`` for bits any
+        bits i,j,k will have symmetric readout errors. Here expectation of a random variable x as is denote
+        ``<x> = sum_i Pr(i) x_i``. It turns out that a strength 3 OA is also a strength 2 and strength 1 OA it also
+        ensures ``<b_j . b_i>`` and ``<b_i>`` have symmetric readout errors for any bits b_j and b_i.
 
         :param program: The program to run symmetrized readout on.
         :param trials: The minimum number of times to run the program; it is recommend that this
@@ -352,7 +334,7 @@ def run_symmetrized_readout(
             )
 
         if meas_qubits is None:
-            meas_qubits = list(cast(Set[int], program.get_qubits()))
+            meas_qubits = list(program.get_qubit_indices())
 
         # It is desirable to have hundreds or thousands of trials more than the minimum
         trials = _check_min_num_trials_for_symmetrized_readout(len(meas_qubits), trials, symm_type)
@@ -383,19 +365,17 @@ def compile(
         *,
         protoquil: Optional[bool] = None,
     ) -> QuantumExecutable:
-        """
-        A high-level interface to program compilation.
+        """Provide a high-level interface for program compilation.
 
-        Compilation currently consists of two stages. Please see the :py:class:`AbstractCompiler`
-        docs for more information. This function does all stages of compilation.
+        Compilation currently consists of two stages. Please see the :py:class:`AbstractCompiler` docs for more
+        information. This function does all stages of compilation.
 
-        Right now both ``to_native_gates`` and ``optimize`` must be either both set or both
-        unset. More modular compilation passes may be available in the future.
+        Right now both ``to_native_gates`` and ``optimize`` must be either both set or both unset. More modular
+        compilation passes may be available in the future.
 
-        Additionally, a call to compile also calls the ``reset`` method if one is running
-        on the QPU. This is a bit of a sneaky hack to guard against stale compiler connections,
-        but shouldn't result in any material hit to performance (especially when taking advantage
-        of parametric compilation for hybrid applications).
+        Additionally, a call to compile also calls the ``reset`` method if one is running on the QPU. This is a bit of
+        a sneaky hack to guard against stale compiler connections, but shouldn't result in any material hit to
+        performance (especially when taking advantage of parametric compilation for hybrid applications).
 
         :param program: A Program
         :param to_native_gates: Whether to compile non-native gates to native gates.
@@ -404,9 +384,10 @@ def compile(
             to protoquil (executable on QPU). A value of ``None`` means defer to server.
         :return: An executable binary suitable for passing to :py:func:`QuantumComputer.run`.
         """
-
         flags = [to_native_gates, optimize]
-        assert all(flags) or all(not f for f in flags), "Must turn quilc all on or all off"
+        if any(flags) and not all(flags):
+            raise ValueError("Must turn to_native_gates and optimize on or off together")
+
         quilc = all(flags)
 
         if quilc:
@@ -428,9 +409,8 @@ def list_quantum_computers(
     qvms: bool = True,
     timeout: float = 10.0,
     client_configuration: Optional[QCSClient] = None,
-) -> List[str]:
-    """
-    List the names of available quantum computers
+) -> list[str]:
+    """List the names of available quantum computers.
 
     :param qpus: Whether to include QPUs in the list.
     :param qvms: Whether to include QVMs in the list.
@@ -438,7 +418,7 @@ def list_quantum_computers(
     :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded.
     """
     client_configuration = client_configuration or QCSClient.load()
-    qc_names: List[str] = []
+    qc_names: list[str] = []
     if qpus:
         qc_names += list_quantum_processors(client=client_configuration, timeout=timeout)
 
@@ -448,9 +428,8 @@ def list_quantum_computers(
     return qc_names
 
 
-def _parse_name(name: str, as_qvm: Optional[bool], noisy: Optional[bool]) -> Tuple[str, Optional[str], bool]:
-    """
-    Try to figure out whether we're getting a (noisy) qvm, and the associated qpu name.
+def _parse_name(name: str, as_qvm: Optional[bool], noisy: Optional[bool]) -> tuple[str, Optional[str], bool]:
+    """Try to figure out whether we're getting a (noisy) qvm, and the associated qpu name.
 
     See :py:func:`get_qc` for examples of valid names + flags.
     """
@@ -525,10 +504,11 @@ def _get_qvm_or_pyqvm(
     if qvm_type == "qvm":
         return QVM(noise_model=noise_model, timeout=execution_timeout, client=qvm_client)
     elif qvm_type == "pyqvm":
-        assert quantum_processor is not None
+        if quantum_processor is None:
+            raise ValueError("Cannot construct a PyQVM without a quantum_processor.")
         return PyQVM(n_qubits=quantum_processor.qubit_topology().number_of_nodes())
 
-    raise ValueError("Unknown qvm type {}".format(qvm_type))
+    raise ValueError(f"Unknown qvm type {qvm_type}")
 
 
 def _get_qvm_qc(
@@ -556,7 +536,6 @@ def _get_qvm_qc(
     :param execution_timeout: Time limit for execution requests, in seconds.
     :return: A QuantumComputer backed by a QVM with the above options.
     """
-
     return QuantumComputer(
         name=name,
         qam=_get_qvm_or_pyqvm(
@@ -633,8 +612,7 @@ def _get_9q_square_qvm(
     quilc_client: Optional[QuilcClient] = None,
     qvm_client: Optional[QVMClient] = None,
 ) -> QuantumComputer:
-    """
-    A nine-qubit 3x3 square lattice.
+    """Nine-qubit 3x3 square lattice.
 
     This uses a "generic" lattice not tied to any specific quantum_processor. 9 qubits is large enough
     to do vaguely interesting algorithms and small enough to simulate quickly.
@@ -673,8 +651,7 @@ def _get_unrestricted_qvm(
     quilc_client: Optional[QuilcClient] = None,
     qvm_client: Optional[QVMClient] = None,
 ) -> QuantumComputer:
-    """
-    A qvm with a fully-connected topology.
+    """QVM with a fully-connected topology.
 
     This is obviously the least realistic QVM, but who am I to tell users what they want.
 
@@ -713,8 +690,7 @@ def _get_qvm_based_on_real_quantum_processor(
     quilc_client: Optional[QuilcClient] = None,
     qvm_client: Optional[QVMClient] = None,
 ) -> QuantumComputer:
-    """
-    A qvm with a based on a real quantum_processor.
+    """QVM based on a real quantum_processor.
 
     This is the most realistic QVM.
 
@@ -757,8 +733,7 @@ def get_qc(
     quilc_client: Optional[QuilcClient] = None,
     qvm_client: Optional[QVMClient] = None,
 ) -> QuantumComputer:
-    """
-    Get a quantum computer.
+    """Get a quantum computer.
 
     A quantum computer is an object of type :py:class:`QuantumComputer` and can be backed
     either by a QVM simulator ("Quantum/Quil Virtual Machine") or a physical Rigetti QPU ("Quantum
@@ -939,14 +914,14 @@ def local_forest_runtime(
     qvm_port: int = 5000,
     quilc_port: int = 5555,
     use_protoquil: bool = False,
-) -> Iterator[Tuple[Optional[subprocess.Popen], Optional[subprocess.Popen]]]:
-    """A context manager for local QVM and QUIL compiler.
+) -> Iterator[tuple[Optional[subprocess.Popen], Optional[subprocess.Popen]]]:
+    """Context manager for local QVM and QUIL compiler.
 
     You must first have installed the `qvm` and `quilc` executables from
     the forest SDK. [https://www.rigetti.com/forest]
 
     This context manager will ensure that the designated ports are not used, start up `qvm` and
-    `quilc` proccesses if possible and terminate them when the context is exited.
+    `quilc` processes if possible and terminate them when the context is exited.
     If one of the ports is in use, a ``RuntimeWarning`` will be issued and the `qvm`/`quilc` process
     won't be started.
 
@@ -959,7 +934,7 @@ def local_forest_runtime(
     >>> from pyquil.gates import CNOT, Z
     >>> from pyquil.api import local_forest_runtime
     >>>
-    >>> qvm = get_qc('9q-square-qvm')
+    >>> qvm = get_qc("9q-square-qvm")
     >>> prog = Program(Z(0), CNOT(0, 1))
     >>>
     >>> with local_forest_runtime():  # doctest: +SKIP
@@ -981,21 +956,20 @@ def local_forest_runtime(
         ports is in use, the process won't be started and the respective
         value in the tuple will be ``None``.
     """
-
     qvm: Optional[subprocess.Popen] = None
     quilc: Optional[subprocess.Popen] = None
 
     # If the host we should listen to is 0.0.0.0, we replace it
     # with 127.0.0.1 to use a valid IP when checking if the port is in use.
-    if _port_used(host if host != "0.0.0.0" else "127.0.0.1", qvm_port):
-        warning_msg = ("Unable to start qvm server, since the specified port {} is in use.").format(qvm_port)
+    if _port_used(host if host != "0.0.0.0" else "127.0.0.1", qvm_port):  # noqa: S104: prevents connection to 0.0.0.0
+        warning_msg = f"Unable to start qvm server, since the specified port {qvm_port} is in use."
         warnings.warn(RuntimeWarning(warning_msg), stacklevel=2)
     else:
         qvm_cmd = ["qvm", "-S", "--host", host, "-p", str(qvm_port)]
-        qvm = subprocess.Popen(qvm_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+        qvm = subprocess.Popen(qvm_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)  # noqa: S603: input valid
 
-    if _port_used(host if host != "0.0.0.0" else "127.0.0.1", quilc_port):
-        warning_msg = ("Unable to start quilc server, since the specified port {} is in use.").format(quilc_port)
+    if _port_used(host if host != "0.0.0.0" else "127.0.0.1", quilc_port):  # noqa: S104: prevents connection to 0.0.0.0
+        warning_msg = f"Unable to start quilc server, since the specified port {quilc_port} is in use."
         warnings.warn(RuntimeWarning(warning_msg), stacklevel=2)
     else:
         quilc_cmd = ["quilc", "--host", host, "-p", str(quilc_port), "-R"]
@@ -1003,7 +977,7 @@ def local_forest_runtime(
         if use_protoquil:
             quilc_cmd += ["-P"]
 
-        quilc = subprocess.Popen(quilc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+        quilc = subprocess.Popen(quilc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)  # noqa: S603
 
     # Return context
     try:
@@ -1017,10 +991,8 @@ def local_forest_runtime(
             quilc.terminate()
 
 
-def _flip_array_to_prog(flip_array: Tuple[bool], qubits: List[int]) -> Program:
-    """
-    Generate a pre-measurement program that flips the qubit state according to the flip_array of
-    bools.
+def _flip_array_to_prog(flip_array: tuple[bool], qubits: Sequence[QubitDesignator]) -> Program:
+    """Generate a pre-measurement program that flips the qubit state according to the flip_array of bools.
 
     This is used, for example, in symmetrization to produce programs which flip a select subset
     of qubits immediately before measurement.
@@ -1030,7 +1002,9 @@ def _flip_array_to_prog(flip_array: Tuple[bool], qubits: List[int]) -> Program:
     :param qubits: list specifying the qubits in order corresponding to the flip_array
     :return: Program which flips each qubit (i.e. instructs RX(pi, q)) according to the flip_array.
     """
-    assert len(flip_array) == len(qubits), "Mismatch of qubits and operations"
+    if len(flip_array) != len(qubits):
+        raise ValueError("Mismatch of qubits and operations")
+
     prog = Program()
     for qubit, flip_output in zip(qubits, flip_array):
         if flip_output == 0:
@@ -1043,11 +1017,9 @@ def _flip_array_to_prog(flip_array: Tuple[bool], qubits: List[int]) -> Program:
 
 
 def _symmetrization(
-    program: Program, meas_qubits: List[int], symm_type: int = 3
-) -> Tuple[List[Program], List[np.ndarray]]:
-    """
-    For the input program generate new programs which flip the measured qubits with an X gate in
-    certain combinations in order to symmetrize readout.
+    program: Program, meas_qubits: Sequence[QubitDesignator], symm_type: int = 3
+) -> tuple[list[Program], list[np.ndarray]]:
+    """Generate new programs that flip the measured qubits with an X gate in order to symmetrize readout.
 
     An expanded list of programs is returned along with a list of bools which indicates which
     qubits are flipped in each program.
@@ -1101,10 +1073,8 @@ def _symmetrization(
     return symm_programs, flip_arrays
 
 
-def _consolidate_symmetrization_outputs(outputs: List[np.ndarray], flip_arrays: List[np.ndarray]) -> np.ndarray:
-    """
-    Given bitarray results from a series of symmetrization programs, appropriately flip output
-    bits and consolidate results into new bitarrays.
+def _consolidate_symmetrization_outputs(outputs: list[np.ndarray], flip_arrays: list[np.ndarray]) -> np.ndarray:
+    """Flip output bits and consolidate results into new bitarrays from symmetrization program bitarray results.
 
     :param outputs: a list of the raw bitarrays resulting from running a list of symmetrized
         programs; for example, the results returned from _measure_bitstrings
@@ -1114,7 +1084,8 @@ def _consolidate_symmetrization_outputs(outputs: List[np.ndarray], flip_arrays:
         the symmetrized outputs of the original programs passed into a symmetrization method. See
         estimate_observables for example usage.
     """
-    assert len(outputs) == len(flip_arrays)
+    if len(outputs) != len(flip_arrays):
+        raise ValueError("The length of outputs must equal the length of flip_arrays")
 
     output = []
     for bitarray, flip_array in zip(outputs, flip_arrays):
@@ -1127,11 +1098,9 @@ def _consolidate_symmetrization_outputs(outputs: List[np.ndarray], flip_arrays:
 
 
 def _measure_bitstrings(
-    qc: QuantumComputer, programs: List[Program], meas_qubits: List[int], num_shots: int = 600
-) -> List[np.ndarray]:
-    """
-    Wrapper for appending measure instructions onto each program, running the program,
-    and accumulating the resulting bitarrays.
+    qc: QuantumComputer, programs: list[Program], meas_qubits: Sequence[QubitDesignator], num_shots: int = 600
+) -> list[np.ndarray]:
+    """Append measure instructions, run the program, and accumulate the resulting bitarrays.
 
     :param qc: a quantum computer object on which to run each program
     :param programs: a list of programs to run
@@ -1153,17 +1122,18 @@ def _measure_bitstrings(
         executable = qc.compiler.native_quil_to_executable(prog)
         result = qc.run(executable)
         shot_values = result.readout_data.get("ro")
-        assert shot_values is not None
+        if shot_values is None:
+            raise RuntimeError("Got no readout data from the quantum computer.")
         results.append(shot_values)
     return results
 
 
 def _construct_orthogonal_array(num_qubits: int, strength: int = 3) -> np.ndarray:
-    """
-    Given a strength and number of qubits this function returns an Orthogonal Array (OA)
-    on 'n' or more qubits. Sometimes the size of the returned array is larger than num_qubits;
-    typically the next power of two relative to num_qubits. This is corrected later in the code
-    flow.
+    """Return an Orthogonal Array (OA) based on the given strength and number of qubits.
+
+    Given a strength and number of qubits, this function returns an Orthogonal Array (OA) on 'n' or more qubits.
+    Sometimes, the size of the returned array is larger than the number of qubits; typically, it is the next power of
+    two relative to the number of qubits. This is corrected later in the code flow.
 
     :param num_qubits: the minimum number of qubits the OA should act on.
     :param strength: the statistical "strength" of the OA
@@ -1195,8 +1165,8 @@ def _next_power_of_2(x: int) -> int:
 # The code below is directly copied from scipy see https://bit.ly/2RjAHJz, the docstrings have
 # been modified.
 def hadamard(n: int, dtype: np.dtype = int) -> np.ndarray:  # type: ignore
-    """
-    Construct a Hadamard matrix.
+    """Construct a Hadamard matrix.
+
     Constructs an n-by-n Hadamard matrix, using Sylvester's
     construction.  `n` must be a power of 2.
 
@@ -1226,6 +1196,7 @@ def hadamard(n: int, dtype: np.dtype = int) -> np.ndarray:  # type: ignore
            [ 1, -1,  1, -1],
            [ 1,  1, -1, -1],
            [ 1, -1, -1,  1]])
+
     """
     if n < 1:
         lg2 = 0
@@ -1244,9 +1215,7 @@ def hadamard(n: int, dtype: np.dtype = int) -> np.ndarray:  # type: ignore
 
 
 def _construct_strength_three_orthogonal_array(num_qubits: int) -> np.ndarray:
-    r"""
-    Given a number of qubits this function returns an Orthogonal Array (OA)
-    on 'n' qubits where n is the next power of two relative to num_qubits.
+    r"""Return an Orthogonal Array (OA) on 'n' qubits where n is the next power of two relative to num_qubits.
 
     Specifically it returns the OA(2n, n, 2, 3).
 
@@ -1274,9 +1243,7 @@ def _construct_strength_three_orthogonal_array(num_qubits: int) -> np.ndarray:
 
 
 def _construct_strength_two_orthogonal_array(num_qubits: int) -> np.ndarray:
-    r"""
-    Given a number of qubits this function returns an Orthogonal Array (OA) on 'n-1' qubits
-    where n-1 is the next integer lambda so that 4*lambda -1 is larger than num_qubits.
+    r"""Return an Orthogonal Array (OA) on 'n-1' qubits where 4*(n-1) - 1 exceeds num_qubits.
 
     Specifically it returns the OA(n, n − 1, 2, 2).
 
@@ -1308,9 +1275,7 @@ def _construct_strength_two_orthogonal_array(num_qubits: int) -> np.ndarray:
 
 
 def _check_min_num_trials_for_symmetrized_readout(num_qubits: int, trials: int, symm_type: int) -> int:
-    """
-    This function sets the minimum number of trials; it is desirable to have hundreds or
-    thousands of trials more than the minimum.
+    """Set the minimum number of trials; it is desirable to have hundreds or thousands of trials more than the minimum.
 
     :param num_qubits: number of qubits to symmetrize
     :param trials: number of trials
diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py
index 02ddcd950..24438c3fc 100644
--- a/pyquil/api/_qvm.py
+++ b/pyquil/api/_qvm.py
@@ -13,16 +13,16 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
+from collections.abc import Iterable, Sequence
 from dataclasses import dataclass
-from typing import Any, Optional, Sequence, Tuple, Dict, List, Iterable
+from typing import Any, Optional
 
 import numpy as np
-
-from qcs_sdk import QCSClient, qvm, ResultData, ExecutionData
-from qcs_sdk.qvm import QVMOptions, QVMResultData, QVMClient
+from qcs_sdk import ExecutionData, QCSClient, ResultData, qvm
+from qcs_sdk.qvm import QVMClient, QVMOptions, QVMResultData
 
 from pyquil._version import pyquil_version
-from pyquil.api import QAM, QuantumExecutable, QAMExecutionResult, MemoryMap
+from pyquil.api import QAM, MemoryMap, QAMExecutionResult, QuantumExecutable
 from pyquil.noise import NoiseModel, apply_noise_model
 from pyquil.quil import Program
 
@@ -36,8 +36,7 @@ class QVMNotRunning(Exception):
 
 
 def check_qvm_version(version: str) -> None:
-    """
-    Verify that there is no mismatch between pyquil and QVM versions.
+    """Verify that there is no mismatch between pyquil and QVM versions.
 
     :param version: The version of the QVM
     """
@@ -54,7 +53,7 @@ class QVMExecuteResponse:
     data: QVMResultData
 
     @property
-    def memory(self) -> Dict[str, np.ndarray]:
+    def memory(self) -> dict[str, np.ndarray]:
         return {key: matrix.as_ndarray() for key, matrix in self.data.memory.items()}
 
 
@@ -62,14 +61,13 @@ class QVM(QAM[QVMExecuteResponse]):
     def __init__(
         self,
         noise_model: Optional[NoiseModel] = None,
-        gate_noise: Optional[Tuple[float, float, float]] = None,
-        measurement_noise: Optional[Tuple[float, float, float]] = None,
+        gate_noise: Optional[tuple[float, float, float]] = None,
+        measurement_noise: Optional[tuple[float, float, float]] = None,
         random_seed: Optional[int] = None,
         timeout: float = 10.0,
         client: Optional[QVMClient] = None,
     ) -> None:
-        """
-        A virtual machine that classically emulates the execution of Quil programs.
+        """Return a virtual machine that classically emulates the execution of Quil programs.
 
         :param noise_model: A noise model that describes noise to apply when emulating a program's
             execution.
@@ -129,11 +127,10 @@ def connect(self) -> None:
 
     def execute_with_memory_map_batch(
         self, executable: QuantumExecutable, memory_maps: Iterable[MemoryMap], **__: Any
-    ) -> List[QVMExecuteResponse]:
-        """
-        Executes a single program on the QVM with multiple memory maps.
+    ) -> list[QVMExecuteResponse]:
+        """Execute a single program on the QVM with multiple memory maps.
 
-        This method is a convenience wrapper around QVM#execute and isn't more efficient than making multiple seperate
+        This method is a convenience wrapper around QVM#execute and isn't more efficient than making multiple separate
         requests to the QVM.
         """
         return [self.execute(executable, memory_map) for memory_map in memory_maps]
@@ -144,9 +141,7 @@ def execute(
         memory_map: Optional[MemoryMap] = None,
         **__: Any,
     ) -> QVMExecuteResponse:
-        """
-        Synchronously execute the input program to completion.
-        """
+        """Execute the input program to completion."""
         if not isinstance(executable, Program):
             raise TypeError(f"`QVM#executable` argument must be a `Program`; got {type(executable)}")
 
@@ -172,28 +167,34 @@ def execute(
         return QVMExecuteResponse(executable=executable, data=result)
 
     def get_result(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult:
-        """
-        Return the results of execution on the QVM.
-        """
-
+        """Return the results of execution on the QVM."""
         result_data = ResultData(execute_response.data)
         data = ExecutionData(result_data=result_data, duration=None)
         return QAMExecutionResult(executable=execute_response.executable, data=data)
 
     def get_version_info(self) -> str:
-        """
-        Return version information for the QVM.
+        """Return version information for the QVM.
 
         :return: String with version information
         """
         return qvm.api.get_version_info(self._client, options=QVMOptions(timeout_seconds=self.timeout))
 
 
-def validate_noise_probabilities(noise_parameter: Optional[Tuple[float, float, float]]) -> None:
-    """
-    Is noise_parameter a valid specification of noise probabilities for depolarizing noise?
+def validate_noise_probabilities(noise_parameter: Optional[tuple[float, float, float]]) -> None:
+    """Validate the noise probabilities.
+
+    This function checks that the provided noise parameters are in the correct format and within the expected ranges.
 
-    :param tuple noise_parameter: Tuple of 3 noise parameter values to be validated.
+    :param noise_parameter: A tuple containing three float values representing noise probabilities. If None, the
+    function returns without any validation.
+
+    :type noise_parameter: Optional[tuple[float, float, float]]
+
+    :raises TypeError: If noise_parameter is not a tuple or if any of its elements are not floats.
+    :raises ValueError: If noise_parameter does not have exactly three elements, if the sum of the elements is not
+        between 0 and 1 (inclusive), or if any element is negative.
+
+    :returns: None
     """
     if not noise_parameter:
         return
@@ -210,8 +211,7 @@ def validate_noise_probabilities(noise_parameter: Optional[Tuple[float, float, f
 
 
 def validate_qubit_list(qubit_list: Sequence[int]) -> Sequence[int]:
-    """
-    Check the validity of qubits for the payload.
+    """Check the validity of qubits for the payload.
 
     :param qubit_list: List of qubits to be validated.
     """
diff --git a/pyquil/api/_rewrite_arithmetic.py b/pyquil/api/_rewrite_arithmetic.py
deleted file mode 100644
index 327d36c82..000000000
--- a/pyquil/api/_rewrite_arithmetic.py
+++ /dev/null
@@ -1,151 +0,0 @@
-from numbers import Real
-import numpy as np
-
-from pyquil.quil import Program
-from pyquil.quilatom import MemoryReference, Expression, Sub, Div
-from pyquil.quilbase import (
-    Declare,
-    Gate,
-    SetFrequency,
-    ShiftFrequency,
-    SetScale,
-    SetPhase,
-    ShiftPhase,
-)
-from rpcq.messages import ParameterSpec, ParameterAref, RewriteArithmeticResponse
-
-from typing import Dict, Union, List, no_type_check
-
-
-# TODO
-# The various reassignments to the variable expr make it difficult
-# to tie the typing down.
-@no_type_check
-def rewrite_arithmetic(prog: Program) -> RewriteArithmeticResponse:
-    """Rewrite compound arithmetic expressions.
-
-    The basic motivation is that a parametric program may have gates with
-    compound arguments which cannot be evaluated natively on the underlying
-    control hardware. The solution provided here is to translate a program like
-
-      DECLARE theta REAL
-      DECLARE beta REAL
-      RZ(3 * theta) 0
-      RZ(beta+theta) 0
-
-    into something like
-
-      DECLARE theta REAL
-      DECLARE beta REAL
-      DECLARE __P REAL[2]
-      RZ(__P[0]) 0
-      RZ(__P[1]) 0
-
-    along with a "recalculation table" mapping new memory references to their
-    corresponding arithmetic expressions,
-
-      {
-        ParameterAref('__P', 0): "((3.0)*theta[0])",
-        ParameterAref('__P', 1): "(beta[0]+theta[0])"
-      }
-
-    When executing the parametric program with specific values for `theta` and
-    `beta`, the PyQuil client will patch in values for `__P` by evaluating the
-    expressions in the recalculation table.
-
-    :param prog: A program.
-    :returns: A RewriteArithmeticResponse, containing the updated program along
-      with its memory descriptors and a recalculation table.
-
-    """
-
-    def spec(inst: Declare) -> ParameterSpec:
-        return ParameterSpec(type=inst.memory_type, length=inst.memory_size)
-
-    def aref(ref: MemoryReference) -> ParameterAref:
-        return ParameterAref(name=ref.name, index=ref.offset)
-
-    updated = prog.copy_everything_except_instructions()
-    old_descriptors = {inst.name: spec(inst) for inst in prog if isinstance(inst, Declare)}
-    recalculation_table: Dict[ParameterAref, str] = {}
-    seen_exprs: Dict[str, MemoryReference] = {}
-
-    # generate a unique name. it's nice to do this in a deterministic fashion
-    # rather than globbing in a UUID
-    suffix = len(old_descriptors)
-    while f"__P{suffix}" in old_descriptors:
-        suffix += 1
-    mref_name = f"__P{suffix}"
-    mref_idx = 0
-
-    def expr_mref(expr: object) -> MemoryReference:
-        """Get a suitable MemoryReference for a given expression."""
-        nonlocal mref_idx
-        expr = str(expr)
-        if expr in seen_exprs:
-            return seen_exprs[expr]
-        new_mref = MemoryReference(mref_name, mref_idx)
-        seen_exprs[expr] = new_mref
-        mref_idx += 1
-        recalculation_table[aref(new_mref)] = expr
-        return new_mref
-
-    for inst in prog:
-        if isinstance(inst, Gate):
-            new_params: List[Union[Real, MemoryReference]] = []
-            for param in inst.params:
-                if isinstance(param, Real):
-                    new_params.append(param)
-                elif isinstance(param, Expression):
-                    # Quil gate angles are in radians,
-                    # but downstream processing expects revolutions
-                    expr = str(Div(param, 2 * np.pi))
-                    new_params.append(expr_mref(expr))
-                else:
-                    raise ValueError(f"Unknown parameter type {type(param)} in {inst}.")
-            updated.inst(Gate(inst.name, new_params, inst.qubits))
-        elif isinstance(inst, (SetFrequency, ShiftFrequency)):
-            if isinstance(inst.freq, Real):
-                updated.inst(inst)
-                continue
-            try:
-                fdefn = prog.frames[inst.frame]
-            except KeyError:
-                raise ValueError(f"Unable to rewrite {inst} without DEFFRAME {inst.frame}.")
-            if fdefn.sample_rate is None:
-                raise ValueError(f"Unable to rewrite {inst} on frame with undefined SAMPLE-RATE.")
-            if fdefn.center_frequency:
-                expr = Sub(inst.freq, fdefn.center_frequency)
-            else:
-                expr = inst.freq
-            expr = Div(expr, fdefn.sample_rate)
-            expr = str(expr)
-            updated.inst(inst.__class__(inst.frame, expr_mref(expr)))
-        elif isinstance(inst, (SetPhase, ShiftPhase)):
-            if isinstance(inst.phase, Real):
-                updated.inst(inst)
-            else:
-                # Quil phases are in radians
-                # but downstream processing expects revolutions
-                expr = str(Div(inst.phase, 2 * np.pi))
-                updated.inst(inst.__class__(inst.frame, expr_mref(expr)))
-        elif isinstance(inst, SetScale):
-            if isinstance(inst.scale, complex) and inst.scale.imag == 0.0:
-                updated.inst(inst)
-            else:
-                # scale is in [-4,4)
-                # binary patching assumes periodic with period 1
-                # so we divide by 8...
-                expr = str(Div(inst.scale, 8))
-                updated.inst(SetScale(inst.frame, expr_mref(expr)))
-        else:
-            updated.inst(inst)
-
-    if mref_idx > 0:
-        updated = Program(Declare(mref_name, "REAL", mref_idx)) + updated
-
-    return RewriteArithmeticResponse(
-        quil=updated.out(),
-        original_memory_descriptors=old_descriptors,
-        recalculation_table=recalculation_table,
-    )
diff --git a/pyquil/api/_wavefunction_simulator.py b/pyquil/api/_wavefunction_simulator.py
index 45419f7aa..6b44937e6 100644
--- a/pyquil/api/_wavefunction_simulator.py
+++ b/pyquil/api/_wavefunction_simulator.py
@@ -13,10 +13,9 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from typing import Dict, List, Union, Optional, Set, cast, Tuple
+from typing import Optional, Union, cast
 
 import numpy as np
-
 from qcs_sdk import QCSClient, qvm
 from qcs_sdk.qvm import QVMOptions
 
@@ -35,14 +34,13 @@ class WavefunctionSimulator:
     def __init__(
         self,
         *,
-        gate_noise: Optional[Tuple[float, float, float]] = None,
-        measurement_noise: Optional[Tuple[float, float, float]] = None,
+        gate_noise: Optional[tuple[float, float, float]] = None,
+        measurement_noise: Optional[tuple[float, float, float]] = None,
         random_seed: Optional[int] = None,
         timeout: float = 10.0,
         client_configuration: Optional[QCSClient] = None,
     ) -> None:
-        """
-        A simulator that propagates a wavefunction representation of a quantum state.
+        """Return a simulator that propagates a wavefunction representation of a quantum state.
 
         :param gate_noise: A tuple of three numbers [Px, Py, Pz] indicating the probability of an X,
             Y, or Z gate getting applied to each qubit after a gate application or reset.
@@ -53,7 +51,6 @@ def __init__(
         :param timeout: Time limit for requests, in seconds.
         :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded.
         """
-
         validate_noise_probabilities(gate_noise)
         validate_noise_probabilities(measurement_noise)
         self.gate_noise = gate_noise
@@ -71,8 +68,7 @@ def __init__(
         self._qvm_client = qvm.QVMClient.new_http(self._client.qvm_url)
 
     def wavefunction(self, quil_program: Program, memory_map: Optional[MemoryMap] = None) -> Wavefunction:
-        """
-        Simulate a Quil program and return the wavefunction.
+        """Simulate a Quil program and return the wavefunction.
 
         .. note:: If your program contains measurements or noisy gates, this method may not do what
             you want. If the execution of ``quil_program`` is **non-deterministic** then the
@@ -84,12 +80,11 @@ def wavefunction(self, quil_program: Program, memory_map: Optional[MemoryMap] =
         :param memory_map: An assignment of classical registers to values, representing an initial
                            state for the QAM's classical memory.
 
-                           This is expected to be of type Dict[str, List[Union[int, float]]],
+                           This is expected to be of type dict[str, list[Union[int, float]]],
                            where the keys are memory region names and the values are arrays of
                            initialization data.
         :return: A Wavefunction object representing the state of the QVM.
         """
-
         if memory_map is not None:
             quil_program = self.augment_program_with_memory_values(quil_program, memory_map)
 
@@ -107,11 +102,10 @@ def wavefunction(self, quil_program: Program, memory_map: Optional[MemoryMap] =
     def expectation(
         self,
         prep_prog: Program,
-        pauli_terms: Union[PauliSum, List[PauliTerm]],
-        memory_map: Optional[Dict[str, List[Union[int, float]]]] = None,
+        pauli_terms: Union[PauliSum, list[PauliTerm]],
+        memory_map: Optional[dict[str, list[Union[int, float]]]] = None,
     ) -> Union[float, np.ndarray]:
-        """
-        Calculate the expectation value of Pauli operators given a state prepared by prep_program.
+        """Calculate the expectation value of Pauli operators given a state prepared by prep_program.
 
         If ``pauli_terms`` is a ``PauliSum`` then the returned value is a single ``float``,
         otherwise the returned value is an array of values, one for each ``PauliTerm`` in the
@@ -128,12 +122,11 @@ def expectation(
         :param memory_map: An assignment of classical registers to values, representing an initial
                            state for the QAM's classical memory.
 
-                           This is expected to be of type Dict[str, List[Union[int, float]]],
+                           This is expected to be of type dict[str, list[Union[int, float]]],
                            where the keys are memory region names and the values are arrays of
                            initialization data.
         :return: Either a float or array floats depending on ``pauli_terms``.
         """
-
         is_pauli_sum = False
         if isinstance(pauli_terms, PauliSum):
             progs, coeffs = pauli_terms.get_programs()
@@ -158,12 +151,11 @@ def expectation(
     def run_and_measure(
         self,
         quil_program: Program,
-        qubits: Optional[List[int]] = None,
+        qubits: Optional[list[int]] = None,
         trials: int = 1,
         memory_map: Optional[MemoryMap] = None,
     ) -> np.ndarray:
-        """
-        Run a Quil program once to determine the final wavefunction, and measure multiple times.
+        """Run a Quil program once to determine the final wavefunction, and measure multiple times.
 
         Alternatively, consider using ``wavefunction`` and calling ``sample_bitstrings`` on the
         resulting object.
@@ -187,13 +179,13 @@ def run_and_measure(
         :param memory_map: An assignment of classical registers to values, representing an initial
                            state for the QAM's classical memory.
 
-                           This is expected to be of type Dict[str, List[Union[int, float]]],
+                           This is expected to be of type dict[str, list[Union[int, float]]],
                            where the keys are memory region names and the values are arrays of
                            initialization data.
         :return: An array of measurement results (0 or 1) of shape (trials, len(qubits))
         """
         if qubits is None:
-            qubits = sorted(cast(Set[int], quil_program.get_qubits(indices=True)))
+            qubits = sorted(cast(set[int], quil_program.get_qubits(indices=True)))
 
         if memory_map is not None:
             quil_program = self.augment_program_with_memory_values(quil_program, memory_map)
@@ -215,8 +207,8 @@ def augment_program_with_memory_values(
     ) -> Program:
         p = Program()
 
-        # we stupidly allowed memory_map to be of type Dict[MemoryReference, Any], whereas qc.run
-        # takes a memory initialization argument of type Dict[str, List[Union[int, float]]. until
+        # we stupidly allowed memory_map to be of type dict[MemoryReference, Any], whereas qc.run
+        # takes a memory initialization argument of type dict[str, list[Union[int, float]]. until
         # we are in a position to remove this, we support both styles of input.
 
         if len(memory_map.keys()) == 0:
@@ -226,7 +218,7 @@ def augment_program_with_memory_values(
                 for index, value in enumerate(arr):
                     p += MOVE(MemoryReference(name, offset=index), value)
         else:
-            raise TypeError("Bad memory_map type; expected Dict[str, List[Union[int, float]]].")
+            raise TypeError("Bad memory_map type; expected dict[str, list[Union[int, float]]].")
 
         p += quil_program
 
diff --git a/pyquil/control_flow_graph.py b/pyquil/control_flow_graph.py
index ee0a46dfa..2e14419f4 100644
--- a/pyquil/control_flow_graph.py
+++ b/pyquil/control_flow_graph.py
@@ -1,14 +1,15 @@
-from typing import List, Optional
-from typing_extensions import Self, override
+"""Classes that represent the control flow graph of a Quil program."""
+
+from typing import Optional
 
 from quil import program as quil_rs
+from typing_extensions import Self, override
 
-from pyquil.quilbase import _convert_to_py_instruction, _convert_to_py_instructions, AbstractInstruction
+from pyquil.quilbase import AbstractInstruction, _convert_to_py_instruction, _convert_to_py_instructions
 
 
 class BasicBlock(quil_rs.BasicBlock):
-    """
-    Represents a basic block in the Program.
+    """Represents a basic block in the Program.
 
     Most functionality is implemented by the `quil` package. See the
     `quil BasicBlock documentation`_ for documentation and available methods.
@@ -21,7 +22,7 @@ def _from_rs(cls, block: quil_rs.BasicBlock) -> Self:
         return super().__new__(cls, block)
 
     @override
-    def instructions(self) -> List[AbstractInstruction]:  # type: ignore[override]
+    def instructions(self) -> list[AbstractInstruction]:  # type: ignore[override]
         return _convert_to_py_instructions(super().instructions())
 
     @override
@@ -33,8 +34,7 @@ def terminator(self) -> Optional[AbstractInstruction]:  # type: ignore[override]
 
 
 class ControlFlowGraph(quil_rs.ControlFlowGraph):
-    """
-    Representation of a control flow graph (CFG) for a Quil program.
+    """Representation of a control flow graph (CFG) for a Quil program.
 
     The CFG is a directed graph where each node is a basic block and each edge is a control flow transition between two
     basic blocks.
@@ -53,8 +53,6 @@ def _from_rs(cls, graph: quil_rs.ControlFlowGraph) -> Self:
         return super().__new__(cls, graph)
 
     @override
-    def basic_blocks(self) -> List[BasicBlock]:  # type: ignore[override]
-        """
-        Return a list of all the basic blocks in the control flow graph, in order of definition.
-        """
+    def basic_blocks(self) -> list[BasicBlock]:  # type: ignore[override]
+        """Return a list of all the basic blocks in the control flow graph, in order of definition."""
         return [BasicBlock._from_rs(block) for block in super().basic_blocks()]
diff --git a/pyquil/diagnostics.py b/pyquil/diagnostics.py
index 991380c8f..4a10ab07e 100644
--- a/pyquil/diagnostics.py
+++ b/pyquil/diagnostics.py
@@ -1,11 +1,12 @@
-import pyquil
+"""Diagnostic utilities for pyQuil and QCS."""
+
 from qcs_sdk import diagnostics
 
+import pyquil
+
 
 def get_report() -> str:
-    """
-    Get a report of the current state of the pyQuil installation, Python environment,
-    and supporting packages.
+    """Get a report of the current state of the pyQuil installation, Python environment, and supporting packages.
 
     Note: this format is not stable and its content may change between versions.
     """
diff --git a/pyquil/experiment/__init__.py b/pyquil/experiment/__init__.py
index 2724a787a..f98a4e7db 100644
--- a/pyquil/experiment/__init__.py
+++ b/pyquil/experiment/__init__.py
@@ -1,3 +1,5 @@
+"""Classes and functions to facilitate running experiments on a quantum computer."""
+
 __all__ = [
     "bitstrings_to_expectations",
     "CalibrationMethod",
@@ -30,9 +32,9 @@
 
 from pyquil.experiment._calibration import CalibrationMethod
 from pyquil.experiment._group import (
-    merge_disjoint_experiments,
     get_results_by_qubit_groups,
     group_settings,
+    merge_disjoint_experiments,
 )
 from pyquil.experiment._main import (
     Experiment,
@@ -48,12 +50,11 @@
     ratio_variance,
 )
 from pyquil.experiment._setting import (
-    _OneQState,
-    ExperimentSetting,
     SIC0,
     SIC1,
     SIC2,
     SIC3,
+    ExperimentSetting,
     TensorProductState,
     minusX,
     minusY,
diff --git a/pyquil/experiment/_group.py b/pyquil/experiment/_group.py
index ea2008c34..0ece4766e 100644
--- a/pyquil/experiment/_group.py
+++ b/pyquil/experiment/_group.py
@@ -15,8 +15,9 @@
 ##############################################################################
 import functools
 import itertools
+from collections.abc import Iterable, Sequence
 from operator import mul
-from typing import Dict, List, Iterable, Sequence, Set, Tuple, Union, cast
+from typing import Union, cast
 
 import networkx as nx
 from networkx.algorithms.approximation.clique import clique_removal
@@ -31,9 +32,8 @@
 
 def get_results_by_qubit_groups(
     results: Iterable[ExperimentResult], qubit_groups: Sequence[Sequence[int]]
-) -> Dict[Tuple[int, ...], List[ExperimentResult]]:
-    """
-    Organizes ExperimentResults by the group of qubits on which the observable of the result acts.
+) -> dict[tuple[int, ...], list[ExperimentResult]]:
+    """Organizes ExperimentResults by the group of qubits on which the observable of the result acts.
 
     Each experiment result will be associated with a qubit group key if the observable of the
     result.setting acts on a subset of the qubits in the group. If the result does not act on a
@@ -49,7 +49,7 @@ def get_results_by_qubit_groups(
         subset of that qubit group. The result order is maintained within each group.
     """
     tuple_groups = [tuple(sorted(group)) for group in qubit_groups]
-    results_by_qubit_group: Dict[Tuple[int, ...], List[ExperimentResult]] = {group: [] for group in tuple_groups}
+    results_by_qubit_group: dict[tuple[int, ...], list[ExperimentResult]] = {group: [] for group in tuple_groups}
     for res in results:
         res_qs = res.setting.out_operator.get_qubits()
 
@@ -60,10 +60,8 @@ def get_results_by_qubit_groups(
     return results_by_qubit_group
 
 
-def merge_disjoint_experiments(experiments: List[Experiment], group_merged_settings: bool = True) -> Experiment:
-    """
-    Merges the list of experiments into a single experiment that runs the sum of the individual
-    experiment programs and contains all of the combined experiment settings.
+def merge_disjoint_experiments(experiments: list[Experiment], group_merged_settings: bool = True) -> Experiment:
+    """Merge the experiments into one that runs all individual programs and includes all combined settings.
 
     A group of Experiments whose programs operate on disjoint sets of qubits can be
     'parallelized' so that the total number of runs can be reduced after grouping the settings.
@@ -90,13 +88,13 @@ def merge_disjoint_experiments(experiments: List[Experiment], group_merged_setti
     :param group_merged_settings: By default group the settings of the merged experiment.
     :return: a single experiment that runs the summed program and all settings.
     """
-    used_qubits: Set[int] = set()
+    used_qubits: set[int] = set()
     for expt in experiments:
         if expt.program.get_qubits().intersection(used_qubits):
             raise ValueError(
                 "Experiment programs act on some shared set of qubits and cannot be " "merged unambiguously."
             )
-        used_qubits = used_qubits.union(cast(Set[int], expt.program.get_qubits()))
+        used_qubits = used_qubits.union(cast(set[int], expt.program.get_qubits()))
 
     # get a flat list of all settings, to be regrouped later
     all_settings = [setting for expt in experiments for simult_settings in expt for setting in simult_settings]
@@ -116,12 +114,12 @@ def merge_disjoint_experiments(experiments: List[Experiment], group_merged_setti
 
 
 def construct_tpb_graph(experiments: Experiment) -> nx.Graph:
-    """
-    Construct a graph where an edge signifies two experiments are diagonal in a TPB.
-    """
+    """Construct a graph where an edge signifies two experiments are diagonal in a TPB."""
     g = nx.Graph()
     for expt in experiments:
-        assert len(expt) == 1, "already grouped?"
+        if len(expt) != 1:
+            raise ValueError("There must be a single set of ExperimentSettings for each Experiment.")
+
         unpacked_expt = expt[0]
 
         if unpacked_expt not in g:
@@ -145,9 +143,9 @@ def construct_tpb_graph(experiments: Experiment) -> nx.Graph:
 
 
 def group_settings_clique_removal(experiments: Experiment) -> Experiment:
-    """
-    Group experiments that are diagonal in a shared tensor product basis (TPB) to minimize number
-    of QPU runs, using a graph clique removal algorithm.
+    """Group experiments that are diagonal in a shared tensor product basis (TPB) to minimize number of QPU runs.
+
+    This function uses a graph clique removal algorithm.
 
     :param experiments: a tomography experiment
     :return: a tomography experiment with all the same settings, just grouped according to shared
@@ -155,9 +153,9 @@ def group_settings_clique_removal(experiments: Experiment) -> Experiment:
     """
     g = construct_tpb_graph(experiments)
     _, cliqs = clique_removal(g)
-    new_cliqs: List[List[ExperimentSetting]] = []
+    new_cliqs: list[list[ExperimentSetting]] = []
     for cliq in cliqs:
-        new_cliq: List[ExperimentSetting] = []
+        new_cliq: list[ExperimentSetting] = []
         for expt in cliq:
             # duplicate `count` times
             new_cliq += [expt] * g.nodes[expt]["count"]
@@ -172,19 +170,18 @@ def group_settings_clique_removal(experiments: Experiment) -> Experiment:
 
 
 def _max_weight_operator(ops: Iterable[PauliTerm]) -> Union[None, PauliTerm]:
-    """Construct a PauliTerm operator by taking the non-identity single-qubit operator at each
-    qubit position.
+    """Construct a PauliTerm operator by taking the non-identity single-qubit operator at each qubit position.
 
-    This function will return ``None`` if the input operators do not share a natural tensor
-    product basis.
+    This function will return ``None`` if the input operators do not share a natural tensor product basis.
 
     For example, the max_weight_operator of ["XI", "IZ"] is "XZ". Asking for the max weight
     operator of something like ["XI", "ZI"] will return None.
     """
-    mapping = dict()  # type: Dict[int, str]
+    mapping: dict[int, str] = dict()
     for op in ops:
         for idx, op_str in op:
-            assert isinstance(idx, int)
+            if not isinstance(idx, int):
+                raise ValueError(f"Expected index to be int but got {type(idx)}")
             if idx in mapping:
                 if mapping[idx] != op_str:
                     return None
@@ -195,15 +192,14 @@ def _max_weight_operator(ops: Iterable[PauliTerm]) -> Union[None, PauliTerm]:
 
 
 def _max_weight_state(states: Iterable[TensorProductState]) -> Union[None, TensorProductState]:
-    """Construct a TensorProductState by taking the single-qubit state at each
-    qubit position.
+    """Construct a TensorProductState by taking the single-qubit state at each qubit position.
 
     This function will return ``None`` if the input states are not compatible
 
     For example, the max_weight_state of ["(+X, q0)", "(-Z, q1)"] is "(+X, q0; -Z q1)". Asking for
     the max weight state of something like ["(+X, q0)", "(+Z, q0)"] will return None.
     """
-    mapping = dict()  # type: Dict[int, _OneQState]
+    mapping: dict[int, _OneQState] = dict()
     for state in states:
         for oneq_state in state.states:
             if oneq_state.qubit in mapping:
@@ -216,10 +212,8 @@ def _max_weight_state(states: Iterable[TensorProductState]) -> Union[None, Tenso
 
 def _max_tpb_overlap(
     tomo_expt: Experiment,
-) -> Dict[ExperimentSetting, List[ExperimentSetting]]:
-    """
-    Given an input Experiment, provide a dictionary indicating which ExperimentSettings
-    share a tensor product basis
+) -> dict[ExperimentSetting, list[ExperimentSetting]]:
+    """Given an input Experiment, provide a dictionary indicating which ExperimentSettings share a tensor product basis.
 
     :param tomo_expt: Experiment, from which to group ExperimentSettings that share a tpb
         and can be run together
@@ -227,11 +221,13 @@ def _max_tpb_overlap(
             list of ExperimentSettings (diagonal in that tpb)
     """
     # initialize empty dictionary
-    diagonal_sets: Dict[ExperimentSetting, List[ExperimentSetting]] = {}
+    diagonal_sets: dict[ExperimentSetting, list[ExperimentSetting]] = {}
     # loop through ExperimentSettings of the Experiment
     for expt_setting in tomo_expt:
         # no need to group already grouped Experiment
-        assert len(expt_setting) == 1, "already grouped?"
+        if len(expt_setting) != 1:
+            raise ValueError("ExperimentSettings should not be grouped before calling this function.")
+
         unpacked_expt_setting = expt_setting[0]
         # calculate max overlap of expt_setting with keys of diagonal_sets
         # keep track of whether a shared tpb was found
@@ -245,12 +241,10 @@ def _max_tpb_overlap(
             # conditional is True if expt_setting can be inserted into the current es_list.
             if diag_in_term is not None and diag_out_term is not None:
                 found_tpb = True
-                assert len(diag_in_term) >= len(
-                    es.in_state
-                ), "Highest weight in-state can't be smaller than the given in-state"
-                assert len(diag_out_term) >= len(
-                    es.out_operator
-                ), "Highest weight out-PauliTerm can't be smaller than the given out-PauliTerm"
+                if len(diag_in_term) < len(es.in_state):
+                    raise ValueError("Highest weight in-state can't be smaller than the given in-state")
+                if len(diag_out_term) < len(es.out_operator):
+                    raise ValueError("Highest weight out-PauliTerm can't be smaller than the given out-PauliTerm")
 
                 # update the diagonalizing basis (key of dict) if necessary
                 if len(diag_in_term) > len(es.in_state) or len(diag_out_term) > len(es.out_operator):
@@ -270,8 +264,7 @@ def _max_tpb_overlap(
 
 
 def group_settings_greedy(tomo_expt: Experiment) -> Experiment:
-    """
-    Greedy method to group ExperimentSettings in a given Experiment
+    """Greedy method to group ExperimentSettings in a given Experiment.
 
     :param tomo_expt: Experiment to group ExperimentSettings within
     :return: Experiment, with grouped ExperimentSettings according to whether
@@ -288,9 +281,7 @@ def group_settings_greedy(tomo_expt: Experiment) -> Experiment:
 
 
 def group_settings(experiments: Experiment, method: str = "greedy") -> Experiment:
-    """
-    Group experiments that are diagonal in a shared tensor product basis (TPB) to minimize number
-    of QPU runs.
+    """Group experiments that are diagonal in a shared tensor product basis (TPB) to minimize number of QPU runs.
 
     .. rubric:: Background
 
diff --git a/pyquil/experiment/_main.py b/pyquil/experiment/_main.py
index 3c88426ba..21ff055df 100644
--- a/pyquil/experiment/_main.py
+++ b/pyquil/experiment/_main.py
@@ -13,26 +13,21 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
+"""Schema definition of a Experiment, a collection of ExperimentSetting objects and a main program body (or ansatz).
+
+This schema is widely useful for defining and executing many common types of algorithms / applications, including state
+and process tomography, and the variational quantum eigensolver.
 """
-Schema definition of a Experiment, which is a collection of ExperimentSetting
-objects and a main program body (or ansatz). This schema is widely useful for defining
-and executing many common types of algorithms / applications, including state and process
-tomography, and the variational quantum eigensolver.
-"""
+
 import json
 import logging
 import warnings
+from collections.abc import Generator, Mapping, Sequence
 from json import JSONEncoder
 from typing import (
     Any,
     Callable,
-    Dict,
-    Generator,
-    List,
-    Mapping,
     Optional,
-    Sequence,
-    Set,
     Union,
     cast,
 )
@@ -43,20 +38,19 @@
     pauli_term_to_preparation_memory_map,
 )
 from pyquil.experiment._program import (
+    measure_qubits,
+    parameterized_readout_symmetrization,
     parameterized_single_qubit_measurement_basis,
     parameterized_single_qubit_state_preparation,
-    parameterized_readout_symmetrization,
-    measure_qubits,
 )
 from pyquil.experiment._result import ExperimentResult
-from pyquil.experiment._setting import ExperimentSetting, _OneQState, TensorProductState
+from pyquil.experiment._setting import ExperimentSetting, TensorProductState, _OneQState
 from pyquil.experiment._symmetrization import SymmetrizationLevel
 from pyquil.gates import RESET
 from pyquil.paulis import PauliTerm, is_identity
 from pyquil.quil import Program
 from pyquil.quilbase import Reset, ResetQubit
 
-
 log = logging.getLogger(__name__)
 
 
@@ -77,8 +71,7 @@ def _abbrev_program(program: Program, max_len: int = 10) -> str:
 
 
 def _remove_reset_from_program(program: Program) -> Program:
-    """
-    Trim the RESET from a program because in measure_observables it is re-added.
+    """Trim the RESET from a program because in measure_observables it is re-added.
 
     :param program: Program to remove RESET(s) from.
     :return: Trimmed Program.
@@ -93,8 +86,7 @@ def _remove_reset_from_program(program: Program) -> Program:
 
 
 class Experiment:
-    """
-    A tomography-like experiment.
+    """A tomography-like experiment.
 
     Many near-term quantum algorithms involve:
 
@@ -144,29 +136,30 @@ class Experiment:
 
     def __init__(
         self,
-        settings: Union[List[ExperimentSetting], List[List[ExperimentSetting]]],
+        settings: Union[list[ExperimentSetting], list[list[ExperimentSetting]]],
         program: Program,
         *,
         symmetrization: int = SymmetrizationLevel.EXHAUSTIVE,
         calibration: int = CalibrationMethod.PLUS_EIGENSTATE,
     ):
         if len(settings) == 0:
-            s: List[List[ExperimentSetting]] = []
+            s: list[list[ExperimentSetting]] = []
         else:
             if isinstance(settings[0], ExperimentSetting):
                 # convenience wrapping in lists of length 1
-                s = [[expt] for expt in cast(List[ExperimentSetting], settings)]
+                s = [[expt] for expt in cast(list[ExperimentSetting], settings)]
             else:
-                s = cast(List[List[ExperimentSetting]], settings)
+                s = cast(list[list[ExperimentSetting]], settings)
 
         self._settings = s
         self.program = program
         self.symmetrization = SymmetrizationLevel(symmetrization)
         if self.symmetrization != SymmetrizationLevel.EXHAUSTIVE:
-            if type(calibration) == int and calibration != 0:
+            if isinstance(calibration, int) and calibration != 0:
                 warnings.warn(
                     "Calibration is only supported for exhaustive symmetrization, "
-                    "thus setting self.calibration = 0 (CalibrationMethod.NONE)."
+                    "thus setting self.calibration = 0 (CalibrationMethod.NONE).",
+                    stacklevel=2,
                 )
             self.calibration = CalibrationMethod.NONE
         else:
@@ -182,51 +175,51 @@ def __init__(
     def __len__(self) -> int:
         return len(self._settings)
 
-    def __getitem__(self, item: int) -> List[ExperimentSetting]:
+    def __getitem__(self, item: int) -> list[ExperimentSetting]:
         return self._settings[item]
 
-    def __setitem__(self, key: int, value: List[ExperimentSetting]) -> None:
+    def __setitem__(self, key: int, value: list[ExperimentSetting]) -> None:
         self._settings[key] = value
 
     def __delitem__(self, key: int) -> None:
         self._settings.__delitem__(key)
 
-    def __iter__(self) -> Generator[List[ExperimentSetting], None, None]:
+    def __iter__(self) -> Generator[list[ExperimentSetting], None, None]:
         yield from self._settings
 
-    def __reversed__(self) -> Generator[List[ExperimentSetting], None, None]:
+    def __reversed__(self) -> Generator[list[ExperimentSetting], None, None]:
         yield from reversed(self._settings)
 
-    def __contains__(self, item: List[ExperimentSetting]) -> bool:
+    def __contains__(self, item: list[ExperimentSetting]) -> bool:
         return item in self._settings
 
-    def append(self, expts: Union[ExperimentSetting, List[ExperimentSetting]]) -> None:
+    def append(self, expts: Union[ExperimentSetting, list[ExperimentSetting]]) -> None:
         if not isinstance(expts, list):
             expts = [expts]
         self._settings.append(expts)
 
-    def count(self, expt: List[ExperimentSetting]) -> int:
+    def count(self, expt: list[ExperimentSetting]) -> int:
         return self._settings.count(expt)
 
-    def index(self, expt: List[ExperimentSetting], start: int = 0, stop: int = 0) -> int:
+    def index(self, expt: list[ExperimentSetting], start: int = 0, stop: int = 0) -> int:
         return self._settings.index(expt, start, stop)
 
-    def extend(self, expts: List[List[ExperimentSetting]]) -> None:
+    def extend(self, expts: list[list[ExperimentSetting]]) -> None:
         self._settings.extend(expts)
 
-    def insert(self, index: int, expt: List[ExperimentSetting]) -> None:
+    def insert(self, index: int, expt: list[ExperimentSetting]) -> None:
         self._settings.insert(index, expt)
 
-    def pop(self, index: int = 0) -> List[ExperimentSetting]:
+    def pop(self, index: int = 0) -> list[ExperimentSetting]:
         return self._settings.pop(index)
 
-    def remove(self, expt: List[ExperimentSetting]) -> None:
+    def remove(self, expt: list[ExperimentSetting]) -> None:
         self._settings.remove(expt)
 
     def reverse(self) -> None:
         self._settings.reverse()
 
-    def sort(self, key: Optional[Callable[[List[ExperimentSetting]], Any]] = None, reverse: bool = False) -> None:
+    def sort(self, key: Optional[Callable[[list[ExperimentSetting]], Any]] = None, reverse: bool = False) -> None:
         return self._settings.sort(key=key, reverse=reverse)
 
     def setting_strings(self) -> Generator[str, None, None]:
@@ -256,7 +249,7 @@ def __repr__(self) -> str:
         string += f"settings:\n{self.settings_string(abbrev_after=20)}"
         return string
 
-    def serializable(self) -> Dict[str, Any]:
+    def serializable(self) -> dict[str, Any]:
         return {
             "type": "Experiment",
             "settings": self._settings,
@@ -271,20 +264,18 @@ def __eq__(self, other: object) -> bool:
             return False
         return self.serializable() == other.serializable()
 
-    def get_meas_qubits(self) -> List[int]:
-        """
-        Return the sorted list of qubits that are involved in the all the out_operators of the
-        settings for this ``Experiment`` object.
-        """
-        meas_qubits: Set[int] = set()
+    def get_meas_qubits(self) -> list[int]:
+        """Return the sorted list of qubits involved in all out_operators of this Experiment object's settings."""
+        meas_qubits: set[int] = set()
         for settings in self:
-            assert len(settings) == 1
-            meas_qubits.update(cast(List[int], settings[0].out_operator.get_qubits()))
+            if len(settings) != 1:
+                raise ValueError("Can't get the measured qubits of grouped ExperimentSettings.")
+            meas_qubits.update(cast(list[int], settings[0].out_operator.get_qubits()))
         return sorted(meas_qubits)
 
-    def get_meas_registers(self, qubits: Optional[Sequence[int]] = None) -> List[int]:
-        """
-        Return the sorted list of memory registers corresponding to the list of qubits provided.
+    def get_meas_registers(self, qubits: Optional[Sequence[int]] = None) -> list[int]:
+        """Return the sorted list of memory registers corresponding to the list of qubits provided.
+
         If no qubits are provided, just returns the list of numbers from 0 to n-1 where n is the
         number of qubits resulting from the ``get_meas_qubits`` method.
         """
@@ -299,10 +290,7 @@ def get_meas_registers(self, qubits: Optional[Sequence[int]] = None) -> List[int
         return sorted(meas_registers)
 
     def generate_experiment_program(self) -> Program:
-        """
-        Generate a parameterized program containing the main body program along with some additions
-        to support the various state preparation, measurement, and symmetrization specifications of
-        this ``Experiment``.
+        """Generate a parameterized program with the main body and additions for state prep, measurement, and symmetrization.
 
         State preparation and measurement are achieved via ZXZXZ-decomposed single-qubit gates,
         where the angles of each ``RZ`` rotation are declared parameters that can be assigned at
@@ -324,7 +312,8 @@ def generate_experiment_program(self) -> Program:
             p += RESET()
 
         for settings in self:
-            assert len(settings) == 1
+            if len(settings) != 1:
+                raise ValueError("Can't generate a program for grouped ExperimentSettings.")
             if ("X" in str(settings[0].in_state)) or ("Y" in str(settings[0].in_state)):
                 if "DECLARE preparation_alpha" in self.program.out():
                     raise ValueError('Memory "preparation_alpha" has been declared already.')
@@ -338,7 +327,8 @@ def generate_experiment_program(self) -> Program:
         p += self.program
 
         for settings in self:
-            assert len(settings) == 1
+            if len(settings) != 1:
+                raise ValueError("Can't generate a program for grouped ExperimentSettings.")
             if ("X" in str(settings[0].out_operator)) or ("Y" in str(settings[0].out_operator)):
                 if "DECLARE measurement_alpha" in self.program.out():
                     raise ValueError('Memory "measurement_alpha" has been declared already.')
@@ -362,11 +352,11 @@ def generate_experiment_program(self) -> Program:
 
         return p
 
-    def build_setting_memory_map(self, setting: ExperimentSetting) -> Dict[str, List[float]]:
-        """
-        Build the memory map corresponding to the state preparation and measurement specifications
-        encoded in the provided ``ExperimentSetting``, taking into account the full set of qubits
-        that are present in the ``Experiment`` object.
+    def build_setting_memory_map(self, setting: ExperimentSetting) -> dict[str, list[float]]:
+        """Build the memory map for state prep and measurement specs in the ExperimentSetting, considering all qubits.
+
+        The memory map is built corresponding to the state preparation and measurement specifications encoded in the
+        provided ExperimentSetting, taking into account the full set of qubits present in the Experiment object.
 
         :return: Memory map for state prep and measurement.
         """
@@ -382,10 +372,10 @@ def build_setting_memory_map(self, setting: ExperimentSetting) -> Dict[str, List
 
     def build_symmetrization_memory_maps(
         self, qubits: Sequence[int], label: str = "symmetrization"
-    ) -> List[Dict[str, List[float]]]:
-        """
-        Build a list of memory maps to be used in a program that is trying to perform readout
-        symmetrization via parametric compilation. For example, if we have the following program:
+    ) -> list[dict[str, list[float]]]:
+        """Build a list of memory maps for readout symmetrization in a program using parametric compilation.
+
+        For example, if we have the following program:
 
             RX(symmetrization[0]) 0
             RX(symmetrization[1]) 1
@@ -414,9 +404,10 @@ def build_symmetrization_memory_maps(
         if self.symmetrization != SymmetrizationLevel.EXHAUSTIVE:
             raise ValueError("We only support exhaustive symmetrization for now.")
 
-        import numpy as np
         import itertools
 
+        import numpy as np
+
         assignments = itertools.product(np.array([0, np.pi]), repeat=len(symm_registers))
         memory_maps = []
         for a in assignments:
@@ -427,25 +418,24 @@ def build_symmetrization_memory_maps(
         return memory_maps
 
     def generate_calibration_experiment(self) -> "Experiment":
-        """
-        Generate another ``Experiment`` object that can be used to calibrate the various multi-qubit
-        observables involved in this ``Experiment``. This is achieved by preparing the plus-one
-        (minus-one) eigenstate of each ``out_operator``, and measuring the resulting expectation
-        value of the same ``out_operator``. Ideally, this would always give +1 (-1), but when
-        symmetric readout error is present the effect is to scale the resultant expectations by some
-        constant factor. Determining this scale factor is what we call *readout calibration*, and
-        then the readout error in subsequent measurements can then be mitigated by simply dividing
-        by the scale factor.
-
-        :return: A new ``Experiment`` that can calibrate the readout error of all the
-            observables involved in this experiment.
+        """Generate an Experiment for calibrating multi-qubit observables to mitigate readout errors.
+
+        Generate another Experiment object to calibrate the various multi-qubit observables involved in this Experiment.
+        This is done by preparing the plus-one (minus-one) eigenstate of each out_operator and measuring the resulting
+        expectation value of the same out_operator. Ideally, this should always yield +1 (-1), but the presence of
+        symmetric readout error scales the results by a constant factor. Determining this scale factor is known as
+        readout calibration, allowing for error mitigation in subsequent measurements by dividing by the scale factor.
+
+        :return: A new ``Experiment`` that can calibrate the readout error of all the observables involved in
+            this experiment.
         """
         if self.calibration != CalibrationMethod.PLUS_EIGENSTATE:
             raise ValueError('We currently only support the "plus eigenstate" calibration method.')
 
         calibration_settings = []
         for settings in self:
-            assert len(settings) == 1
+            if len(settings) != 1:
+                raise ValueError("Can't generate calibration experiment with grouped ExperimentSettings.")
             calibration_settings.append(
                 ExperimentSetting(
                     in_state=_pauli_to_product_state(settings[0].out_operator),
@@ -471,9 +461,7 @@ def generate_calibration_experiment(self) -> "Experiment":
 
 
 def _pauli_to_product_state(in_state: PauliTerm) -> TensorProductState:
-    """
-    Convert a Pauli term to a TensorProductState.
-    """
+    """Convert a Pauli term to a TensorProductState."""
     if is_identity(in_state):
         return TensorProductState()
     else:
@@ -497,8 +485,7 @@ def default(self, o: Any) -> Any:
 
 
 def to_json(fn: str, obj: Any) -> str:
-    """
-    Convenience method to save pyquil.experiment objects as a JSON file.
+    """Save pyquil.experiment objects as a JSON file.
 
     See :py:func:`read_json`.
     """
@@ -521,8 +508,7 @@ def _operator_object_hook(obj: Mapping[str, Any]) -> Union[Mapping[str, Any], Ex
 
 
 def read_json(fn: str) -> Any:
-    """
-    Convenience method to read pyquil.experiment objects from a JSON file.
+    """Read pyquil.experiment objects from a JSON file.
 
     See :py:func:`to_json`.
     """
diff --git a/pyquil/experiment/_memory.py b/pyquil/experiment/_memory.py
index 79bfc47a1..5a6ec5d20 100644
--- a/pyquil/experiment/_memory.py
+++ b/pyquil/experiment/_memory.py
@@ -15,16 +15,15 @@
 ##############################################################################
 
 import itertools
-from typing import Dict, List, Tuple, cast
+from typing import cast
 
 import numpy as np
 
 from pyquil.paulis import PauliTerm
 
 
-def euler_angles_RX(theta: float) -> Tuple[float, float, float]:
-    """
-    A tuple of angles which corresponds to a ZXZXZ-decomposed ``RX`` gate.
+def euler_angles_RX(theta: float) -> tuple[float, float, float]:
+    """Tuple of angles which corresponds to a ZXZXZ-decomposed ``RX`` gate.
 
     :param theta: The angle parameter for the ``RX`` gate.
     :return: The corresponding Euler angles for that gate.
@@ -32,9 +31,8 @@ def euler_angles_RX(theta: float) -> Tuple[float, float, float]:
     return (np.pi / 2, theta, -np.pi / 2)
 
 
-def euler_angles_RY(theta: float) -> Tuple[float, float, float]:
-    """
-    A tuple of angles which corresponds to a ZXZXZ-decomposed ``RY`` gate.
+def euler_angles_RY(theta: float) -> tuple[float, float, float]:
+    """Tuple of angles which corresponds to a ZXZXZ-decomposed ``RY`` gate.
 
     :param theta: The angle parameter for the ``RY`` gate.
     :return: The corresponding Euler angles for that gate.
@@ -58,21 +56,18 @@ def pauli_term_to_euler_memory_map(
     term: PauliTerm,
     *,
     prefix: str,
-    tuple_x: Tuple[float, float, float],
-    tuple_y: Tuple[float, float, float],
-    tuple_z: Tuple[float, float, float],
+    tuple_x: tuple[float, float, float],
+    tuple_y: tuple[float, float, float],
+    tuple_z: tuple[float, float, float],
     suffix_alpha: str = "alpha",
     suffix_beta: str = "beta",
     suffix_gamma: str = "gamma",
-) -> Dict[str, List[float]]:
-    """
-    Given a ``PauliTerm``, create a memory map corresponding to a collection of ZXZXZ-decomposed
-    single-qubit gates. The intent is that these gate are used to prepare an eigenstate of the
-    ``PauliTerm`` or measure in the eigenbasis of the ``PauliTerm``, which is more clearly
-    discernible from the calling functions ``pauli_term_to_preparation_memory_map`` (for state
-    preparation) and ``pauli_term_to_measurement_memory_map`` (for measuring in different bases).
-    This function is not really meant to be used by itself, but rather by the aforementioned
-    calling functions.
+) -> dict[str, list[float]]:
+    """Create a memory map for a `PauliTerm` using ZXZXZ-decomposed single-qubit gates.
+
+    This memory map is intended for use in preparing an eigenstate of the `PauliTerm` or measuring in its eigenbasis.
+    It is primarily used by the `pauli_term_to_preparation_memory_map` and `pauli_term_to_measurement_memory_map`
+    functions.
 
     :param term: The ``PauliTerm`` in question.
     :param prefix: The prefix for the declared memory region labels. For example, if the prefix
@@ -94,8 +89,8 @@ def pauli_term_to_euler_memory_map(
         of angles as values).
     """
     # no need to provide a memory map when no rotations are necessary
-    if ("X" not in term.pauli_string(cast(List[int], term.get_qubits()))) and (
-        "Y" not in term.pauli_string(cast(List[int], term.get_qubits()))
+    if ("X" not in term.pauli_string(cast(list[int], term.get_qubits()))) and (
+        "Y" not in term.pauli_string(cast(list[int], term.get_qubits()))
     ):
         return {}
 
@@ -104,7 +99,7 @@ def pauli_term_to_euler_memory_map(
     gamma_label = f"{prefix}_{suffix_gamma}"
 
     # assume the pauli indices are equivalent to the memory region
-    memory_size = max(cast(List[int], term.get_qubits())) + 1
+    memory_size = max(cast(list[int], term.get_qubits())) + 1
 
     memory_map = {
         alpha_label: [0.0] * memory_size,
@@ -115,7 +110,8 @@ def pauli_term_to_euler_memory_map(
     tuples = {"X": tuple_x, "Y": tuple_y, "Z": tuple_z, "I": tuple_z}
 
     for qubit, operator in term:
-        assert isinstance(qubit, int)
+        if not isinstance(qubit, int):
+            raise TypeError("qubit must be an int")
         if operator not in tuples:
             raise ValueError(f"Unknown operator {operator}")
         memory_map[alpha_label][qubit] = tuples[operator][0]
@@ -125,11 +121,10 @@ def pauli_term_to_euler_memory_map(
     return memory_map
 
 
-def pauli_term_to_preparation_memory_map(term: PauliTerm, label: str = "preparation") -> Dict[str, List[float]]:
-    """
-    Given a ``PauliTerm``, create a memory map corresponding to the ZXZXZ-decomposed single-qubit
-    gates that prepare the plus one eigenstate of the ``PauliTerm``. For example, if we have the
-    following program:
+def pauli_term_to_preparation_memory_map(term: PauliTerm, label: str = "preparation") -> dict[str, list[float]]:
+    """Create a memory map for ZXZXZ-decomposed single-qubit gates to prepare the plus one eigenstate of the PauliTerm.
+
+    For example, if we have the following program:
 
         RZ(preparation_alpha[0]) 0
         RX(pi/2) 0
@@ -150,11 +145,10 @@ def pauli_term_to_preparation_memory_map(term: PauliTerm, label: str = "preparat
     return pauli_term_to_euler_memory_map(term, prefix=label, tuple_x=P_X, tuple_y=P_Y, tuple_z=P_Z)
 
 
-def pauli_term_to_measurement_memory_map(term: PauliTerm, label: str = "measurement") -> Dict[str, List[float]]:
-    """
-    Given a ``PauliTerm``, create a memory map corresponding to the ZXZXZ-decomposed single-qubit
-    gates that allow for measurement in the eigenbasis of the ``PauliTerm``. For example, if we
-    have the following program:
+def pauli_term_to_measurement_memory_map(term: PauliTerm, label: str = "measurement") -> dict[str, list[float]]:
+    """Create a memory map for ZXZXZ-decomposed single-qubit gates to measure in the eigenbasis of the PauliTerm.
+
+    For example, if we have the following program:
 
         RZ(measurement_alpha[0]) 0
         RX(pi/2) 0
@@ -177,10 +171,11 @@ def pauli_term_to_measurement_memory_map(term: PauliTerm, label: str = "measurem
 
 
 def merge_memory_map_lists(
-    mml1: List[Dict[str, List[float]]], mml2: List[Dict[str, List[float]]]
-) -> List[Dict[str, List[float]]]:
-    """
-    Given two lists of memory maps, produce the "cartesian product" of the memory maps:
+    mml1: list[dict[str, list[float]]], mml2: list[dict[str, list[float]]]
+) -> list[dict[str, list[float]]]:
+    """Given two lists of memory maps, produce the "cartesian product" of the memory maps.
+
+    For example:
 
         merge_memory_map_lists([{a: 1}, {a: 2}], [{b: 3, c: 4}, {b: 5, c: 6}])
 
diff --git a/pyquil/experiment/_program.py b/pyquil/experiment/_program.py
index 8b1f6723c..579356a5a 100644
--- a/pyquil/experiment/_program.py
+++ b/pyquil/experiment/_program.py
@@ -14,7 +14,7 @@
 #    limitations under the License.
 ##############################################################################
 
-from typing import Sequence
+from collections.abc import Sequence
 
 import numpy as np
 
@@ -30,10 +30,10 @@ def parameterized_euler_rotations(
     suffix_beta: str = "beta",
     suffix_gamma: str = "gamma",
 ) -> Program:
-    """
-    Given a number of qubits (n), build a ``Program`` containing a ZXZXZ-decomposed gate on each
-    qubit, where each ``RZ`` is parameterized by declared values with labels given by the "prefix"
-    and "suffix" arguments. Put more plainly, the resulting Quil program on n qubits is::
+    """Given a number of qubits (n), build a ``Program`` containing a ZXZXZ-decomposed gate on each qubit.
+
+    Each ``RZ`` is parameterized by declared values with labels given by the "prefix" and "suffix" arguments. Put more
+    plainly, the resulting Quil program on n qubits is::
 
         RZ(alpha_label[0]) 0
         RX(pi/2) 0
@@ -80,37 +80,33 @@ def parameterized_euler_rotations(
 
 
 def parameterized_single_qubit_state_preparation(qubits: Sequence[int], label: str = "preparation") -> Program:
-    """
-    Given a number of qubits, produce a program as in ``parameterized_euler_rotations`` where each
-    memory region is prefixed by ``label``, where label defaults to "preparation".
+    """Produce a program as in ``parameterized_euler_rotations`` where each memory region is prefixed by "preparation".
 
     :param qubits: The number of qubits (n).
-    :param label: The prefix to use when declaring memory in ``parameterized_euler_rotations``.
+    :param label: The prefix to use when declaring memory in ``parameterized_euler_rotations``. (default: "preparation")
     :return: A parameterized ``Program`` that can be used to prepare a product state.
     """
     return parameterized_euler_rotations(qubits, prefix=label)
 
 
 def parameterized_single_qubit_measurement_basis(qubits: Sequence[int], label: str = "measurement") -> Program:
-    """
-    Given a number of qubits, produce a program as in ``parameterized_euler_rotations`` where each
-    memory region is prefixed by ``label``, where label defaults to "measurement".
+    """Produce a program as in ``parameterized_euler_rotations`` where each memory region is prefixed by "measurement".
 
     :param qubits: The number of qubits (n).
-    :param label: The prefix to use when declaring memory in ``parameterized_euler_rotations``.
-    :return: A parameterized ``Program`` that can be used to measure in a multi-qubit Pauli basis.
+    :param label: The prefix to use when declaring memory in ``parameterized_euler_rotations``. (default: "measurement")
+    :return: A parameterized ``Program`` that can be used to prepare a product state.
     """
     return parameterized_euler_rotations(qubits, prefix=label)
 
 
 def parameterized_readout_symmetrization(qubits: Sequence[int], label: str = "symmetrization") -> Program:
-    """
-    Given a number of qubits (n), produce a ``Program`` with an ``RX`` instruction on qubits
-    0 through n-1, parameterized by memory regions label[0] through label[n-1], where "label"
-    defaults to "symmetrization".
+    """Given a number of qubits (n), produce a parameterized ``Program`` with an ``RX`` instruction on qubits [0, n-1].
+
+    Qubits 0 through n-1 are parameterized by memory regions label[0] through label[n-1], where "label" defaults to
+    "symmetrization".
 
     :param qubits: The number of qubits (n).
-    :param label: The name of the declared memory region.
+    :param label: The name of the declared memory region. (default: "symmetrization")
     :return: A ``Program`` with parameterized ``RX`` gates on n qubits.
     """
     p = Program()
@@ -121,9 +117,9 @@ def parameterized_readout_symmetrization(qubits: Sequence[int], label: str = "sy
 
 
 def measure_qubits(qubits: Sequence[int]) -> Program:
-    """
-    Given a number of qubits (n), produce a ``Program`` with a ``MEASURE`` instruction on qubits
-    0 through n-1, with corresponding readout registers ro[0] through ro[n-1].
+    """Given a number of qubits (n), produce a ``Program`` with a ``MEASURE`` instruction on qubits [0, n-1].
+
+    Each MEASURE is written to corresponding readout registers ro[0] through ro[n-1].
 
     :param qubits: The number of qubits (n).
     :return: A ``Program`` that measures n qubits.
diff --git a/pyquil/experiment/_result.py b/pyquil/experiment/_result.py
index ce769d64a..5885c99cc 100644
--- a/pyquil/experiment/_result.py
+++ b/pyquil/experiment/_result.py
@@ -13,13 +13,15 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
+"""Definition of an ExperimentResult.
+
+An ExperimentResult encapsulates the outcome of a collection of measurements that are aimed at estimating the
+expectation value of some observable.
 """
-Schema definition of an ExperimentResult, which encapsulates the outcome of a collection of
-measurements that are aimed at estimating the expectation value of some observable.
-"""
+
 import logging
 from dataclasses import dataclass
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Optional, Union
 
 import numpy as np
 
@@ -30,8 +32,7 @@
 
 @dataclass(frozen=True)
 class ExperimentResult:
-    """An expectation and standard deviation for the measurement of one experiment setting
-    in a tomographic experiment.
+    """An expectation and standard deviation for the measurement of one experiment setting in a tomographic experiment.
 
     In the case of readout error calibration, we also include
     expectation, standard deviation and count for the calibration results, as well as the
@@ -47,7 +48,7 @@ class ExperimentResult:
     calibration_expectation: Optional[Union[float, complex]] = None
     calibration_std_err: Optional[Union[float, complex]] = None
     calibration_counts: Optional[int] = None
-    additional_results: Optional[List["ExperimentResult"]] = None
+    additional_results: Optional[list["ExperimentResult"]] = None
 
     def __init__(
         self,
@@ -60,9 +61,8 @@ def __init__(
         calibration_expectation: Optional[Union[float, complex]] = None,
         calibration_std_err: Optional[Union[float, complex]] = None,
         calibration_counts: Optional[int] = None,
-        additional_results: Optional[List["ExperimentResult"]] = None,
+        additional_results: Optional[list["ExperimentResult"]] = None,
     ):
-
         object.__setattr__(self, "setting", setting)
         object.__setattr__(self, "expectation", expectation)
         object.__setattr__(self, "total_counts", total_counts)
@@ -80,7 +80,7 @@ def __str__(self) -> str:
     def __repr__(self) -> str:
         return f"ExperimentResult[{self}]"
 
-    def serializable(self) -> Dict[str, Any]:
+    def serializable(self) -> dict[str, Any]:
         return {
             "type": "ExperimentResult",
             "setting": self.setting,
@@ -96,12 +96,11 @@ def serializable(self) -> Dict[str, Any]:
 
 
 def bitstrings_to_expectations(
-    bitstrings: np.ndarray, joint_expectations: Optional[List[List[int]]] = None
+    bitstrings: np.ndarray, joint_expectations: Optional[list[list[int]]] = None
 ) -> np.ndarray:
-    """
-    Given an array of bitstrings (each of which is represented as an array of bits), map them to
-    expectation values and return the desired joint expectation values. If no joint expectations
-    are desired, then just the 1 -> -1, 0 -> 1 mapping is performed.
+    """Given an array of bitstrings, map them to expectation values and return the desired joint expectation values.
+
+    If no joint expectations are desired, then just the 1 -> -1, 0 -> 1 mapping is performed.
 
     :param bitstrings: Array of bitstrings to map.
     :param joint_expectations: Joint expectation values to calculate. Each entry is a list which
@@ -131,9 +130,7 @@ def correct_experiment_result(
     result: ExperimentResult,
     calibration: ExperimentResult,
 ) -> ExperimentResult:
-    """
-    Given a raw, unmitigated result and its associated readout calibration, produce the result
-    absent readout error.
+    """Given a raw, unmitigated result and its associated readout calibration, produce the result absent readout error.
 
     :param result: An ``ExperimentResult`` object with unmitigated readout error.
     :param calibration: An ``ExperimentResult`` object resulting from running readout calibration
@@ -142,16 +139,23 @@ def correct_experiment_result(
     """
     corrected_expectation = result.expectation / calibration.expectation
 
-    # combine standard errors (are we assuming the counts are the same?)
-    assert result.std_err is not None and calibration.std_err is not None
+    if result.std_err is None or calibration.std_err is None:
+        raise ValueError("Standard error not present in result or calibration.")
+
     corrected_variance = ratio_variance(
-        result.expectation, result.std_err**2, calibration.expectation, calibration.std_err**2  # type: ignore
+        result.expectation,
+        result.std_err**2,
+        calibration.expectation,
+        calibration.std_err**2,
     )
 
     # recursively apply to additional results
     additional_results = None
     if result.additional_results is not None and calibration.additional_results:
-        assert len(result.additional_results) == len(calibration.additional_results)
+        if len(result.additional_results) != len(calibration.additional_results):
+            len_result = len(result.additional_results)
+            len_calibration = len(calibration.additional_results)
+            raise ValueError(f"Length of results ({len_result}) should match calibration ({len_calibration}).")
         additional_results = [
             correct_experiment_result(r, c) for r, c in zip(result.additional_results, calibration.additional_results)
         ]
@@ -171,12 +175,13 @@ def correct_experiment_result(
 
 
 def ratio_variance(
-    a: Union[float, np.ndarray],
-    var_a: Union[float, np.ndarray],
-    b: Union[float, np.ndarray],
-    var_b: Union[float, np.ndarray],
-) -> Union[float, np.ndarray]:
-    r"""
+    a: Union[float, complex, np.number, np.ndarray],
+    var_a: Union[float, complex, np.number, np.ndarray],
+    b: Union[float, complex, np.number, np.ndarray],
+    var_b: Union[float, complex, np.number, np.ndarray],
+) -> Union[float, complex, np.number, np.ndarray]:
+    r"""Compute the variance on the ratio Y = A/B.
+
     Given random variables 'A' and 'B', compute the variance on the ratio Y = A/B. Denote the
     mean of the random variables as a = E[A] and b = E[B] while the variances are var_a = Var[A]
     and var_b = Var[B] and the covariance as Cov[A,B]. The following expression approximates the
@@ -204,4 +209,5 @@ def ratio_variance(
     :param b: Mean of 'B', to be used as the numerator in a ratio.
     :param var_b: Variance in 'B'
     """
-    return var_a / b**2 + (a**2 * var_b) / b**4
+    result = var_a / b**2 + (a**2 * var_b) / b**4
+    return result
diff --git a/pyquil/experiment/_setting.py b/pyquil/experiment/_setting.py
index d8d91234f..faf443927 100644
--- a/pyquil/experiment/_setting.py
+++ b/pyquil/experiment/_setting.py
@@ -13,14 +13,17 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
+"""Definition of an ExperimentSetting.
+
+Each ExperimentSetting corresponds to preparing a collection of qubits in a TensorProductState and measuring them in a
+PauliTerm-defined basis.
 """
-Schema definition of an ExperimentSetting, which corresponds to preparing a collection of qubits
-in a TensorProductState and measuring them in a PauliTerm-defined basis.
-"""
+
 import logging
 import re
+from collections.abc import Generator, Iterable
 from dataclasses import dataclass
-from typing import Any, FrozenSet, Generator, Iterable, List, Optional, cast
+from typing import Any, Optional, cast
 
 from pyquil.paulis import PauliTerm, sI
 
@@ -29,8 +32,7 @@
 
 @dataclass(frozen=True)
 class _OneQState:
-    """
-    A description of a named one-qubit quantum state.
+    """A description of a named one-qubit quantum state.
 
     This can be used to generate pre-rotations for quantum process tomography. For example,
     X0_14 will generate the +1 eigenstate of the X operator on qubit 14. X1_14 will generate the
@@ -54,12 +56,9 @@ def from_str(cls, s: str) -> "_OneQState":
 
 @dataclass(frozen=True)
 class TensorProductState:
-    """
-    A description of a multi-qubit quantum state that is a tensor product of many _OneQStates
-    states.
-    """
+    """A description of a multi-qubit quantum state that is a tensor product of many _OneQStates states."""
 
-    states: List[_OneQState]
+    states: list[_OneQState]
 
     def __init__(self, states: Optional[Iterable[_OneQState]] = None):
         if states is None:
@@ -88,7 +87,7 @@ def __iter__(self) -> Generator[_OneQState, None, None]:
     def __len__(self) -> int:
         return len(self.states)
 
-    def states_as_set(self) -> FrozenSet[_OneQState]:
+    def states_as_set(self) -> frozenset[_OneQState]:
         return frozenset(self.states)
 
     def __eq__(self, other: Any) -> bool:
@@ -153,8 +152,7 @@ def zeros_state(qubits: Iterable[int]) -> TensorProductState:
 
 @dataclass(frozen=True, init=False)
 class ExperimentSetting:
-    """
-    Input and output settings for a tomography-like experiment.
+    """Input and output settings for a tomography-like experiment.
 
     Many near-term quantum algorithms take the following form:
 
@@ -174,13 +172,13 @@ class ExperimentSetting:
 
     in_state: TensorProductState
     out_operator: PauliTerm
-    additional_expectations: Optional[List[List[int]]] = None
+    additional_expectations: Optional[list[list[int]]] = None
 
     def __init__(
         self,
         in_state: TensorProductState,
         out_operator: PauliTerm,
-        additional_expectations: Optional[List[List[int]]] = None,
+        additional_expectations: Optional[list[list[int]]] = None,
     ):
         object.__setattr__(self, "in_state", in_state)
         object.__setattr__(self, "out_operator", out_operator)
@@ -211,7 +209,7 @@ def serializable(self) -> str:
 
     @classmethod
     def from_str(cls, s: str) -> "ExperimentSetting":
-        """The opposite of str(expt)"""
+        """Opposite of str(expt)."""
         instr, outstr = s.split("→")
         return ExperimentSetting(
             in_state=TensorProductState.from_str(instr),
diff --git a/pyquil/external/__init__.py b/pyquil/external/__init__.py
index e69de29bb..929c5e33d 100644
--- a/pyquil/external/__init__.py
+++ b/pyquil/external/__init__.py
@@ -0,0 +1,1 @@
+"""Miscellaneous supporting modules."""
diff --git a/pyquil/external/rpcq.py b/pyquil/external/rpcq.py
index 714f4f7cb..25890dfeb 100644
--- a/pyquil/external/rpcq.py
+++ b/pyquil/external/rpcq.py
@@ -1,15 +1,19 @@
+"""Models and functions that support the use of the `quilc` rpcq server."""
+
 import json
-from typing import Dict, List, Union, Optional, Literal
+from dataclasses import dataclass, field
+from typing import Literal, Optional, Union
 
 from deprecated.sphinx import deprecated
 from rpcq.messages import TargetDevice as TargetQuantumProcessor
-from dataclasses import dataclass, field
 
-JsonValue = Union[type(None), bool, int, float, str, List["JsonValue"], Dict[str, "JsonValue"]]
+JsonValue = Union[type(None), bool, int, float, str, list["JsonValue"], dict[str, "JsonValue"]]
 
 
 @dataclass
 class Operator:
+    """Operator class for representing a quantum gate or measurement."""
+
     operator: Optional[str] = None
     duration: Optional[float] = None
     fidelity: Optional[float] = None
@@ -18,7 +22,7 @@ def __post_init__(self):
         self.duration = float(self.duration) if self.duration is not None else None
         self.fidelity = float(self.fidelity) if self.fidelity is not None else None
 
-    def _dict(self) -> Dict[str, JsonValue]:
+    def _dict(self) -> dict[str, JsonValue]:
         if type(self) is Operator:
             raise ValueError("Should be a subclass")
         return dict(
@@ -32,10 +36,11 @@ def _dict(self) -> Dict[str, JsonValue]:
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
     def dict(self):
+        """Return a dictionary representation of the Operator."""
         return self._dict()
 
     @classmethod
-    def _parse_obj(cls, dictionary: Dict):
+    def _parse_obj(cls, dictionary: dict):
         return Operator(**dictionary)
 
     @classmethod
@@ -43,20 +48,24 @@ def _parse_obj(cls, dictionary: Dict):
         version="4.6.2",
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
-    def parse_obj(cls, dictionary: Dict):
+    def parse_obj(cls, dictionary: dict):
+        """Parse a dictionary into an Operator object."""
         return cls._parse_obj(dictionary)
 
 
 @dataclass
 class MeasureInfo(Operator):
+    """MeasureInfo class for representing a measurement operation."""
+
     qubit: Optional[Union[int, str]] = None
     target: Optional[Union[int, str]] = None
     operator_type: Literal["measure"] = "measure"
 
     def __post_init__(self):
+        """Post initialization method for MeasureInfo."""
         self.qubit = str(self.qubit)
 
-    def _dict(self) -> Dict[str, JsonValue]:
+    def _dict(self) -> dict[str, JsonValue]:
         return dict(
             operator_type=self.operator_type,
             operator=self.operator,
@@ -71,10 +80,11 @@ def _dict(self) -> Dict[str, JsonValue]:
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
     def dict(self):
+        """Return a dictionary representation of the MeasureInfo."""
         return self._dict()
 
     @classmethod
-    def _parse_obj(cls, dictionary: Dict) -> "MeasureInfo":
+    def _parse_obj(cls, dictionary: dict) -> "MeasureInfo":
         return MeasureInfo(
             operator=dictionary.get("operator"),
             duration=dictionary.get("duration"),
@@ -88,17 +98,20 @@ def _parse_obj(cls, dictionary: Dict) -> "MeasureInfo":
         version="4.6.2",
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
-    def parse_obj(cls, dictionary: Dict):
+    def parse_obj(cls, dictionary: dict):
+        """Parse a dictionary into a MeasureInfo object."""
         return cls._parse_obj(dictionary)
 
 
 @dataclass
 class GateInfo(Operator):
-    parameters: List[Union[float, str]] = field(default_factory=list)
-    arguments: List[Union[int, str]] = field(default_factory=list)
+    """GateInfo class for representing a quantum gate operation."""
+
+    parameters: list[Union[float, str]] = field(default_factory=list)
+    arguments: list[Union[int, str]] = field(default_factory=list)
     operator_type: Literal["gate"] = "gate"
 
-    def _dict(self) -> Dict[str, JsonValue]:
+    def _dict(self) -> dict[str, JsonValue]:
         return dict(
             operator_type=self.operator_type,
             operator=self.operator,
@@ -113,6 +126,7 @@ def _dict(self) -> Dict[str, JsonValue]:
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
     def dict(self):
+        """Return a dictionary representation of the GateInfo."""
         return self._dict()
 
     @classmethod
@@ -130,7 +144,8 @@ def _parse_obj(cls, dictionary: dict) -> "GateInfo":
         version="4.6.2",
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
-    def parse_obj(cls, dictionary: Dict):
+    def parse_obj(cls, dictionary: dict):
+        """Parse a dictionary into a GateInfo object."""
         return cls._parse_obj(dictionary)
 
 
@@ -145,11 +160,13 @@ def _parse_operator(dictionary: dict) -> Union[GateInfo, MeasureInfo]:
 
 @dataclass
 class Qubit:
+    """Qubit class for representing a qubit in a quantum processor."""
+
     id: int
     dead: Optional[bool] = False
-    gates: List[Union[GateInfo, MeasureInfo]] = field(default_factory=list)
+    gates: list[Union[GateInfo, MeasureInfo]] = field(default_factory=list)
 
-    def _dict(self) -> Dict[str, JsonValue]:
+    def _dict(self) -> dict[str, JsonValue]:
         encoding = dict(id=self.id, gates=[g._dict() for g in self.gates])
         if self.dead:
             encoding["dead"] = self.dead
@@ -160,10 +177,11 @@ def _dict(self) -> Dict[str, JsonValue]:
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
     def dict(self):
+        """Return a dictionary representation of the Qubit."""
         return self._dict()
 
     @classmethod
-    def _parse_obj(cls, dictionary: Dict) -> "Qubit":
+    def _parse_obj(cls, dictionary: dict) -> "Qubit":
         return Qubit(
             id=dictionary["id"],
             dead=bool(dictionary.get("dead")),
@@ -175,17 +193,20 @@ def _parse_obj(cls, dictionary: Dict) -> "Qubit":
         version="4.6.2",
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
-    def parse_obj(cls, dictionary: Dict):
+    def parse_obj(cls, dictionary: dict):
+        """Parse a dictionary into a Qubit object."""
         return cls._parse_obj(dictionary)
 
 
 @dataclass
 class Edge:
-    ids: List[int]
+    """Edge class for representing a connection between two qubits."""
+
+    ids: list[int]
     dead: Optional[bool] = False
-    gates: List[GateInfo] = field(default_factory=list)
+    gates: list[GateInfo] = field(default_factory=list)
 
-    def _dict(self) -> Dict[str, JsonValue]:
+    def _dict(self) -> dict[str, JsonValue]:
         encoding = dict(ids=self.ids, gates=[g._dict() for g in self.gates])
         if self.dead:
             encoding["dead"] = self.dead
@@ -196,6 +217,7 @@ def _dict(self) -> Dict[str, JsonValue]:
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
     def dict(self):
+        """Return a dictionary representation of the Edge."""
         return self._dict()
 
     @classmethod
@@ -211,16 +233,19 @@ def _parse_obj(cls, dictionary: dict) -> "Edge":
         version="4.6.2",
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
-    def parse_obj(cls, dictionary: Dict):
+    def parse_obj(cls, dictionary: dict):
+        """Parse a dictionary into an Edge object."""
         return cls._parse_obj(dictionary)
 
 
 @dataclass
 class CompilerISA:
-    qubits: Dict[str, Qubit] = field(default_factory=dict)
-    edges: Dict[str, Edge] = field(default_factory=dict)
+    """CompilerISA class for representing the instruction set architecture of a quantum processor."""
+
+    qubits: dict[str, Qubit] = field(default_factory=dict)
+    edges: dict[str, Edge] = field(default_factory=dict)
 
-    def _dict(self, by_alias=False) -> Dict[str, JsonValue]:
+    def _dict(self, by_alias=False) -> dict[str, JsonValue]:
         return {
             "1Q" if by_alias else "qubits": {k: q._dict() for k, q in self.qubits.items()},
             "2Q" if by_alias else "edges": {k: e._dict() for k, e in self.edges.items()},
@@ -231,10 +256,11 @@ def _dict(self, by_alias=False) -> Dict[str, JsonValue]:
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
     def dict(self, by_alias=False):
+        """Return a dictionary representation of the CompilerISA."""
         return self._dict(by_alias=by_alias)
 
     @classmethod
-    def _parse_obj(cls, dictionary: Dict):
+    def _parse_obj(cls, dictionary: dict):
         qubit_dict = dictionary.get("1Q", {})
         edge_dict = dictionary.get("2Q", {})
         return CompilerISA(
@@ -247,7 +273,8 @@ def _parse_obj(cls, dictionary: Dict):
         version="4.6.2",
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
-    def parse_obj(cls, dictionary: Dict):
+    def parse_obj(cls, dictionary: dict):
+        """Parse a dictionary into a CompilerISA object."""
         return cls._parse_obj(dictionary)
 
     @classmethod
@@ -256,26 +283,31 @@ def parse_obj(cls, dictionary: Dict):
         reason="No longer requires serialization of RPCQ objects and is dropping Pydantic as a dependency.",  # noqa: E501
     )
     def parse_file(cls, filename: str):
-        with open(filename, "r") as file:
+        """Parse a JSON file into a CompilerISA object."""
+        with open(filename) as file:
             json_dict = json.load(file)
             return cls._parse_obj(json_dict)
 
 
 def add_qubit(quantum_processor: CompilerISA, node_id: int) -> Qubit:
+    """Add a qubit to the quantum processor ISA."""
     if node_id not in quantum_processor.qubits:
         quantum_processor.qubits[str(node_id)] = Qubit(id=node_id)
     return quantum_processor.qubits[str(node_id)]
 
 
 def get_qubit(quantum_processor: CompilerISA, node_id: int) -> Optional[Qubit]:
+    """Get a qubit from the quantum processor ISA."""
     return quantum_processor.qubits.get(str(node_id))
 
 
 def make_edge_id(qubit1: int, qubit2: int) -> str:
+    """Make an edge ID from two qubit IDs."""
     return "-".join([str(qubit) for qubit in sorted([qubit1, qubit2])])
 
 
 def add_edge(quantum_processor: CompilerISA, qubit1: int, qubit2: int) -> Edge:
+    """Add an Edge between two qubit IDs."""
     edge_id = make_edge_id(qubit1, qubit2)
     if edge_id not in quantum_processor.edges:
         quantum_processor.edges[edge_id] = Edge(ids=sorted([qubit1, qubit2]))
@@ -283,16 +315,20 @@ def add_edge(quantum_processor: CompilerISA, qubit1: int, qubit2: int) -> Edge:
 
 
 def get_edge(quantum_processor: CompilerISA, qubit1: int, qubit2: int) -> Optional[Edge]:
+    """Get an Edge between two qubit IDs."""
     edge_id = make_edge_id(qubit1, qubit2)
     return quantum_processor.edges.get(edge_id)
 
 
 def compiler_isa_to_target_quantum_processor(compiler_isa: CompilerISA) -> TargetQuantumProcessor:
+    """Convert a CompilerISA object to a TargetQuantumProcessor object."""
     return TargetQuantumProcessor(isa=compiler_isa.dict(by_alias=True), specs={})
 
 
 class Supported1QGate:
-    I = "I"
+    """Supported 1Q gates."""
+
+    I = "I"  # noqa: E741 - I is not an ambiguous gate name.
     RX = "RX"
     RZ = "RZ"
     MEASURE = "MEASURE"
@@ -300,6 +336,8 @@ class Supported1QGate:
 
 
 class Supported2QGate:
+    """Supported 2Q gates."""
+
     WILDCARD = "WILDCARD"
     CZ = "CZ"
     ISWAP = "ISWAP"
diff --git a/pyquil/gates.py b/pyquil/gates.py
index 068caa19d..9709e13b3 100644
--- a/pyquil/gates.py
+++ b/pyquil/gates.py
@@ -13,12 +13,14 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from deprecated.sphinx import deprecated
-from deprecated.sphinx import versionadded
+"""Standard Gate definitions to use in a ``Program``."""
+
+from collections.abc import Mapping, Sequence
 from numbers import Real
-from typing import Callable, Mapping, Optional, Tuple, Union, Sequence, no_type_check
+from typing import Callable, Optional, Union, no_type_check
 
 import numpy as np
+from deprecated.sphinx import deprecated, versionadded
 
 from pyquil.quilatom import (
     Expression,
@@ -27,64 +29,62 @@
     MemoryReference,
     MemoryReferenceDesignator,
     ParameterDesignator,
-    QubitDesignator,
     Qubit,
+    QubitDesignator,
+    Waveform,
     unpack_classical_reg,
     unpack_qubit,
-    Waveform,
 )
 from pyquil.quilbase import (
     AbstractInstruction,
-    Declare,
-    Gate,
-    Halt,
-    Reset,
-    ResetQubit,
-    Measurement,
-    Nop,
-    Wait,
-    ClassicalNeg,
-    ClassicalNot,
+    Capture,
+    ClassicalAdd,
     ClassicalAnd,
-    ClassicalInclusiveOr,
-    ClassicalExclusiveOr,
+    ClassicalConvert,
+    ClassicalDiv,
     ClassicalEqual,
+    ClassicalExchange,
+    ClassicalExclusiveOr,
     ClassicalGreaterEqual,
     ClassicalGreaterThan,
+    ClassicalInclusiveOr,
     ClassicalLessEqual,
     ClassicalLessThan,
-    ClassicalMove,
-    ClassicalExchange,
-    ClassicalConvert,
     ClassicalLoad,
+    ClassicalMove,
+    ClassicalMul,
+    ClassicalNeg,
+    ClassicalNot,
     ClassicalStore,
-    ClassicalAdd,
     ClassicalSub,
-    ClassicalMul,
-    ClassicalDiv,
+    Declare,
+    DelayFrames,
+    DelayQubits,
+    Fence,
+    FenceAll,
+    Gate,
+    Halt,
+    Measurement,
+    Nop,
     Pulse,
+    RawCapture,
+    Reset,
+    ResetQubit,
     SetFrequency,
-    ShiftFrequency,
     SetPhase,
+    SetScale,
+    ShiftFrequency,
     ShiftPhase,
     SwapPhases,
-    SetScale,
-    Capture,
-    RawCapture,
-    DelayFrames,
-    DelayQubits,
-    FenceAll,
-    Fence,
+    Wait,
 )
 
 
 def unpack_reg_val_pair(
     classical_reg1: MemoryReferenceDesignator,
     classical_reg2: Union[MemoryReferenceDesignator, int, float],
-) -> Tuple[MemoryReference, Union[MemoryReference, int, float]]:
-    """
-    Helper function for typechecking / type-coercing arguments to constructors for binary classical
-    operators.
+) -> tuple[MemoryReference, Union[MemoryReference, int, float]]:
+    """Type check/coerce arguments to constructors for binary classical operators.
 
     :param classical_reg1: Specifier for the classical memory address to be modified.
     :param classical_reg2: Specifier for the second argument: a classical memory address or an
@@ -101,10 +101,8 @@ def prepare_ternary_operands(
     classical_reg1: MemoryReferenceDesignator,
     classical_reg2: MemoryReferenceDesignator,
     classical_reg3: Union[MemoryReferenceDesignator, int, float],
-) -> Tuple[MemoryReference, MemoryReference, Union[MemoryReference, int, float]]:
-    """
-    Helper function for typechecking / type-coercing arguments to constructors for ternary
-    classical operators.
+) -> tuple[MemoryReference, MemoryReference, Union[MemoryReference, int, float]]:
+    """Type check/coerce arguments to constructors for ternary classical operators.
 
     :param classical_reg1: Specifier for the classical memory address to be modified.
     :param classical_reg2: Specifier for the left operand: a classical memory address.
@@ -124,8 +122,8 @@ def prepare_ternary_operands(
     return classical_reg1, classical_reg2, classical_reg3
 
 
-def I(qubit: QubitDesignator) -> Gate:
-    """Produces the I identity gate::
+def I(qubit: QubitDesignator) -> Gate:  # noqa: E743 - I is not ambiguous in this case
+    """Produce a single-qubit identity gate (I).
 
         I = [1, 0]
             [0, 1]
@@ -142,7 +140,7 @@ def I(qubit: QubitDesignator) -> Gate:
 
 
 def X(qubit: QubitDesignator) -> Gate:
-    """Produces the X ("NOT") gate::
+    """Produce the X ("NOT") gate.
 
         X = [[0, 1],
              [1, 0]]
@@ -156,7 +154,7 @@ def X(qubit: QubitDesignator) -> Gate:
 
 
 def Y(qubit: QubitDesignator) -> Gate:
-    """Produces the Y gate::
+    """Produce the Y gate.
 
         Y = [[0, 0 - 1j],
              [0 + 1j, 0]]
@@ -170,7 +168,7 @@ def Y(qubit: QubitDesignator) -> Gate:
 
 
 def Z(qubit: QubitDesignator) -> Gate:
-    """Produces the Z gate::
+    """Produce the Z gate.
 
         Z = [[1,  0],
              [0, -1]]
@@ -184,12 +182,12 @@ def Z(qubit: QubitDesignator) -> Gate:
 
 
 def H(qubit: QubitDesignator) -> Gate:
-    """Produces the Hadamard gate::
+    """Produce the Hadamard gate.
 
         H = (1 / sqrt(2)) * [[1,  1],
                              [1, -1]]
 
-    Produces the H instruction. This gate is a single qubit Hadamard gate.
+    Produce the H instruction. This gate is a single qubit Hadamard gate.
 
     :param qubit: The qubit apply the gate to.
     :returns: A Gate object.
@@ -198,7 +196,7 @@ def H(qubit: QubitDesignator) -> Gate:
 
 
 def S(qubit: QubitDesignator) -> Gate:
-    """Produces the S gate::
+    """Produce the S gate.
 
         S = [[1, 0],
              [0, 1j]]
@@ -212,7 +210,7 @@ def S(qubit: QubitDesignator) -> Gate:
 
 
 def T(qubit: QubitDesignator) -> Gate:
-    """Produces the T gate::
+    """Produce the T gate.
 
         T = [[1, 0],
              [0, exp(1j * pi / 4)]]
@@ -226,7 +224,7 @@ def T(qubit: QubitDesignator) -> Gate:
 
 
 def RX(angle: ParameterDesignator, qubit: QubitDesignator) -> Gate:
-    """Produces the RX gate::
+    """Produce the RX gate.
 
         RX(phi) = [[cos(phi / 2), -1j * sin(phi / 2)],
                    [-1j * sin(phi / 2), cos(phi / 2)]]
@@ -241,7 +239,7 @@ def RX(angle: ParameterDesignator, qubit: QubitDesignator) -> Gate:
 
 
 def RY(angle: ParameterDesignator, qubit: QubitDesignator) -> Gate:
-    """Produces the RY gate::
+    """Produce the RY gate.
 
         RY(phi) = [[cos(phi / 2), -sin(phi / 2)],
                    [sin(phi / 2), cos(phi / 2)]]
@@ -256,7 +254,7 @@ def RY(angle: ParameterDesignator, qubit: QubitDesignator) -> Gate:
 
 
 def RZ(angle: ParameterDesignator, qubit: QubitDesignator) -> Gate:
-    """Produces the RZ gate::
+    """Produce the RZ gate.
 
         RZ(phi) = [[cos(phi / 2) - 1j * sin(phi / 2), 0]
                    [0, cos(phi / 2) + 1j * sin(phi / 2)]]
@@ -271,7 +269,7 @@ def RZ(angle: ParameterDesignator, qubit: QubitDesignator) -> Gate:
 
 
 def U(theta: ParameterDesignator, phi: ParameterDesignator, lam: ParameterDesignator, qubit: QubitDesignator) -> Gate:
-    """Produces a generic single-qubit rotation::
+    """Produce a generic single-qubit rotation.
 
         U(theta, phi, lam) = [[              cos(theta / 2),  -1 * exp(1j*lam) * sin(theta / 2)]
                               [exp(1j*phi) * sin(theta / 2), exp(1j*(phi+lam)) * cos(theta / 2)]]
@@ -288,7 +286,7 @@ def U(theta: ParameterDesignator, phi: ParameterDesignator, lam: ParameterDesign
 
 
 def PHASE(angle: ParameterDesignator, qubit: QubitDesignator) -> Gate:
-    """Produces the PHASE gate::
+    """Produce the PHASE gate.
 
         PHASE(phi) = [[1, 0],
                       [0, exp(1j * phi)]]
@@ -303,7 +301,7 @@ def PHASE(angle: ParameterDesignator, qubit: QubitDesignator) -> Gate:
 
 
 def CZ(control: QubitDesignator, target: QubitDesignator) -> Gate:
-    """Produces a controlled-Z gate::
+    """Produce a controlled-Z gate.
 
         CZ = [[1, 0, 0,  0],
               [0, 1, 0,  0],
@@ -322,7 +320,7 @@ def CZ(control: QubitDesignator, target: QubitDesignator) -> Gate:
 
 
 def CNOT(control: QubitDesignator, target: QubitDesignator) -> Gate:
-    """Produces a controlled-NOT (controlled-X) gate::
+    """Produce a controlled-NOT (controlled-X) gate.
 
         CNOT = [[1, 0, 0, 0],
                 [0, 1, 0, 0],
@@ -340,7 +338,7 @@ def CNOT(control: QubitDesignator, target: QubitDesignator) -> Gate:
 
 
 def CCNOT(control1: QubitDesignator, control2: QubitDesignator, target: QubitDesignator) -> Gate:
-    """Produces a doubly-controlled NOT gate::
+    """Produce a doubly-controlled NOT gate.
 
         CCNOT = [[1, 0, 0, 0, 0, 0, 0, 0],
                  [0, 1, 0, 0, 0, 0, 0, 0],
@@ -365,7 +363,7 @@ def CCNOT(control1: QubitDesignator, control2: QubitDesignator, target: QubitDes
 
 
 def CPHASE00(angle: ParameterDesignator, control: QubitDesignator, target: QubitDesignator) -> Gate:
-    """Produces a controlled-phase gate that phases the ``|00>`` state::
+    """Produce a controlled-phase gate that phases the ``|00>`` state.
 
         CPHASE00(phi) = diag([exp(1j * phi), 1, 1, 1])
 
@@ -382,7 +380,7 @@ def CPHASE00(angle: ParameterDesignator, control: QubitDesignator, target: Qubit
 
 
 def CPHASE01(angle: ParameterDesignator, control: QubitDesignator, target: QubitDesignator) -> Gate:
-    """Produces a controlled-phase gate that phases the ``|01>`` state::
+    """Produce a controlled-phase gate that phases the ``|01>`` state.
 
         CPHASE01(phi) = diag([1.0, exp(1j * phi), 1.0, 1.0])
 
@@ -400,7 +398,7 @@ def CPHASE01(angle: ParameterDesignator, control: QubitDesignator, target: Qubit
 
 
 def CPHASE10(angle: ParameterDesignator, control: QubitDesignator, target: QubitDesignator) -> Gate:
-    """Produces a controlled-phase gate that phases the ``|10>`` state::
+    """Produce a controlled-phase gate that phases the ``|10>`` state.
 
         CPHASE10(phi) = diag([1, 1, exp(1j * phi), 1])
 
@@ -426,7 +424,7 @@ def CPHASE(
     control: QubitDesignator,
     target: QubitDesignator,
 ) -> Gate:
-    """Produces a controlled-phase instruction::
+    """Produce a controlled-phase instruction.
 
         CPHASE(phi) = diag([1, 1, 1, exp(1j * phi)])
 
@@ -446,7 +444,7 @@ def CPHASE(
 
 
 def SWAP(q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces a SWAP gate which swaps the state of two qubits::
+    """Produce a SWAP gate which swaps the state of two qubits.
 
         SWAP = [[1, 0, 0, 0],
                 [0, 0, 1, 0],
@@ -462,7 +460,7 @@ def SWAP(q1: QubitDesignator, q2: QubitDesignator) -> Gate:
 
 
 def CSWAP(control: QubitDesignator, target_1: QubitDesignator, target_2: QubitDesignator) -> Gate:
-    """Produces a controlled-SWAP gate. This gate conditionally swaps the state of two qubits::
+    """Produce a controlled-SWAP gate. This gate conditionally swaps the state of two qubits.
 
         CSWAP = [[1, 0, 0, 0, 0, 0, 0, 0],
                  [0, 1, 0, 0, 0, 0, 0, 0],
@@ -484,7 +482,7 @@ def CSWAP(control: QubitDesignator, target_1: QubitDesignator, target_2: QubitDe
 
 
 def ISWAP(q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces an ISWAP gate::
+    """Produce an ISWAP gate.
 
         ISWAP = [[1, 0,  0,  0],
                  [0, 0,  1j, 0],
@@ -502,7 +500,7 @@ def ISWAP(q1: QubitDesignator, q2: QubitDesignator) -> Gate:
 
 
 def PSWAP(angle: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces a parameterized SWAP gate::
+    """Produce a parameterized SWAP gate.
 
         PSWAP(phi) = [[1, 0,             0,             0],
                       [0, 0,             exp(1j * phi), 0],
@@ -520,7 +518,7 @@ def PSWAP(angle: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator)
 
 
 def XY(angle: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces a parameterized ISWAP gate::
+    """Produce a parameterized ISWAP gate.
 
         XY(phi) = [[1,               0,               0, 0],
                    [0,      cos(phi/2), 1j * sin(phi/2), 0],
@@ -536,7 +534,7 @@ def XY(angle: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) ->
 
 
 def SQISW(q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces a SQISW gate::
+    """Produce a SQISW gate.
 
         SQiSW = [[1,               0,               0, 0],
                  [0,     1 / sqrt(2),    1j / sqrt(2), 0],
@@ -551,7 +549,7 @@ def SQISW(q1: QubitDesignator, q2: QubitDesignator) -> Gate:
 
 
 def FSIM(theta: ParameterDesignator, phi: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces an fsim (Fermionic simulation) gate:
+    """Produce an fsim (Fermionic simulation) gate.
 
         FSIM(theta, phi) = [[1,                 0,                 0,           0],
                             [0,      cos(theta/2), 1j * sin(theta/2),           0],
@@ -576,7 +574,7 @@ def PHASEDFSIM(
     q1: QubitDesignator,
     q2: QubitDesignator,
 ) -> Gate:
-    """Produces an phasedfsim (Fermionic simulation) gate:
+    """Produce an phasedfsim (Fermionic simulation) gate.
 
         PHASEDFSIM(theta, zeta, chi, gamma, phi) = [
             [1, 0, 0, 0],
@@ -597,7 +595,7 @@ def PHASEDFSIM(
 
 
 def RZZ(phi: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces a RZZ(phi) gate:
+    """Produce a RZZ gate.
 
         RZZ(phi) = [[ exp(-1j*phi/2),             0,             0,              0],
                     [              0, exp(1j*phi/2),             0,              0],
@@ -613,7 +611,7 @@ def RZZ(phi: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> G
 
 
 def RXX(phi: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces a RXX(phi) gate:
+    """Produce a RXX gate.
 
         RXX(phi) = [[     cos(phi/2),              0,              0, -1j*sin(phi/2)],
                     [              0,     cos(phi/2), -1j*sin(phi/2),              0],
@@ -629,7 +627,7 @@ def RXX(phi: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> G
 
 
 def RYY(phi: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> Gate:
-    """Produces a RYY(phi) gate:
+    """Produce a RYY gate.
 
         RYY(phi) = [[    cos(phi/2),              0,              0, 1j*sin(phi/2)],
                     [             0,     cos(phi/2), -1j*sin(phi/2),             0],
@@ -654,8 +652,7 @@ def RYY(phi: ParameterDesignator, q1: QubitDesignator, q2: QubitDesignator) -> G
 
 
 def RESET(qubit_index: Optional[QubitDesignator] = None) -> Union[Reset, ResetQubit]:
-    """
-    Reset all qubits or just one specific qubit.
+    """Reset all qubits or just one specific qubit.
 
     :param qubit_index: The qubit to reset.
         This can be a qubit's index, a Qubit, or a QubitPlaceholder.
@@ -689,7 +686,7 @@ def DECLARE(
     memory_type: str = "BIT",
     memory_size: int = 1,
     shared_region: Optional[str] = None,
-    offsets: Optional[Sequence[Tuple[int, str]]] = None,
+    offsets: Optional[Sequence[tuple[int, str]]] = None,
 ) -> Declare:
     return Declare(
         name=name,
@@ -701,8 +698,7 @@ def DECLARE(
 
 
 def MEASURE(qubit: QubitDesignator, classical_reg: Optional[MemoryReferenceDesignator]) -> Measurement:
-    """
-    Produce a MEASURE instruction.
+    """Produce a MEASURE instruction.
 
     :param qubit: The qubit to measure.
     :param classical_reg: The classical register to measure into, or None.
@@ -717,8 +713,7 @@ def MEASURE(qubit: QubitDesignator, classical_reg: Optional[MemoryReferenceDesig
 
 
 def NEG(classical_reg: MemoryReferenceDesignator) -> ClassicalNeg:
-    """
-    Produce a NEG instruction.
+    """Produce a NEG instruction.
 
     :param classical_reg: A classical memory address to modify.
     :return: A ClassicalNeg instance.
@@ -727,8 +722,7 @@ def NEG(classical_reg: MemoryReferenceDesignator) -> ClassicalNeg:
 
 
 def NOT(classical_reg: MemoryReferenceDesignator) -> ClassicalNot:
-    """
-    Produce a NOT instruction.
+    """Produce a NOT instruction.
 
     :param classical_reg: A classical register to modify.
     :return: A ClassicalNot instance.
@@ -739,8 +733,7 @@ def NOT(classical_reg: MemoryReferenceDesignator) -> ClassicalNot:
 def AND(
     classical_reg1: MemoryReferenceDesignator, classical_reg2: Union[MemoryReferenceDesignator, int]
 ) -> ClassicalAnd:
-    """
-    Produce an AND instruction.
+    """Produce an AND instruction.
 
     NOTE: The order of operands was reversed in pyQuil <=1.9 .
 
@@ -749,37 +742,38 @@ def AND(
     :return: A ClassicalAnd instance.
     """
     left, right = unpack_reg_val_pair(classical_reg1, classical_reg2)
-    assert isinstance(right, (MemoryReference, int))  # placate mypy
+    if not isinstance(right, (MemoryReference, int)):
+        raise TypeError("Right operand of AND must be a memory address or integer.")
     return ClassicalAnd(left, right)
 
 
 def IOR(
     classical_reg1: MemoryReferenceDesignator, classical_reg2: Union[MemoryReferenceDesignator, int]
 ) -> ClassicalInclusiveOr:
-    """
-    Produce an inclusive OR instruction.
+    """Produce an inclusive OR instruction.
 
     :param classical_reg1: The first classical register, which gets modified.
     :param classical_reg2: The second classical register or immediate value.
     :return: A ClassicalInclusiveOr instance.
     """
     left, right = unpack_reg_val_pair(classical_reg1, classical_reg2)
-    assert isinstance(right, (MemoryReference, int))  # placate mypy
+    if not isinstance(right, (MemoryReference, int)):
+        raise TypeError("Right operand of AND must be a memory address or integer.")
     return ClassicalInclusiveOr(left, right)
 
 
 def XOR(
     classical_reg1: MemoryReferenceDesignator, classical_reg2: Union[MemoryReferenceDesignator, int]
 ) -> ClassicalExclusiveOr:
-    """
-    Produce an exclusive OR instruction.
+    """Produce an exclusive OR instruction.
 
     :param classical_reg1: The first classical register, which gets modified.
     :param classical_reg2: The second classical register or immediate value.
     :return: A ClassicalExclusiveOr instance.
     """
     left, right = unpack_reg_val_pair(classical_reg1, classical_reg2)
-    assert isinstance(right, (MemoryReference, int))  # placate mypy
+    if not isinstance(right, (MemoryReference, int)):
+        raise TypeError("Right operand of AND must be a memory address or integer.")
     return ClassicalExclusiveOr(left, right)
 
 
@@ -787,8 +781,7 @@ def MOVE(
     classical_reg1: MemoryReferenceDesignator,
     classical_reg2: Union[MemoryReferenceDesignator, int, float],
 ) -> ClassicalMove:
-    """
-    Produce a MOVE instruction.
+    """Produce a MOVE instruction.
 
     :param classical_reg1: The first classical register, which gets modified.
     :param classical_reg2: The second classical register or immediate value.
@@ -799,8 +792,7 @@ def MOVE(
 
 
 def EXCHANGE(classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator) -> ClassicalExchange:
-    """
-    Produce an EXCHANGE instruction.
+    """Produce an EXCHANGE instruction.
 
     :param classical_reg1: The first classical register, which gets modified.
     :param classical_reg2: The second classical register, which gets modified.
@@ -814,8 +806,7 @@ def EXCHANGE(classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryRe
 def LOAD(
     target_reg: MemoryReferenceDesignator, region_name: str, offset_reg: MemoryReferenceDesignator
 ) -> ClassicalLoad:
-    """
-    Produce a LOAD instruction.
+    """Produce a LOAD instruction.
 
     :param target_reg: LOAD storage target.
     :param region_name: Named region of memory to load from.
@@ -830,8 +821,7 @@ def STORE(
     offset_reg: MemoryReferenceDesignator,
     source: Union[MemoryReferenceDesignator, int, float],
 ) -> ClassicalStore:
-    """
-    Produce a STORE instruction.
+    """Produce a STORE instruction.
 
     :param region_name: Named region of memory to store to.
     :param offset_reg: Offset into memory region. Must be a MemoryReference.
@@ -844,8 +834,7 @@ def STORE(
 
 
 def CONVERT(classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryReferenceDesignator) -> ClassicalConvert:
-    """
-    Produce a CONVERT instruction.
+    """Produce a CONVERT instruction.
 
     :param classical_reg1: MemoryReference to store to.
     :param classical_reg2: MemoryReference to read from.
@@ -855,8 +844,7 @@ def CONVERT(classical_reg1: MemoryReferenceDesignator, classical_reg2: MemoryRef
 
 
 def ADD(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDesignator, int, float]) -> ClassicalAdd:
-    """
-    Produce an ADD instruction.
+    """Produce an ADD instruction.
 
     :param classical_reg: Left operand for the arithmetic operation. Also serves as the store
         target.
@@ -868,8 +856,7 @@ def ADD(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDe
 
 
 def SUB(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDesignator, int, float]) -> ClassicalSub:
-    """
-    Produce a SUB instruction.
+    """Produce a SUB instruction.
 
     :param classical_reg: Left operand for the arithmetic operation. Also serves as the store
         target.
@@ -881,8 +868,7 @@ def SUB(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDe
 
 
 def MUL(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDesignator, int, float]) -> ClassicalMul:
-    """
-    Produce a MUL instruction.
+    """Produce a MUL instruction.
 
     :param classical_reg: Left operand for the arithmetic operation. Also serves as the store
         target.
@@ -894,8 +880,7 @@ def MUL(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDe
 
 
 def DIV(classical_reg: MemoryReferenceDesignator, right: Union[MemoryReferenceDesignator, int, float]) -> ClassicalDiv:
-    """
-    Produce an DIV instruction.
+    """Produce an DIV instruction.
 
     :param classical_reg: Left operand for the arithmetic operation. Also serves as the store
         target.
@@ -911,8 +896,7 @@ def EQ(
     classical_reg2: MemoryReferenceDesignator,
     classical_reg3: Union[MemoryReferenceDesignator, int, float],
 ) -> ClassicalEqual:
-    """
-    Produce an EQ instruction.
+    """Produce an EQ instruction.
 
     :param classical_reg1: Memory address to which to store the comparison result.
     :param classical_reg2: Left comparison operand.
@@ -931,8 +915,7 @@ def LT(
     classical_reg2: MemoryReferenceDesignator,
     classical_reg3: Union[MemoryReferenceDesignator, int, float],
 ) -> ClassicalLessThan:
-    """
-    Produce an LT instruction.
+    """Produce an LT instruction.
 
     :param classical_reg1: Memory address to which to store the comparison result.
     :param classical_reg2: Left comparison operand.
@@ -950,8 +933,7 @@ def LE(
     classical_reg2: MemoryReferenceDesignator,
     classical_reg3: Union[MemoryReferenceDesignator, int, float],
 ) -> ClassicalLessEqual:
-    """
-    Produce an LE instruction.
+    """Produce an LE instruction.
 
     :param classical_reg1: Memory address to which to store the comparison result.
     :param classical_reg2: Left comparison operand.
@@ -969,8 +951,7 @@ def GT(
     classical_reg2: MemoryReferenceDesignator,
     classical_reg3: Union[MemoryReferenceDesignator, int, float],
 ) -> ClassicalGreaterThan:
-    """
-    Produce an GT instruction.
+    """Produce an GT instruction.
 
     :param classical_reg1: Memory address to which to store the comparison result.
     :param classical_reg2: Left comparison operand.
@@ -988,8 +969,7 @@ def GE(
     classical_reg2: MemoryReferenceDesignator,
     classical_reg3: Union[MemoryReferenceDesignator, int, float],
 ) -> ClassicalGreaterEqual:
-    """
-    Produce an GE instruction.
+    """Produce an GE instruction.
 
     :param classical_reg1: Memory address to which to store the comparison result.
     :param classical_reg2: Left comparison operand.
@@ -1003,8 +983,7 @@ def GE(
 
 
 def PULSE(frame: Frame, waveform: Waveform, nonblocking: bool = False) -> Pulse:
-    """
-    Produce a PULSE instruction.
+    """Produce a PULSE instruction.
 
     :param frame: The frame on which to apply the pulse.
     :param waveform: The pulse waveform.
@@ -1015,8 +994,7 @@ def PULSE(frame: Frame, waveform: Waveform, nonblocking: bool = False) -> Pulse:
 
 
 def SET_FREQUENCY(frame: Frame, freq: ParameterDesignator) -> SetFrequency:
-    """
-    Produce a SET-FREQUENCY instruction.
+    """Produce a SET-FREQUENCY instruction.
 
     :param frame: The frame on which to set the frequency.
     :param freq: The frequency value, in Hz.
@@ -1026,8 +1004,7 @@ def SET_FREQUENCY(frame: Frame, freq: ParameterDesignator) -> SetFrequency:
 
 
 def SHIFT_FREQUENCY(frame: Frame, freq: ParameterDesignator) -> ShiftFrequency:
-    """
-    Produce a SHIFT-FREQUENCY instruction.
+    """Produce a SHIFT-FREQUENCY instruction.
 
     :param frame: The frame on which to shift the frequency.
     :param freq: The value, in Hz, to add to the existing frequency.
@@ -1037,8 +1014,7 @@ def SHIFT_FREQUENCY(frame: Frame, freq: ParameterDesignator) -> ShiftFrequency:
 
 
 def SET_PHASE(frame: Frame, phase: ParameterDesignator) -> SetPhase:
-    """
-    Produce a SET-PHASE instruction.
+    """Produce a SET-PHASE instruction.
 
     :param frame: The frame on which to set the phase.
     :param phase: The new phase value, in radians.
@@ -1048,8 +1024,7 @@ def SET_PHASE(frame: Frame, phase: ParameterDesignator) -> SetPhase:
 
 
 def SHIFT_PHASE(frame: Frame, phase: ParameterDesignator) -> ShiftPhase:
-    """
-    Produce a SHIFT-PHASE instruction.
+    """Produce a SHIFT-PHASE instruction.
 
     :param frame: The frame on which to shift the phase.
     :param phase: The value, in radians, to add to the existing phase.
@@ -1060,8 +1035,7 @@ def SHIFT_PHASE(frame: Frame, phase: ParameterDesignator) -> ShiftPhase:
 
 @versionadded(version="3.5.1", reason="The correct instruction is SWAP-PHASES, not SWAP-PHASE")
 def SWAP_PHASES(frameA: Frame, frameB: Frame) -> SwapPhases:
-    """
-    Produce a SWAP-PHASES instruction.
+    """Produce a SWAP-PHASES instruction.
 
     :param frameA: A frame.
     :param frameB: A frame.
@@ -1072,15 +1046,12 @@ def SWAP_PHASES(frameA: Frame, frameB: Frame) -> SwapPhases:
 
 @deprecated(version="3.5.1", reason="The correct instruction is SWAP-PHASES, not SWAP-PHASE")
 def SWAP_PHASE(frameA: Frame, frameB: Frame) -> SwapPhases:
-    """
-    Alias of :func:`SWAP_PHASES`.
-    """
+    """Alias of :func:`SWAP_PHASES`."""
     return SWAP_PHASES(frameA, frameB)
 
 
 def SET_SCALE(frame: Frame, scale: ParameterDesignator) -> SetScale:
-    """
-    Produce a SET-SCALE instruction.
+    """Produce a SET-SCALE instruction.
 
     :param frame: The frame on which to set the scale.
     :param scale: The scaling factor.
@@ -1095,8 +1066,7 @@ def CAPTURE(
     memory_region: MemoryReferenceDesignator,
     nonblocking: bool = False,
 ) -> Capture:
-    """
-    Produce a CAPTURE instruction.
+    """Produce a CAPTURE instruction.
 
     :param frame: The frame on which to capture an IQ value.
     :param kernel: The integrating kernel for the capture.
@@ -1114,8 +1084,7 @@ def RAW_CAPTURE(
     memory_region: MemoryReferenceDesignator,
     nonblocking: bool = False,
 ) -> RawCapture:
-    """
-    Produce a RAW-CAPTURE instruction.
+    """Produce a RAW-CAPTURE instruction.
 
     :param frame: The frame on which to capture raw values.
     :param duration: The duration of the capture, in seconds.
@@ -1133,8 +1102,7 @@ def RAW_CAPTURE(
 # type T.
 @no_type_check
 def DELAY(*args) -> Union[DelayFrames, DelayQubits]:
-    """
-    Produce a DELAY instruction.
+    """Produce a DELAY instruction.
 
     Note: There are two variants of DELAY. One applies to specific frames on some
     qubit, e.g. `DELAY 0 "rf" "ff" 1.0` delays the `"rf"` and `"ff"` frames on 0.
@@ -1166,8 +1134,7 @@ def DELAY(*args) -> Union[DelayFrames, DelayQubits]:
 
 
 def FENCE(*qubits: Union[int, Qubit, FormalArgument]) -> Union[FenceAll, Fence]:
-    """
-    Produce a FENCE instruction.
+    """Produce a FENCE instruction.
 
     Note: If no qubits are specified, then this is interpreted as a global FENCE.
 
diff --git a/pyquil/latex/__init__.py b/pyquil/latex/__init__.py
index 95a4f832e..ace730dfe 100644
--- a/pyquil/latex/__init__.py
+++ b/pyquil/latex/__init__.py
@@ -1,9 +1,11 @@
+"""Generate LaTeX diagrams from a ``Program``."""
+
 __all__ = [
     "DiagramSettings",
     "display",
     "to_latex",
 ]
 
-from pyquil.latex._main import to_latex
 from pyquil.latex._diagram import DiagramSettings
 from pyquil.latex._ipython import display
+from pyquil.latex._main import to_latex
diff --git a/pyquil/latex/_diagram.py b/pyquil/latex/_diagram.py
index d8d9e12c8..ca0867739 100644
--- a/pyquil/latex/_diagram.py
+++ b/pyquil/latex/_diagram.py
@@ -15,39 +15,38 @@
 ##############################################################################
 
 from collections import defaultdict
+from collections.abc import Iterable, Mapping, Sequence
 from dataclasses import dataclass, replace
-from typing import Iterable, List, Sequence, Mapping, Optional, Set, Tuple, cast
+from typing import Optional, cast
 from warnings import warn
 
 from pyquil.quil import Program
 from pyquil.quilatom import ParameterDesignator, QubitDesignator, format_parameter
 from pyquil.quilbase import (
     AbstractInstruction,
-    Wait,
-    ResetQubit,
-    JumpWhen,
-    JumpUnless,
-    Jump,
-    UnaryClassicalInstruction,
-    LogicalBinaryOp,
     ArithmeticBinaryOp,
-    ClassicalMove,
-    ClassicalExchange,
+    ClassicalComparison,
     ClassicalConvert,
+    ClassicalExchange,
     ClassicalLoad,
+    ClassicalMove,
     ClassicalStore,
-    ClassicalComparison,
-    Measurement,
     Gate,
+    Jump,
+    JumpUnless,
+    JumpWhen,
+    LogicalBinaryOp,
+    Measurement,
     Pragma,
+    ResetQubit,
+    UnaryClassicalInstruction,
+    Wait,
 )
 
 
 @dataclass
 class DiagramSettings:
-    """
-    Settings to control the layout and rendering of circuits.
-    """
+    """Settings to control the layout and rendering of circuits."""
 
     texify_numerical_constants: bool = True
     """
@@ -114,11 +113,11 @@ class DiagramSettings:
 
 
 def TIKZ_LEFT_KET(qubit: int) -> str:
-    return r"\lstick{{\ket{{q_{{{qubit}}}}}}}".format(qubit=qubit)
+    return rf"\lstick{{\ket{{q_{{{qubit}}}}}}}"
 
 
 def TIKZ_CONTROL(control: int, offset: int) -> str:
-    return r"\ctrl{{{offset}}}".format(offset=offset)
+    return rf"\ctrl{{{offset}}}"
 
 
 def TIKZ_CNOT_TARGET() -> str:
@@ -130,7 +129,7 @@ def TIKZ_CPHASE_TARGET() -> str:
 
 
 def TIKZ_SWAP(source: int, offset: int) -> str:
-    return r"\swap{{{offset}}}".format(offset=offset)
+    return rf"\swap{{{offset}}}"
 
 
 def TIKZ_SWAP_TARGET() -> str:
@@ -166,11 +165,11 @@ def TIKZ_GATE(
     cmd = r"\gate"
     rotations = ["RX", "RY", "RZ"]
     if settings and settings.abbreviate_controlled_rotations and name in rotations and params:
-        name = name[1] + "_{{{param}}}".format(param=_format_parameter(params[0], settings))
-        return cmd + "{{{name}}}".format(name=name)
+        name = name[1] + f"_{{{_format_parameter(params[0], settings)}}}"
+        return cmd + f"{{{name}}}"
     # now, handle the general case
     if size > 1:
-        cmd += "[wires={size}]".format(size=size)
+        cmd += f"[wires={size}]"
     # TeXify names
     if name in ["RX", "RY", "RZ"]:
         name = name[0] + "_" + name[1].lower()
@@ -178,14 +177,14 @@ def TIKZ_GATE(
         name += r"^{\dagger}"
     if params:
         name += _format_parameters(params, settings)
-    return cmd + "{{{name}}}".format(name=name)
+    return cmd + f"{{{name}}}"
 
 
 def TIKZ_GATE_GROUP(qubits: Sequence[int], width: int, label: str) -> str:
     num_qubits = max(qubits) - min(qubits) + 1
     return (
-        "\\gategroup[{qubits},steps={width},style={{dashed, rounded corners,"
-        "fill=blue!20, inner xsep=2pt}}, background]{{{label}}}".format(qubits=num_qubits, width=width, label=label)
+        f"\\gategroup[{num_qubits},steps={width},style={{dashed, rounded corners,"
+        f"fill=blue!20, inner xsep=2pt}}, background]{{{label}}}"
     )
 
 
@@ -197,8 +196,7 @@ def TIKZ_GATE_GROUP(qubits: Sequence[int], width: int, label: str) -> str:
 
 
 class DiagramState:
-    """
-    A representation of a circuit diagram.
+    """A representation of a circuit diagram.
 
     This maintains an ordered list of qubits, and for each qubit a 'line': that is, a list of
     TikZ operators.
@@ -206,35 +204,25 @@ class DiagramState:
 
     def __init__(self, qubits: Sequence[int]):
         self.qubits = qubits
-        self.lines: Mapping[int, List[str]] = defaultdict(list)
+        self.lines: Mapping[int, list[str]] = defaultdict(list)
 
     def extend_lines_to_common_edge(self, qubits: Iterable[int], offset: int = 0) -> None:
-        """
-        Add NOP operations on the lines associated with the given qubits, until
-        all lines are of the same width.
-        """
+        """Add NOP operations on the lines associated with the given qubits, until all lines are of the same width."""
         max_width = max(self.width(q) for q in qubits) + offset
         for q in qubits:
             while self.width(q) < max_width:
                 self.append(q, TIKZ_NOP())
 
     def width(self, qubit: int) -> int:
-        """
-        The width of the diagram, in terms of the number of operations, on the
-        specified qubit line.
-        """
+        """Calculate the width of the diagram, in terms of the number of operations, on the specified qubit line."""
         return len(self.lines[qubit])
 
     def append(self, qubit: int, op: str) -> None:
-        """
-        Add an operation to the rightmost edge of the specified qubit line.
-        """
+        """Add an operation to the rightmost edge of the specified qubit line."""
         self.lines[qubit].append(op)
 
     def append_diagram(self, diagram: "DiagramState", group: Optional[str] = None) -> "DiagramState":
-        """
-        Add all operations represented by the given diagram to their
-        corresponding qubit lines in this diagram.
+        """Add all operations represented by the given diagram to their corresponding qubit lines in this diagram.
 
         If group is not None, then a TIKZ_GATE_GROUP is created with the label indicated by group.
         """
@@ -259,38 +247,33 @@ def append_diagram(self, diagram: "DiagramState", group: Optional[str] = None) -
             self.lines[corner_row][corner_col] += " " + TIKZ_GATE_GROUP(grouped_qubits, group_width, group)
         return self
 
-    def interval(self, low: int, high: int) -> List[int]:
-        """
-        All qubits in the diagram, from low to high, inclusive.
-        """
+    def interval(self, low: int, high: int) -> list[int]:
+        """All qubits in the diagram, from low to high, inclusive."""
         full_interval = range(low, high + 1)
         qubits = list(set(full_interval) & set(self.qubits))
         return sorted(qubits)
 
     def is_interval(self, qubits: Sequence[int]) -> bool:
-        """
-        Do the specified qubits correspond to an interval in this diagram?
-        """
+        """Return True if the specified qubits correspond to an interval in this diagram, False otherwise."""
         return qubits == self.interval(min(qubits), max(qubits))
 
 
 def split_on_terminal_measures(
     program: Program,
-) -> Tuple[List[AbstractInstruction], List[AbstractInstruction]]:
-    """
-    Split a program into two lists of instructions:
+) -> tuple[list[AbstractInstruction], list[AbstractInstruction]]:
+    """Split a program into two lists of instructions.
 
-    1. A set of measurement instructions occuring as the final operation on their qubit.
+    1. A set of measurement instructions occurring as the final operation on their qubit.
     2. The rest.
     """
     # handle the easy case explicitly (mainly to avoid warning when we can avoid it)
     if not any(isinstance(instr, Measurement) for instr in program.instructions):
         return [], program.instructions
 
-    seen_qubits: Set[QubitDesignator] = set()
+    seen_qubits: set[QubitDesignator] = set()
 
-    measures: List[AbstractInstruction] = []
-    remaining: List[AbstractInstruction] = []
+    measures: list[AbstractInstruction] = []
+    remaining: list[AbstractInstruction] = []
     in_group = False
     for instr in reversed(program.instructions):
         if not in_group and isinstance(instr, Measurement) and instr.qubit not in seen_qubits:
@@ -302,7 +285,10 @@ def split_on_terminal_measures(
                 seen_qubits |= set(instr.get_qubit_indices() or {})
             elif isinstance(instr, Pragma):
                 if instr.command == PRAGMA_END_GROUP:
-                    warn("Alignment of terminal MEASURE operations may" "conflict with gate group declaration.")
+                    warn(
+                        "Alignment of terminal MEASURE operations may" "conflict with gate group declaration.",
+                        stacklevel=2,
+                    )
                     in_group = True
                 elif instr.command == PRAGMA_BEGIN_GROUP:
                     in_group = False
@@ -310,8 +296,7 @@ def split_on_terminal_measures(
 
 
 class DiagramBuilder:
-    """
-    Constructs DiagramStates from a given circuit and settings.
+    """Constructs DiagramStates from a given circuit and settings.
 
     This is essentially a state machine, represented by a few instance variables and some mutually
     recursive methods.
@@ -321,7 +306,7 @@ def __init__(self, circuit: Program, settings: DiagramSettings):
         self.circuit = circuit
         self.settings = settings
         # instructions currently being processed
-        self.working_instructions: Optional[List[AbstractInstruction]] = None
+        self.working_instructions: Optional[list[AbstractInstruction]] = None
         # index into working instructions. we maintain the invariant that
         # working_instructions[0:index] has been processed, with the diagram
         # updated accordingly
@@ -330,10 +315,8 @@ def __init__(self, circuit: Program, settings: DiagramSettings):
         self.diagram: Optional[DiagramState] = None
 
     def build(self) -> DiagramState:
-        """
-        Actually build the diagram.
-        """
-        qubits = cast(Set[int], self.circuit.get_qubits(indices=True))
+        """Build the diagram."""
+        qubits = cast(set[int], self.circuit.get_qubits(indices=True))
         all_qubits = range(min(qubits), max(qubits) + 1) if self.settings.impute_missing_qubits else sorted(qubits)
         self.diagram = DiagramState(all_qubits)
 
@@ -359,12 +342,12 @@ def build(self) -> DiagramState:
             if isinstance(instr, Pragma) and instr.command == PRAGMA_BEGIN_GROUP:
                 self._build_group()
             elif isinstance(instr, Pragma) and instr.command == PRAGMA_END_GROUP:
-                raise ValueError("PRAGMA {} found without matching {}.".format(PRAGMA_END_GROUP, PRAGMA_BEGIN_GROUP))
+                raise ValueError(f"PRAGMA {PRAGMA_END_GROUP} found without matching {PRAGMA_BEGIN_GROUP}.")
             elif isinstance(instr, Measurement):
                 self._build_measure()
             elif isinstance(instr, Gate):
                 if "FORKED" in instr.modifiers:
-                    raise ValueError("LaTeX output does not currently support" "FORKED modifiers: {}.".format(instr))
+                    raise ValueError("LaTeX output does not currently support" f"FORKED modifiers: {instr}.")
                 # the easy case is 1q operations
                 if len(instr.qubits) == 1:
                     self._build_1q_unitary()
@@ -374,9 +357,7 @@ def build(self) -> DiagramState:
                     else:
                         self._build_generic_unitary()
             elif isinstance(instr, UNSUPPORTED_INSTRUCTION_CLASSES):
-                raise ValueError(
-                    "LaTeX output does not currently support" "the following instruction: {}".format(instr.out())
-                )
+                raise ValueError("LaTeX output does not currently support" f"the following instruction: {instr.out()}")
             else:
                 self.index += 1
 
@@ -394,14 +375,15 @@ def build(self) -> DiagramState:
         return self.diagram
 
     def _build_group(self) -> None:
-        """
-        Update the partial diagram with the subcircuit delimited by the grouping PRAGMA.
+        """Update the partial diagram with the subcircuit delimited by the grouping PRAGMA.
 
         Advances the index beyond the ending pragma.
         """
-        assert self.working_instructions is not None
+        if self.working_instructions is None:
+            raise RuntimeError("Internal error: working_instructions is None.")
         instr = self.working_instructions[self.index]
-        assert isinstance(instr, Pragma)
+        if not isinstance(instr, Pragma):
+            raise RuntimeError("Internal error: expected a PRAGMA instruction.")
         if len(instr.args) != 0:
             raise ValueError(f"PRAGMA {PRAGMA_BEGIN_GROUP} expected a freeform string, or nothing at all.")
         start = self.index + 1
@@ -415,39 +397,43 @@ def _build_group(self) -> None:
                 subcircuit = Program(*self.working_instructions[start:j])
                 block = DiagramBuilder(subcircuit, block_settings).build()
                 block_name = instr.freeform_string if instr.freeform_string else ""
-                assert self.diagram is not None
+                if self.diagram is None:
+                    raise RuntimeError("Internal error: expected diagram to exist.")
                 self.diagram.append_diagram(block, group=block_name)
                 # advance to the instruction following this one
                 self.index = j + 1
                 return
 
-        raise ValueError("Unable to find PRAGMA {} matching {}.".format(PRAGMA_END_GROUP, instr))
+        raise ValueError(f"Unable to find PRAGMA {PRAGMA_END_GROUP} matching {instr}.")
 
     def _build_measure(self) -> None:
-        """
-        Update the partial diagram with a measurement operation.
+        """Update the partial diagram with a measurement operation.
 
         Advances the index by one.
         """
-        assert self.working_instructions is not None
+        if self.working_instructions is None:
+            raise RuntimeError("Internal error: working_instructions is None.")
         instr = self.working_instructions[self.index]
-        assert isinstance(instr, Measurement)
-        assert self.diagram is not None
+        if not isinstance(instr, Measurement):
+            raise RuntimeError("Internal error: expected a Measurement instruction.")
+        if self.diagram is None:
+            raise RuntimeError("Internal error: expected diagram to exist.")
         self.diagram.append(instr.get_qubit_indices().pop(), TIKZ_MEASURE())
         self.index += 1
 
     def _build_custom_source_target_op(self) -> None:
-        """
-        Update the partial diagram with a single operation involving a source and a target
-        (e.g. a controlled gate, a swap).
+        """Update the partial diagram with a single operation involving a source and a target (e.g. a controlled gate).
 
         Advances the index by one.
         """
-        assert self.working_instructions is not None
+        if self.working_instructions is None:
+            raise RuntimeError("Internal error: working_instructions is None.")
         instr = self.working_instructions[self.index]
-        assert isinstance(instr, Gate)
+        if not isinstance(instr, Gate):
+            raise RuntimeError("Internal error: expected a Gate instruction, got ({type(instr)}).")
         source, target = qubit_indices(instr)
-        assert self.diagram is not None
+        if self.diagram is None:
+            raise RuntimeError("Internal error: expected diagram to exist.")
         displaced = self.diagram.interval(min(source, target), max(source, target))
         self.diagram.extend_lines_to_common_edge(displaced)
         source_op, target_op = SOURCE_TARGET_OP[instr.name]
@@ -458,18 +444,19 @@ def _build_custom_source_target_op(self) -> None:
         self.index += 1
 
     def _build_1q_unitary(self) -> None:
-        """
-        Update the partial diagram with a 1Q gate.
+        """Update the partial diagram with a 1Q gate.
 
         Advances the index by one.
         """
-        assert self.working_instructions is not None
+        if self.working_instructions is None:
+            raise RuntimeError("Internal error: working_instructions is None.")
         instr = self.working_instructions[self.index]
-        assert isinstance(instr, Gate)
+        if not isinstance(instr, Gate):
+            raise RuntimeError("Internal error: expected a Gate instruction, got ({type(instr)}).")
         qubits = qubit_indices(instr)
         dagger = sum(m == "DAGGER" for m in instr.modifiers) % 2 == 1
-
-        assert self.diagram is not None
+        if self.diagram is None:
+            raise RuntimeError("Internal error: expected diagram to exist.")
         self.diagram.append(
             qubits[0],
             TIKZ_GATE(instr.name, params=instr.params, dagger=dagger, settings=self.settings),
@@ -477,19 +464,20 @@ def _build_1q_unitary(self) -> None:
         self.index += 1
 
     def _build_generic_unitary(self) -> None:
-        """
-        Update the partial diagram with a unitary operation.
+        """Update the partial diagram with a unitary operation.
 
         Advances the index by one.
         """
-        assert self.working_instructions is not None
+        if self.working_instructions is None:
+            raise RuntimeError("Internal error: working_instructions is None.")
         instr = self.working_instructions[self.index]
-        assert isinstance(instr, Gate)
+        if not isinstance(instr, Gate):
+            raise RuntimeError("Internal error: expected a Gate instruction, got ({type(instr)}).")
         qubits = qubit_indices(instr)
         dagger = sum(m == "DAGGER" for m in instr.modifiers) % 2 == 1
         controls = sum(m == "CONTROLLED" for m in instr.modifiers)
-
-        assert self.diagram is not None
+        if self.diagram is None:
+            raise RuntimeError("Internal error: expected diagram to exist.")
         self.diagram.extend_lines_to_common_edge(qubits)
         control_qubits = qubits[:controls]
         # sort the target qubit list because the first qubit indicates wire placement on the diagram
@@ -512,10 +500,8 @@ def _build_generic_unitary(self) -> None:
         self.index += 1
 
 
-def qubit_indices(instr: AbstractInstruction) -> List[int]:
-    """
-    Get a list of indices associated with the given instruction.
-    """
+def qubit_indices(instr: AbstractInstruction) -> list[int]:
+    """Get a list of indices associated with the given instruction."""
     if isinstance(instr, (Measurement, Gate)):
         return list(instr.get_qubit_indices())
     else:
diff --git a/pyquil/latex/_ipython.py b/pyquil/latex/_ipython.py
index 05a2be0cd..18a7cb23c 100644
--- a/pyquil/latex/_ipython.py
+++ b/pyquil/latex/_ipython.py
@@ -22,14 +22,13 @@
 
 from IPython.display import Image
 
-from pyquil.latex._main import to_latex
 from pyquil.latex._diagram import DiagramSettings
+from pyquil.latex._main import to_latex
 from pyquil.quil import Program
 
 
 def display(circuit: Program, settings: Optional[DiagramSettings] = None, **image_options: Any) -> Image:
-    """
-    Displays a PyQuil circuit as an IPython image object.
+    """Display a PyQuil circuit as an IPython image object.
 
     .. note::
 
@@ -60,11 +59,11 @@ def display(circuit: Program, settings: Optional[DiagramSettings] = None, **imag
             texfile.write(to_latex(circuit, settings))
 
         result = subprocess.run(
-            [pdflatex_path, "-halt-on-error", "-output-directory", tmpdir, texfile.name],
+            [pdflatex_path, "-halt-on-error", "-output-directory", tmpdir, texfile.name],  # noqa: S603 - valid input
             stdout=subprocess.PIPE,
         )
         if result.returncode != 0:
-            msg = "'pdflatex' terminated with return code {}.".format(result.returncode)
+            msg = f"'pdflatex' terminated with return code {result.returncode}."
             # NOTE: pdflatex writes all error messages to stdout
             if result.stdout:
                 msg += " Transcript:\n\n" + result.stdout.decode("utf-8")
@@ -73,9 +72,9 @@ def display(circuit: Program, settings: Optional[DiagramSettings] = None, **imag
         png = os.path.join(tmpdir, "diagram.png")
         pdf = os.path.join(tmpdir, "diagram.pdf")
 
-        result = subprocess.run([convert_path, "-density", "300", pdf, png], stderr=subprocess.PIPE)
+        result = subprocess.run([convert_path, "-density", "300", pdf, png], stderr=subprocess.PIPE)  # noqa: S603
         if result.returncode != 0:
-            msg = "'convert' terminated with return code {}.".format(result.returncode)
+            msg = f"'convert' terminated with return code {result.returncode}."
             if result.stderr:
                 msg += " Transcript:\n\n" + result.stderr.decode("utf-8")
             raise RuntimeError(msg)
diff --git a/pyquil/latex/_main.py b/pyquil/latex/_main.py
index 6294aa6b4..c45d91646 100644
--- a/pyquil/latex/_main.py
+++ b/pyquil/latex/_main.py
@@ -13,9 +13,8 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-"""
-The main entry point to the LaTeX generation functionality in pyQuil.
-"""
+"""The main entry point to the LaTeX generation functionality in pyQuil."""
+
 from typing import Optional
 
 from pyquil.latex._diagram import DiagramBuilder, DiagramSettings
@@ -23,8 +22,7 @@
 
 
 def to_latex(circuit: Program, settings: Optional[DiagramSettings] = None) -> str:
-    """
-    Translates a given pyQuil Program to a TikZ picture in a LaTeX document.
+    """Translate a given pyQuil Program to a TikZ picture in a LaTeX document.
 
     Here are some high points of the generation procedure (see ``pyquil/latex/_diagram.py``):
 
@@ -61,8 +59,7 @@ def to_latex(circuit: Program, settings: Optional[DiagramSettings] = None) -> st
 
 
 def header() -> str:
-    """
-    Writes the LaTeX header using the settings file.
+    """Write the LaTeX header using the settings file.
 
     The header includes all packages and defines all tikz styles.
 
@@ -81,8 +78,7 @@ def header() -> str:
 
 
 def footer() -> str:
-    """
-    Return the footer of the LaTeX document.
+    """Return the footer of the LaTeX document.
 
     :return: LaTeX document footer.
     """
@@ -90,16 +86,13 @@ def footer() -> str:
 
 
 def body(circuit: Program, settings: DiagramSettings) -> str:
-    """
-    Return the body of the LaTeX document, including the entire circuit in
-    TikZ format.
+    """Return the body of the LaTeX document, including the entire circuit in TikZ format.
 
     :param circuit: The circuit to be drawn, represented as a pyquil program.
     :param settings: Options controlling rendering and layout.
 
     :return: LaTeX string to draw the entire circuit.
     """
-
     diagram = DiagramBuilder(circuit, settings).build()
 
     # flush lines
diff --git a/pyquil/latex/latex_generation.py b/pyquil/latex/latex_generation.py
index cea87c5d7..815c0ae96 100644
--- a/pyquil/latex/latex_generation.py
+++ b/pyquil/latex/latex_generation.py
@@ -13,20 +13,25 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
+"""Convert a ``Program`` to a LaTeX quantum circuit diagram.
+
+Note: this is a deprecated module: Import from pyquil.latex instead.
+"""
 
-import warnings
 from typing import Optional
 
+from deprecated.classic import deprecated
+
 from pyquil.latex._diagram import DiagramSettings
 from pyquil.quil import Program
 
 
+@deprecated(
+    version="4.0",
+    reason="This module has been moved -- please import it as 'from pyquil.latex import to_latex' going forward",
+)
 def to_latex(circuit: Program, settings: Optional[DiagramSettings] = None) -> str:
+    """Produce a circuit diagram in LaTeX for a given pyQuil Program."""
     from pyquil.latex._main import to_latex
 
-    warnings.warn(
-        '"pyquil.latex.latex_generation.to_latex" has been moved -- please import it'
-        'as "from pyquil.latex import to_latex going forward"',
-        FutureWarning,
-    )
     return to_latex(circuit, settings)
diff --git a/pyquil/noise.py b/pyquil/noise.py
index 5651dae89..d794c4f78 100644
--- a/pyquil/noise.py
+++ b/pyquil/noise.py
@@ -13,24 +13,24 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-"""
-Module for creating and verifying noisy gate and readout definitions.
-"""
+"""Module for creating and verifying noisy gate and readout definitions."""
+
 import sys
 from collections import namedtuple
-from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, TYPE_CHECKING, Union, cast
+from collections.abc import Iterable, Sequence
+from typing import TYPE_CHECKING, Any, Optional, Union, cast
 
 import numpy as np
 
 from pyquil.external.rpcq import CompilerISA
-from pyquil.gates import I, RX, MEASURE
+from pyquil.gates import MEASURE, RX, I
 from pyquil.noise_gates import _get_qvm_noise_supported_gates
-from pyquil.quilatom import MemoryReference, format_parameter, ParameterDesignator
-from pyquil.quilbase import Pragma, Gate, Declare
+from pyquil.quilatom import MemoryReference, ParameterDesignator, format_parameter
+from pyquil.quilbase import Declare, Gate, Pragma
 
 if TYPE_CHECKING:
-    from pyquil.quil import Program
     from pyquil.api import QuantumComputer as PyquilApiQuantumComputer
+    from pyquil.quil import Program
 
 INFINITY = float("inf")
 "Used for infinite coherence times."
@@ -39,8 +39,7 @@
 
 
 class KrausModel(_KrausModel):
-    """
-    Encapsulate a single gate's noise model.
+    """Encapsulate a single gate's noise model.
 
     :ivar str gate: The name of the gate.
     :ivar Sequence[float] params: Optional parameters for the gate.
@@ -51,9 +50,8 @@ class KrausModel(_KrausModel):
     """
 
     @staticmethod
-    def unpack_kraus_matrix(m: Union[List[Any], np.ndarray]) -> np.ndarray:
-        """
-        Helper to optionally unpack a JSON compatible representation of a complex Kraus matrix.
+    def unpack_kraus_matrix(m: Union[list[Any], np.ndarray]) -> np.ndarray:
+        """Unpack a JSON compatible representation of a complex Kraus matrix.
 
         :param m: The representation of a Kraus operator. Either a complex
             square matrix (as numpy array or nested lists) or a JSON-able pair of real matrices
@@ -69,9 +67,8 @@ def unpack_kraus_matrix(m: Union[List[Any], np.ndarray]) -> np.ndarray:
             raise ValueError("Need square matrix.")
         return matrix
 
-    def to_dict(self) -> Dict[str, Any]:
-        """
-        Create a dictionary representation of a KrausModel.
+    def to_dict(self) -> dict[str, Any]:
+        """Create a dictionary representation of a KrausModel.
 
         For example::
 
@@ -79,26 +76,31 @@ def to_dict(self) -> Dict[str, Any]:
                 "gate": "RX",
                 "params": np.pi,
                 "targets": [0],
-                "kraus_ops": [            # In this example single Kraus op = ideal RX(pi) gate
-                    [[[0,   0],           # element-wise real part of matrix
-                      [0,   0]],
-                      [[0, -1],           # element-wise imaginary part of matrix
-                      [-1, 0]]]
+                "kraus_ops": [  # In this example single Kraus op = ideal RX(pi) gate
+                    [
+                        [
+                            [0, 0],  # element-wise real part of matrix
+                            [0, 0],
+                        ],
+                        [
+                            [0, -1],  # element-wise imaginary part of matrix
+                            [-1, 0],
+                        ],
+                    ]
                 ],
-                "fidelity": 1.0
+                "fidelity": 1.0,
             }
 
         :return: A JSON compatible dictionary representation.
-        :rtype: Dict[str,Any]
+        :rtype: dict[str,Any]
         """
         res = self._asdict()
         res["kraus_ops"] = [[k.real.tolist(), k.imag.tolist()] for k in self.kraus_ops]
         return res
 
     @staticmethod
-    def from_dict(d: Dict[str, Any]) -> "KrausModel":
-        """
-        Recreate a KrausModel from the dictionary representation.
+    def from_dict(d: dict[str, Any]) -> "KrausModel":
+        """Recreate a KrausModel from the dictionary representation.
 
         :param d: The dictionary representing the KrausModel. See `to_dict` for an
             example.
@@ -108,27 +110,23 @@ def from_dict(d: Dict[str, Any]) -> "KrausModel":
         return KrausModel(d["gate"], d["params"], d["targets"], kraus_ops, d["fidelity"])
 
     def __eq__(self, other: object) -> bool:
+        """Return True if both KrausModels are equal."""
         return isinstance(other, KrausModel) and self.to_dict() == other.to_dict()
 
-    def __neq__(self, other: object) -> bool:
-        return not self.__eq__(other)
-
 
 _NoiseModel = namedtuple("_NoiseModel", ["gates", "assignment_probs"])
 
 
 class NoiseModel(_NoiseModel):
-    """
-    Encapsulate the QPU noise model containing information about the noisy gates.
+    """Encapsulate the QPU noise model containing information about the noisy gates.
 
     :ivar Sequence[KrausModel] gates: The tomographic estimates of all gates.
-    :ivar Dict[int,np.array] assignment_probs: The single qubit readout assignment
+    :ivar dict[int,np.array] assignment_probs: The single qubit readout assignment
         probability matrices keyed by qubit id.
     """
 
-    def to_dict(self) -> Dict[str, Any]:
-        """
-        Create a JSON serializable representation of the noise model.
+    def to_dict(self) -> dict[str, Any]:
+        """Create a JSON serializable representation of the noise model.
 
         For example::
 
@@ -152,9 +150,8 @@ def to_dict(self) -> Dict[str, Any]:
         }
 
     @staticmethod
-    def from_dict(d: Dict[str, Any]) -> "NoiseModel":
-        """
-        Re-create the noise model from a dictionary representation.
+    def from_dict(d: dict[str, Any]) -> "NoiseModel":
+        """Re-create the noise model from a dictionary representation.
 
         :param d: The dictionary representation.
         :return: The restored noise model.
@@ -164,9 +161,8 @@ def from_dict(d: Dict[str, Any]) -> "NoiseModel":
             assignment_probs={int(qid): np.array(a) for qid, a in d["assignment_probs"].items()},
         )
 
-    def gates_by_name(self, name: str) -> List[KrausModel]:
-        """
-        Return all defined noisy gates of a particular gate name.
+    def gates_by_name(self, name: str) -> list[KrausModel]:
+        """Return all defined noisy gates of a particular gate name.
 
         :param str name: The gate name.
         :return: A list of noise models representing that gate.
@@ -174,38 +170,33 @@ def gates_by_name(self, name: str) -> List[KrausModel]:
         return [g for g in self.gates if g.gate == name]
 
     def __eq__(self, other: object) -> bool:
+        """Return True if NoiseModels are equal."""
         return isinstance(other, NoiseModel) and self.to_dict() == other.to_dict()
 
-    def __neq__(self, other: object) -> bool:
-        return not self.__eq__(other)
-
 
 def _check_kraus_ops(n: int, kraus_ops: Sequence[np.ndarray]) -> None:
-    """
-    Verify that the Kraus operators are of the correct shape and satisfy the correct normalization.
+    """Verify that the Kraus operators are of the correct shape and satisfy the correct normalization.
 
     :param n: Number of qubits
     :param kraus_ops: The Kraus operators as numpy.ndarrays.
     """
     for k in kraus_ops:
         if not np.shape(k) == (2**n, 2**n):
-            raise ValueError("Kraus operators for {0} qubits must have shape {1}x{1}: {2}".format(n, 2**n, k))
+            raise ValueError(f"Kraus operators for {n} qubits must have shape {2**n}x{2**n}: {k}")
 
     kdk_sum = sum(np.transpose(k).conjugate().dot(k) for k in kraus_ops)
     if not np.allclose(kdk_sum, np.eye(2**n), atol=1e-3):
-        raise ValueError("Kraus operator not correctly normalized: sum_j K_j^*K_j == {}".format(kdk_sum))
+        raise ValueError(f"Kraus operator not correctly normalized: sum_j K_j^*K_j == {kdk_sum}")
 
 
-def _create_kraus_pragmas(name: str, qubit_indices: Sequence[int], kraus_ops: Sequence[np.ndarray]) -> List[Pragma]:
-    """
-    Generate the pragmas to define a Kraus map for a specific gate on some qubits.
+def _create_kraus_pragmas(name: str, qubit_indices: Sequence[int], kraus_ops: Sequence[np.ndarray]) -> list[Pragma]:
+    """Generate the pragmas to define a Kraus map for a specific gate on some qubits.
 
     :param name: The name of the gate.
     :param qubit_indices: The qubits
     :param kraus_ops: The Kraus operators as matrices.
     :return: A QUIL string with PRAGMA ADD-KRAUS ... statements.
     """
-
     pragmas = [
         Pragma(
             "ADD-KRAUS",
@@ -219,9 +210,8 @@ def _create_kraus_pragmas(name: str, qubit_indices: Sequence[int], kraus_ops: Se
 
 def append_kraus_to_gate(
     kraus_ops: Sequence[np.ndarray], gate_matrix: np.ndarray
-) -> List[Union[np.number, np.ndarray]]:
-    """
-    Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``.
+) -> list[Union[np.number, np.ndarray]]:
+    """Follow a gate ``gate_matrix`` by a Kraus map described by ``kraus_ops``.
 
     :param kraus_ops: The Kraus operators.
     :param gate_matrix: The unitary gate.
@@ -230,9 +220,8 @@ def append_kraus_to_gate(
     return [kj.dot(gate_matrix) for kj in kraus_ops]
 
 
-def pauli_kraus_map(probabilities: Sequence[float]) -> List[np.ndarray]:
-    r"""
-    Generate the Kraus operators corresponding to a pauli channel.
+def pauli_kraus_map(probabilities: Sequence[float]) -> list[np.ndarray]:
+    r"""Generate the Kraus operators corresponding to a pauli channel.
 
     :params probabilities: The 4^num_qubits list of probabilities specifying the
         desired pauli channel. There should be either 4 or 16 probabilities specified in the
@@ -270,10 +259,8 @@ def pauli_kraus_map(probabilities: Sequence[float]) -> List[np.ndarray]:
     return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)]
 
 
-def damping_kraus_map(p: float = 0.10) -> List[np.ndarray]:
-    """
-    Generate the Kraus operators corresponding to an amplitude damping
-    noise channel.
+def damping_kraus_map(p: float = 0.10) -> list[np.ndarray]:
+    """Generate the Kraus operators corresponding to an amplitude damping noise channel.
 
     :param p: The one-step damping probability.
     :return: A list [k1, k2] of the Kraus operators that parametrize the map.
@@ -285,9 +272,8 @@ def damping_kraus_map(p: float = 0.10) -> List[np.ndarray]:
     return [residual_kraus, damping_op]
 
 
-def dephasing_kraus_map(p: float = 0.10) -> List[np.ndarray]:
-    """
-    Generate the Kraus operators corresponding to a dephasing channel.
+def dephasing_kraus_map(p: float = 0.10) -> list[np.ndarray]:
+    """Generate the Kraus operators corresponding to a dephasing channel.
 
     :params float p: The one-step dephasing probability.
     :return: A list [k1, k2] of the Kraus operators that parametrize the map.
@@ -296,10 +282,8 @@ def dephasing_kraus_map(p: float = 0.10) -> List[np.ndarray]:
     return [np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * np.diag([1, -1])]
 
 
-def tensor_kraus_maps(k1: List[np.ndarray], k2: List[np.ndarray]) -> List[np.ndarray]:
-    """
-    Generate the Kraus map corresponding to the composition
-    of two maps on different qubits.
+def tensor_kraus_maps(k1: list[np.ndarray], k2: list[np.ndarray]) -> list[np.ndarray]:
+    """Generate the Kraus map corresponding to the composition of two maps on different qubits.
 
     :param k1: The Kraus operators for the first qubit.
     :param k2: The Kraus operators for the second qubit.
@@ -308,11 +292,8 @@ def tensor_kraus_maps(k1: List[np.ndarray], k2: List[np.ndarray]) -> List[np.nda
     return [np.kron(k1j, k2l) for k1j in k1 for k2l in k2]
 
 
-def combine_kraus_maps(k1: List[np.ndarray], k2: List[np.ndarray]) -> List[np.ndarray]:
-    """
-    Generate the Kraus map corresponding to the composition
-    of two maps on the same qubits with k1 being applied to the state
-    after k2.
+def combine_kraus_maps(k1: list[np.ndarray], k2: list[np.ndarray]) -> list[np.ndarray]:
+    """Generate the Kraus map for two composed maps, with k1 applied after k2 on the same qubits.
 
     :param k1: The list of Kraus operators that are applied second.
     :param k2: The list of Kraus operators that are applied first.
@@ -321,18 +302,16 @@ def combine_kraus_maps(k1: List[np.ndarray], k2: List[np.ndarray]) -> List[np.nd
     return [np.dot(k1j, k2l) for k1j in k1 for k2l in k2]
 
 
-def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> List[np.ndarray]:
-    """
-    Generate the Kraus map corresponding to the composition
-    of a dephasing channel followed by an amplitude damping channel.
+def damping_after_dephasing(T1: float, T2: float, gate_time: float) -> list[np.ndarray]:
+    """Generate the Kraus map for a dephasing channel followed by an amplitude damping channel.
 
     :param T1: The amplitude damping time
     :param T2: The dephasing time
     :param gate_time: The gate duration.
     :return: A list of Kraus operators.
     """
-    assert T1 >= 0
-    assert T2 >= 0
+    if T1 < 0 or T2 < 0:
+        raise ValueError("T1 and T2 must be non-negative.")
 
     if T1 != INFINITY:
         damping = damping_kraus_map(p=1 - np.exp(-float(gate_time) / float(T1)))
@@ -363,9 +342,8 @@ class NoisyGateUndefined(Exception):
     pass
 
 
-def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> Tuple[np.ndarray, str]:
-    """
-    Look up the numerical gate representation and a proposed 'noisy' name.
+def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> tuple[np.ndarray, str]:
+    """Look up the numerical gate representation and a proposed 'noisy' name.
 
     :param gate_name: The Quil gate name
     :param params: The gate parameters.
@@ -374,7 +352,8 @@ def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> Tup
     """
     params = tuple(params)
     if gate_name == "I":
-        assert params == ()
+        if params != ():
+            raise ValueError(f"Identity gate does not take parameters: {params}")
         return np.eye(2), "NOISY-I"
     if gate_name == "RX":
         (angle,) = params
@@ -390,18 +369,17 @@ def get_noisy_gate(gate_name: str, params: Iterable[ParameterDesignator]) -> Tup
         elif np.isclose(angle, -np.pi, atol=ANGLE_TOLERANCE):
             return np.array([[0, 1j], [1j, 0]]), "NOISY-RX-MINUS-180"
     elif gate_name == "CZ":
-        assert params == ()
+        if params != ():
+            raise ValueError(f"CZ gate does not take parameters: {params}")
         return np.diag([1, 1, 1, -1]), "NOISY-CZ"
 
     raise NoisyGateUndefined(
-        "Undefined gate and params: {}{}\n"
-        "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ".format(gate_name, params)
+        f"Undefined gate and params: {gate_name}{params}\n" "Please restrict yourself to I, RX(+/-pi), RX(+/-pi/2), CZ"
     )
 
 
-def _get_program_gates(prog: "Program") -> List[Gate]:
-    """
-    Get all gate applications appearing in prog.
+def _get_program_gates(prog: "Program") -> list[Gate]:
+    """Get all gate applications appearing in prog.
 
     :param prog: The program
     :return: A list of all Gates in prog (without duplicates).
@@ -411,14 +389,13 @@ def _get_program_gates(prog: "Program") -> List[Gate]:
 
 def _decoherence_noise_model(
     gates: Sequence[Gate],
-    T1: Union[Dict[int, float], float] = 30e-6,
-    T2: Union[Dict[int, float], float] = 30e-6,
+    T1: Union[dict[int, float], float] = 30e-6,
+    T2: Union[dict[int, float], float] = 30e-6,
     gate_time_1q: float = 50e-9,
     gate_time_2q: float = 150e-09,
-    ro_fidelity: Union[Dict[int, float], float] = 0.95,
+    ro_fidelity: Union[dict[int, float], float] = 0.95,
 ) -> NoiseModel:
-    """
-    The default noise parameters
+    """Return default noise model.
 
     - T1 = 30 us
     - T2 = 30 us
@@ -514,8 +491,8 @@ def decoherence_noise_with_asymmetric_ro(isa: CompilerISA, p00: float = 0.975, p
 
 
 def _noise_model_program_header(noise_model: NoiseModel) -> "Program":
-    """
-    Generate the header for a pyquil Program that uses ``noise_model`` to overload noisy gates.
+    """Generate the header for a pyquil Program that uses ``noise_model`` to overload noisy gates.
+
     The program header consists of 3 sections:
 
         - The ``DEFGATE`` statements that define the meaning of the newly introduced "noisy" gate
@@ -530,7 +507,7 @@ def _noise_model_program_header(noise_model: NoiseModel) -> "Program":
     from pyquil.quil import Program
 
     p = Program()
-    defgates: Set[str] = set()
+    defgates: set[str] = set()
     for k in noise_model.gates:
         # obtain ideal gate matrix and new, noisy name by looking it up in the NOISY_GATES dict
         try:
@@ -542,7 +519,7 @@ def _noise_model_program_header(noise_model: NoiseModel) -> "Program":
                 defgates.add(new_name)
         except NoisyGateUndefined:
             print(
-                "WARNING: Could not find ideal gate definition for gate {}".format(k.gate),
+                f"WARNING: Could not find ideal gate definition for gate {k.gate}",
                 file=sys.stderr,
             )
             new_name = k.gate
@@ -557,8 +534,7 @@ def _noise_model_program_header(noise_model: NoiseModel) -> "Program":
 
 
 def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program":
-    """
-    Apply a noise model to a program and generated a 'noisy-fied' version of the program.
+    """Apply a noise model to a program and generated a 'noisy-fied' version of the program.
 
     :param prog: A Quil Program object.
     :param noise_model: A NoiseModel, either generated from an ISA or
@@ -581,14 +557,13 @@ def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program":
 
 def add_decoherence_noise(
     prog: "Program",
-    T1: Union[Dict[int, float], float] = 30e-6,
-    T2: Union[Dict[int, float], float] = 30e-6,
+    T1: Union[dict[int, float], float] = 30e-6,
+    T2: Union[dict[int, float], float] = 30e-6,
     gate_time_1q: float = 50e-9,
     gate_time_2q: float = 150e-09,
-    ro_fidelity: Union[Dict[int, float], float] = 0.95,
+    ro_fidelity: Union[dict[int, float], float] = 0.95,
 ) -> "Program":
-    """
-    Add generic damping and dephasing noise to a program.
+    """Add generic damping and dephasing noise to a program.
 
     This high-level function is provided as a convenience to investigate the effects of a
     generic noise model on a program. For more fine-grained control, please investigate
@@ -636,9 +611,7 @@ def add_decoherence_noise(
 
 
 def _bitstring_probs_by_qubit(p: np.ndarray) -> np.ndarray:
-    """
-    Ensure that an array ``p`` with bitstring probabilities has a separate axis for each qubit such
-    that ``p[i,j,...,k]`` gives the estimated probability of bitstring ``ij...k``.
+    """Ensure array p has a separate axis for each qubit so ``p[i,j,...,k]`` gives the probability of bitstring ``ij...k``.
 
     This should not allocate much memory if ``p`` is already in ``C``-contiguous order (row-major).
 
@@ -653,8 +626,7 @@ def _bitstring_probs_by_qubit(p: np.ndarray) -> np.ndarray:
 
 
 def estimate_bitstring_probs(results: np.ndarray) -> np.ndarray:
-    """
-    Given an array of single shot results estimate the probability distribution over all bitstrings.
+    """Given an array of single shot results estimate the probability distribution over all bitstrings.
 
     :param results: A 2d array where the outer axis iterates over shots
         and the inner axis over bits.
@@ -671,7 +643,8 @@ def estimate_bitstring_probs(results: np.ndarray) -> np.ndarray:
 
 
 def _apply_local_transforms(p: np.ndarray, ts: Iterable[np.ndarray]) -> np.ndarray:
-    """
+    """Apply local 2x2 matrices to each index in a 2D array of single shot results using assignment probability matrices.
+
     Given a 2d array of single shot results (outer axis iterates over shots, inner axis over bits)
     and a list of assignment probability matrices (one for each bit in the readout, ordered like
     the inner axis of results) apply local 2x2 matrices to each bit index.
@@ -702,8 +675,9 @@ def _apply_local_transforms(p: np.ndarray, ts: Iterable[np.ndarray]) -> np.ndarr
     return p_corrected
 
 
-def corrupt_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.ndarray]) -> np.ndarray:
-    """
+def corrupt_bitstring_probs(p: np.ndarray, assignment_probabilities: list[np.ndarray]) -> np.ndarray:
+    """Given a 2D array of bitstring probabilities and assignment matrices, compute the corrupted probabilities.
+
     Given a 2d array of true bitstring probabilities (outer axis iterates over shots, inner axis
     over bits) and a list of assignment probability matrices (one for each bit in the readout,
     ordered like the inner axis of results) compute the corrupted probabilities.
@@ -725,8 +699,9 @@ def corrupt_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.nda
     return _apply_local_transforms(p, assignment_probabilities)
 
 
-def correct_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.ndarray]) -> np.ndarray:
-    """
+def correct_bitstring_probs(p: np.ndarray, assignment_probabilities: list[np.ndarray]) -> np.ndarray:
+    """Given a 2D array of corrupted bitstring probabilities and assignment matrices, compute the corrected probabilities.
+
     Given a 2d array of corrupted bitstring probabilities (outer axis iterates over shots, inner
     axis over bits) and a list of assignment probability matrices (one for each bit in the readout)
     compute the corrected probabilities.
@@ -749,8 +724,7 @@ def correct_bitstring_probs(p: np.ndarray, assignment_probabilities: List[np.nda
 
 
 def bitstring_probs_to_z_moments(p: np.ndarray) -> np.ndarray:
-    """
-    Convert between bitstring probabilities and joint Z moment expectations.
+    """Convert between bitstring probabilities and joint Z moment expectations.
 
     :param p: An array that enumerates bitstring probabilities. When
         flattened out ``p = [p_00...0, p_00...1, ...,p_11...1]``. The total number of elements must
@@ -772,8 +746,8 @@ def estimate_assignment_probs(
     qc: "PyquilApiQuantumComputer",
     p0: Optional["Program"] = None,
 ) -> np.ndarray:
-    """
-    Estimate the readout assignment probabilities for a given qubit ``q``.
+    """Estimate the readout assignment probabilities for a given qubit ``q``.
+
     The returned matrix is of the form::
 
             [[p00 p01]
@@ -816,8 +790,9 @@ def estimate_assignment_probs(
     return np.array([[p00, 1 - p11], [1 - p00, p11]])
 
 
-def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> List[List[int]]:
+def _run(qc: "PyquilApiQuantumComputer", program: "Program") -> list[list[int]]:
     result = qc.run(qc.compiler.native_quil_to_executable(program))
     bitstrings = result.readout_data.get("ro")
-    assert bitstrings is not None
-    return cast(List[List[int]], bitstrings.tolist())
+    if bitstrings is None:
+        raise ValueError("No readout data found in result.")
+    return cast(list[list[int]], bitstrings.tolist())
diff --git a/pyquil/noise_gates.py b/pyquil/noise_gates.py
index b7c7c6e3e..2699c8739 100644
--- a/pyquil/noise_gates.py
+++ b/pyquil/noise_gates.py
@@ -1,16 +1,18 @@
-from pyquil.quilbase import Gate
-from pyquil.quilatom import Parameter, unpack_qubit
-from pyquil.external.rpcq import CompilerISA, Supported1QGate, Supported2QGate, GateInfo, Edge
-from typing import List, Optional
+"""Utility functions for generating noise gates compatible with a QVM's instruction set architecture."""
+
 import logging
+from typing import Optional
+
+from pyquil.external.rpcq import CompilerISA, Edge, GateInfo, Supported1QGate, Supported2QGate
+from pyquil.quilatom import Parameter, unpack_qubit
+from pyquil.quilbase import Gate
 
 _log = logging.getLogger(__name__)
 THETA = Parameter("theta")
 
 
-def _get_qvm_noise_supported_gates(isa: CompilerISA) -> List[Gate]:
-    """
-    Generate the gate set associated with an ISA for which QVM noise is supported.
+def _get_qvm_noise_supported_gates(isa: CompilerISA) -> list[Gate]:
+    """Generate the gate set associated with an ISA for which QVM noise is supported.
 
     :param isa: The instruction set architecture for a QPU.
     :return: A list of Gate objects encapsulating all gates compatible with the ISA.
@@ -23,7 +25,9 @@ def _get_qvm_noise_supported_gates(isa: CompilerISA) -> List[Gate]:
             if gate.operator == Supported1QGate.MEASURE:
                 continue
 
-            assert isinstance(gate, GateInfo)
+            if not isinstance(gate, GateInfo):
+                raise ValueError(f"ISA contained {type(gate)} where a GateInfo was expected.")
+
             qvm_noise_supported_gate = _transform_rpcq_qubit_gate_info_to_qvm_noise_supported_gate(
                 qubit_id=q.id,
                 gate=gate,
@@ -55,11 +59,11 @@ def _transform_rpcq_qubit_gate_info_to_qvm_noise_supported_gate(qubit_id: int, g
     if gate.operator == Supported1QGate.I:
         return Gate(Supported1QGate.I, [], [unpack_qubit(qubit_id)])
 
-    _log.warning("Unknown qubit gate operator: {}".format(gate.operator))
+    _log.warning(f"Unknown qubit gate operator: {gate.operator}")
     return None
 
 
-def _transform_rpcq_edge_gate_info_to_qvm_noise_supported_gates(edge: Edge) -> List[Gate]:
+def _transform_rpcq_edge_gate_info_to_qvm_noise_supported_gates(edge: Edge) -> list[Gate]:
     operators = [gate.operator for gate in edge.gates]
     targets = [unpack_qubit(t) for t in edge.ids]
 
diff --git a/pyquil/operator_estimation.py b/pyquil/operator_estimation.py
index bfa46a8a8..50a2e97da 100644
--- a/pyquil/operator_estimation.py
+++ b/pyquil/operator_estimation.py
@@ -1,43 +1,36 @@
+"""Tools for estimating the expectation value of operators on a quantum computer."""
+
 import logging
+from collections.abc import Generator, Mapping
 from math import pi
 from numbers import Complex
-from typing import Callable, Generator, List, Mapping, Tuple, Optional, cast
+from typing import Callable, Optional, cast
 
 import numpy as np
 
 from pyquil.api import QuantumComputer
-from pyquil.quil import Program
-from pyquil.quilatom import QubitDesignator
 
 # import the full public API of the pyquil experiment module
 from pyquil.experiment._group import (
-    _max_weight_state,
     _max_weight_operator,
-    construct_tpb_graph,
+    _max_weight_state,
+)
+from pyquil.experiment._group import (
     group_settings as group_experiments,
+)
+from pyquil.experiment._group import (
     group_settings_clique_removal as group_experiments_clique_removal,
+)
+from pyquil.experiment._group import (
     group_settings_greedy as group_experiments_greedy,
 )
 from pyquil.experiment._main import (
     Experiment,
-    OperatorEncoder,
 )
 from pyquil.experiment._result import ExperimentResult, ratio_variance
 from pyquil.experiment._setting import (
-    _OneQState,
     ExperimentSetting,
-    SIC0,
-    SIC1,
-    SIC2,
-    SIC3,
-    TensorProductState,
-    minusX,
-    minusY,
-    minusZ,
-    plusX,
-    plusY,
-    plusZ,
-    zeros_state,
+    _OneQState,
 )
 from pyquil.experiment._symmetrization import SymmetrizationLevel
 from pyquil.gates import RESET, RX, RY, RZ, X
@@ -47,6 +40,13 @@
 
 log = logging.getLogger(__name__)
 
+__all__ = [
+    "group_experiments",
+    "group_experiments_clique_removal",
+    "group_experiments_greedy",
+    "measure_observables",
+]
+
 
 def _one_q_sic_prep(index: int, qubit: QubitDesignator) -> Program:
     """Prepare the index-th SIC basis state."""
@@ -109,11 +109,10 @@ def _one_q_state_prep(oneq_state: _OneQState) -> Program:
 
 
 def _local_pauli_eig_meas(op: str, idx: QubitDesignator) -> Program:
-    """
-    Generate gate sequence to measure in the eigenbasis of a Pauli operator, assuming
-    we are only able to measure in the Z eigenbasis. (Note: The unitary operations of this
-    Program are essentially the Hermitian conjugates of those in :py:func:`_one_q_pauli_prep`)
+    """Generate gate sequence to measure in a Pauli operator's eigenbasis using only Z eigenbasis measurements.
 
+    Note: The unitary operations of this Program are essentially the Hermitian conjugates of those in
+    :py:func:`_one_q_pauli_prep`
     """
     if op == "X":
         return Program(RY(-pi / 2, idx))
@@ -126,11 +125,11 @@ def _local_pauli_eig_meas(op: str, idx: QubitDesignator) -> Program:
 
 def _generate_experiment_programs(
     tomo_experiment: Experiment, active_reset: bool = False
-) -> Tuple[List[Program], List[List[int]]]:
-    """
-    Generate the programs necessary to estimate the observables in a TomographyExperiment.
-    Grouping of settings to be run in parallel, e.g. by a call to group_experiments, should be
-    done before this function is called.
+) -> tuple[list[Program], list[list[int]]]:
+    """Generate the programs necessary to estimate the observables in a TomographyExperiment.
+
+    Grouping of settings to be run in parallel, e.g. by a call to group_experiments, should be done before this function
+    is called.
 
     .. CAUTION::
         One must be careful with compilation of the output programs before the appropriate MEASURE
@@ -151,7 +150,6 @@ def _generate_experiment_programs(
     programs = []
     meas_qubits = []
     for settings in tomo_experiment:
-
         # Prepare a state according to the amalgam of all setting.in_state
         total_prog = Program()
         if active_reset:
@@ -176,12 +174,13 @@ def _generate_experiment_programs(
                 "so that groups of parallel settings have compatible observables."
             )
         for qubit, op_str in max_weight_out_op:
-            assert isinstance(qubit, int)
+            if not isinstance(qubit, int):
+                raise TypeError("Qubit must be an integer.")
             total_prog += _local_pauli_eig_meas(op_str, qubit)
 
         programs.append(total_prog)
 
-        meas_qubits.append(cast(List[int], max_weight_out_op.get_qubits()))
+        meas_qubits.append(cast(list[int], max_weight_out_op.get_qubits()))
     return programs, meas_qubits
 
 
@@ -191,8 +190,7 @@ def measure_observables(
     progress_callback: Optional[Callable[[int, int], None]] = None,
     calibrate_readout: Optional[str] = "plus-eig",
 ) -> Generator[ExperimentResult, None, None]:
-    """
-    Measure all the observables in a TomographyExperiment.
+    """Measure all the observables in a TomographyExperiment.
 
     :param qc: A QuantumComputer which can run quantum programs
     :param tomo_experiment: A suite of tomographic observables to measure
@@ -238,7 +236,8 @@ def measure_observables(
         for setting in settings:
             # Get the term's coefficient so we can multiply it in later.
             coeff = setting.out_operator.coefficient
-            assert isinstance(coeff, Complex)
+            if not isinstance(coeff, Complex):
+                raise TypeError("Coefficient must be a complex number.")
             if not np.isclose(coeff.imag, 0):
                 raise ValueError(f"{setting}'s out_operator has a complex coefficient.")
             coeff = coeff.real
@@ -299,14 +298,16 @@ def measure_observables(
                 raise ValueError("Calibration readout method must be either 'plus-eig' or None")
 
 
-def _ops_bool_to_prog(ops_bool: Tuple[bool], qubits: List[int]) -> Program:
-    """
+def _ops_bool_to_prog(ops_bool: tuple[bool], qubits: list[int]) -> Program:
+    """Specify a program based on the operations to be carried out on the qubits.
+
     :param ops_bool: tuple of booleans specifying the operation to be carried out on `qubits`
     :param qubits: list specifying the qubits to be carried operations on
     :return: Program with the operations specified in `ops_bool` on the qubits specified in
         `qubits`
     """
-    assert len(ops_bool) == len(qubits), "Mismatch of qubits and operations"
+    if len(ops_bool) != len(qubits):
+        raise ValueError("Length of ops_bool must match length of qubits.")
     prog = Program()
     for i, op_bool in enumerate(ops_bool):
         if op_bool == 0:
@@ -324,8 +325,9 @@ def _stats_from_measurements(
     setting: ExperimentSetting,
     n_shots: int,
     coeff: float = 1.0,
-) -> Tuple[np.number, np.number]:
-    """
+) -> tuple[np.number, np.number]:
+    """Calculate statistics from the results of a measurement process.
+
     :param bs_results: results from running `qc.run`
     :param qubit_index_map: dict mapping qubit to classical register index
     :param setting: ExperimentSetting
@@ -348,8 +350,7 @@ def _stats_from_measurements(
 
 
 def _calibration_program(qc: QuantumComputer, tomo_experiment: Experiment, setting: ExperimentSetting) -> Program:
-    """
-    Program required for calibration in a tomography-like experiment.
+    """Program required for calibration in a tomography-like experiment.
 
     :param tomo_experiment: A suite of tomographic observables
     :param ExperimentSetting: The particular tomographic observable to measure
@@ -361,7 +362,7 @@ def _calibration_program(qc: QuantumComputer, tomo_experiment: Experiment, setti
     # Inherit any noisy attributes from main Program, including gate definitions
     # and applications which can be handy in creating simulating noisy channels
     calibr_prog = Program()
-    # Inherit readout errro instructions from main Program
+    # Inherit readout error instructions from main Program
     readout_povm_instruction = [i for i in tomo_experiment.program.out().split("\n") if "PRAGMA READOUT-POVM" in i]
     calibr_prog += readout_povm_instruction
     # Inherit any definitions of noisy gates from main Program
@@ -369,11 +370,13 @@ def _calibration_program(qc: QuantumComputer, tomo_experiment: Experiment, setti
     calibr_prog += kraus_instructions
     # Prepare the +1 eigenstate for the out operator
     for q, op in setting.out_operator.operations_as_set():
-        assert isinstance(q, int)
+        if not isinstance(q, int):
+            raise TypeError("Qubit must be an integer.")
         calibr_prog += _one_q_pauli_prep(label=op, index=0, qubit=q)
     # Measure the out operator in this state
     for q, op in setting.out_operator.operations_as_set():
-        assert isinstance(q, int)
+        if not isinstance(q, int):
+            raise TypeError("Qubit must be an integer.")
         calibr_prog += _local_pauli_eig_meas(op, q)
 
     return calibr_prog
diff --git a/pyquil/paulis.py b/pyquil/paulis.py
index 8ae2b201f..a82e832ec 100644
--- a/pyquil/paulis.py
+++ b/pyquil/paulis.py
@@ -13,55 +13,43 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-"""
-Module for working with Pauli algebras.
-"""
+"""Module for working with Pauli algebras."""
 
+import copy
 import re
+import warnings
+from collections import OrderedDict
+from collections.abc import Hashable, Iterable, Iterator, Sequence
+from functools import reduce
 from itertools import product
-import numpy as np
-import copy
-
+from numbers import Complex, Number
 from typing import (
     Callable,
-    Dict,
-    FrozenSet,
-    Hashable,
-    Iterable,
-    Iterator,
-    List,
     Optional,
-    Sequence,
-    Set,
-    Tuple,
     Union,
     cast,
 )
+
+import numpy as np
+import quil.instructions as quil_rs
 from numpy.typing import NDArray
-from functools import reduce
 from scipy.linalg import expm
 
 from pyquil.quilatom import (
-    Qubit,
-    QubitPlaceholder,
-    FormalArgument,
     Expression,
     ExpressionDesignator,
-    MemoryReference,
+    FormalArgument,
+    QubitDesignator,
+    QubitPlaceholder,
     _convert_to_py_expression,
-    _convert_to_rs_expression,
     _convert_to_py_qubits,
+    _convert_to_rs_expression,
 )
 
-import quil.instructions as quil_rs
-
+from .gates import CNOT, PHASE, QUANTUM_GATES, RX, RZ, H, X
 from .quil import Program
-from .gates import H, RZ, RX, CNOT, X, PHASE, QUANTUM_GATES
-from numbers import Number, Complex
-from collections import OrderedDict
-import warnings
 
-PauliTargetDesignator = Union[int, FormalArgument, Qubit, QubitPlaceholder]
+PauliTargetDesignator = QubitDesignator
 PauliDesignator = Union["PauliTerm", "PauliSum"]
 
 PAULI_OPS = ["X", "Y", "Z", "I"]
@@ -112,7 +100,10 @@
 
 
 class UnequalLengthWarning(Warning):
+    """Warning issued when multiplying PauliTerms of different lengths."""
+
     def __init__(self, *args: object, **kwargs: object):
+        """Initialize the warning."""
         super().__init__(*args, **kwargs)
 
 
@@ -134,7 +125,7 @@ def _valid_qubit(index: Optional[Union[PauliTargetDesignator, QubitPlaceholder]]
     )
 
 
-class PauliTerm(object):
+class PauliTerm:
     """A term is a product of Pauli operators operating on different qubits."""
 
     def __init__(
@@ -143,8 +134,7 @@ def __init__(
         index: Optional[PauliTargetDesignator],
         coefficient: ExpressionDesignator = 1.0,
     ):
-        """Create a new Pauli Term with a Pauli operator at a particular index and a leading
-        coefficient.
+        """Create a new Pauli Term with a Pauli operator at a particular index and a leading coefficient.
 
         :param op: The Pauli operator as a string "X", "Y", "Z", or "I"
         :param index: The qubit index that that operator is applied to.
@@ -153,11 +143,12 @@ def __init__(
         if op not in PAULI_OPS:
             raise ValueError(f"{op} is not a valid Pauli operator")
 
-        self._ops: Dict[PauliTargetDesignator, str] = OrderedDict()
+        self._ops: dict[PauliTargetDesignator, str] = OrderedDict()
         if op != "I":
             if not _valid_qubit(index):
                 raise ValueError(f"{index} is not a valid qubit")
-            assert index is not None
+            if index is None:
+                raise ValueError("PauliTerm index cannot be None")
             self._ops[index] = op
 
         if isinstance(coefficient, Number):
@@ -177,8 +168,7 @@ def _to_rs_pauli_term(self) -> quil_rs.PauliTerm:
         return quil_rs.PauliTerm(arguments, _convert_to_rs_expression(self.coefficient))
 
     def id(self, sort_ops: bool = True) -> str:
-        """
-        Returns an identifier string for the PauliTerm (ignoring the coefficient).
+        """Return an identifier string for the PauliTerm (ignoring the coefficient).
 
         Don't use this to compare terms. This function will not work with qubits that
         aren't sortable.
@@ -201,14 +191,14 @@ def id(self, sort_ops: bool = True) -> str:
                 "`PauliTerm.id()` will not work on PauliTerms where the qubits are not "
                 "sortable and should be avoided in favor of `operations_as_set`.",
                 FutureWarning,
+                stacklevel=2,
             )
-            return "".join("{}{}".format(self._ops[q], q) for q in sorted(self._ops.keys()))  # type: ignore
+            return "".join(f"{self._ops[q]}{q}" for q in sorted(self._ops.keys()))  # type: ignore
         else:
-            return "".join("{}{}".format(p, q) for q, p in self._ops.items())
+            return "".join(f"{p}{q}" for q, p in self._ops.items())
 
-    def operations_as_set(self) -> FrozenSet[Tuple[PauliTargetDesignator, str]]:
-        """
-        Return a frozenset of operations in this term.
+    def operations_as_set(self) -> frozenset[tuple[PauliTargetDesignator, str]]:
+        """Return a frozenset of operations in this term.
 
         Use this in place of :py:func:`id` if the order of operations in the term does not
         matter.
@@ -218,8 +208,9 @@ def operations_as_set(self) -> FrozenSet[Tuple[PauliTargetDesignator, str]]:
         return frozenset(self._ops.items())
 
     def __eq__(self, other: object) -> bool:
+        """Return True if two PauliTerms are equal."""
         if not isinstance(other, (PauliTerm, PauliSum)):
-            raise TypeError("Can't compare PauliTerm with object of type {}.".format(type(other)))
+            raise TypeError(f"Can't compare PauliTerm with object of type {type(other)}.")
         elif isinstance(other, PauliSum):
             return other == self
         else:
@@ -228,7 +219,9 @@ def __eq__(self, other: object) -> bool:
             )
 
     def __hash__(self) -> int:
-        assert isinstance(self.coefficient, Complex)
+        """Return a hash of the PauliTerm."""
+        if not isinstance(self.coefficient, Complex):
+            raise ValueError("PauliTerm cannot be hashed if the coefficient is not a complex number.")
         return hash(
             (
                 round(self.coefficient.real * HASH_PRECISION),
@@ -238,17 +231,14 @@ def __hash__(self) -> int:
         )
 
     def __len__(self) -> int:
-        """
-        The length of the PauliTerm is the number of Pauli operators in the term. A term that
-        consists of only a scalar has a length of zero.
+        """Length of the PauliTerm is the number of Pauli operators in the term.
+
+        A term that consists of only a scalar has a length of zero.
         """
         return len(self._ops)
 
     def copy(self) -> "PauliTerm":
-        """
-        Properly creates a new PauliTerm, with a completely new dictionary
-        of operators
-        """
+        """Create a new PauliTerm, with a completely new dictionary of operators."""
         new_term = PauliTerm("I", 0, 1.0)  # create new object
         # manually copy all attributes over
         for key in self.__dict__.keys():
@@ -262,16 +252,19 @@ def copy(self) -> "PauliTerm":
 
     @property
     def program(self) -> Program:
+        """Create Program from the PauliTerm."""
         return Program([QUANTUM_GATES[gate](q) for q, gate in self])
 
-    def get_qubits(self) -> List[PauliTargetDesignator]:
-        """Gets all the qubits that this PauliTerm operates on."""
+    def get_qubits(self) -> list[PauliTargetDesignator]:
+        """Get all the qubits that this PauliTerm operates on."""
         return list(self._ops.keys())
 
     def __getitem__(self, i: PauliTargetDesignator) -> str:
+        """Get the Pauli operator for a particular target."""
         return self._ops.get(i, "I")
 
-    def __iter__(self) -> Iterator[Tuple[PauliTargetDesignator, str]]:
+    def __iter__(self) -> Iterator[tuple[PauliTargetDesignator, str]]:
+        """Iterate over the quibit indices and Pauli operators in the term."""
         for i in self.get_qubits():
             yield i, self[i]
 
@@ -294,8 +287,7 @@ def _multiply_factor(self, factor: str, index: PauliTargetDesignator) -> "PauliT
         return new_term
 
     def __mul__(self, term: Union[PauliDesignator, ExpressionDesignator]) -> PauliDesignator:
-        """Multiplies this Pauli Term with another PauliTerm, PauliSum, or number according to the
-        Pauli algebra rules.
+        """Multiply this Pauli Term with another PauliTerm, PauliSum, or number according to the Pauli algebra rules.
 
         :param term: (PauliTerm or PauliSum or Number) A term to multiply by.
         :returns: The product of this PauliTerm and term.
@@ -319,11 +311,12 @@ def __rmul__(self, other: ExpressionDesignator) -> "PauliTerm":
         :returns: A new PauliTerm
         """
         p = self * other
-        assert isinstance(p, PauliTerm)
+        if not isinstance(p, PauliTerm):
+            raise ValueError(f"PauliTerm multiplication should return a PauliTerm, got {type(other)}")
         return p
 
     def __pow__(self, power: int) -> "PauliTerm":
-        """Raises this PauliTerm to power.
+        """Raise the PauliTerm to power.
 
         :param power: The power to raise this PauliTerm to.
         :return: The power-fold product of power.
@@ -341,7 +334,7 @@ def __pow__(self, power: int) -> "PauliTerm":
         return result
 
     def __add__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum":
-        """Adds this PauliTerm with another one.
+        """Add this PauliTerm with another one.
 
         :param other: A PauliTerm object, a PauliSum object, or a Number
         :returns: A PauliSum object representing the sum of this PauliTerm and other
@@ -355,7 +348,7 @@ def __add__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "Pauli
             return self + PauliTerm("I", 0, other)
 
     def __radd__(self, other: ExpressionDesignator) -> "PauliSum":
-        """Adds this PauliTerm with a Number.
+        """Add this PauliTerm with a Number.
 
         :param other: A Number
         :returns: A new PauliSum
@@ -379,19 +372,20 @@ def __rsub__(self, other: Union["PauliTerm", ExpressionDesignator]) -> "PauliSum
         return other + -1.0 * self
 
     def __repr__(self) -> str:
+        """Get a string representation of the PauliTerm."""
         term_strs = []
         for index in self._ops.keys():
-            term_strs.append("%s%s" % (self[index], index))
+            term_strs.append(f"{self[index]}{index}")
 
         if len(term_strs) == 0:
             term_strs.append("I")
-        out = "%s*%s" % (self.coefficient, "*".join(term_strs))
+        out = f"{self.coefficient}*{'*'.join(term_strs)}"
         return out
 
     def compact_str(self) -> str:
-        """A string representation of the Pauli term that is more compact than ``str(term)``
+        """Return string representation of the Pauli term that is more compact than ``str(term)``.
 
-        >>> term = 2.0 * sX(1)* sZ(2)
+        >>> term = 2.0 * sX(1) * sZ(2)
         >>> str(term)
         '(2+0j)*X1*Z2'
         >>> term.compact_str()
@@ -401,11 +395,11 @@ def compact_str(self) -> str:
 
     @classmethod
     def from_list(
-        cls, terms_list: Sequence[Tuple[str, PauliTargetDesignator]], coefficient: ExpressionDesignator = 1.0
+        cls, terms_list: Sequence[tuple[str, PauliTargetDesignator]], coefficient: ExpressionDesignator = 1.0
     ) -> "PauliTerm":
-        """
-        Allocates a Pauli Term from a list of operators and indices. This is more efficient than
-        multiplying together individual terms.
+        """Allocate a PauliTerm from a list of operators and indices.
+
+        This is more efficient than multiplying together individual terms.
 
         :param list terms_list: A list of tuples, e.g. [("X", 0), ("Y", 1)]
         :return: PauliTerm
@@ -416,10 +410,12 @@ def from_list(
             )
 
         pterm = PauliTerm("I", 0)
-        assert all([op[0] in PAULI_OPS for op in terms_list])
+        if not all([op[0] in PAULI_OPS for op in terms_list]):
+            raise ValueError("All operators must be in ['X', 'Y', 'Z', 'I']")
 
         indices = [op[1] for op in terms_list]
-        assert all(_valid_qubit(index) for index in indices)
+        if not all(_valid_qubit(index) for index in indices):
+            raise ValueError("All qubit indices must be valid.")
 
         # this is because from_list doesn't call simplify in order to be more efficient.
         if len(set(indices)) != len(indices):
@@ -440,16 +436,16 @@ def from_list(
 
     @classmethod
     def from_compact_str(cls, str_pauli_term: str) -> "PauliTerm":
-        """Construct a PauliTerm from the result of str(pauli_term)"""
+        """Construct a PauliTerm from the result of str(pauli_term)."""
         # split into str_coef, str_op at first '*'' outside parenthesis
         try:
             str_coef, str_op = re.split(r"\*(?![^(]*\))", str_pauli_term, maxsplit=1)
-        except ValueError:
+        except ValueError as e:
             raise ValueError(
                 "Could not separate the pauli string into "
                 f"coefficient and operator. {str_pauli_term} does"
                 " not match <coefficient>*<operator>"
-            )
+            ) from e
 
         # parse the coefficient into either a float or complex
         str_coef = str_coef.replace(" ", "")
@@ -458,12 +454,13 @@ def from_compact_str(cls, str_pauli_term: str) -> "PauliTerm":
         except ValueError:
             try:
                 coef = complex(str_coef)
-            except ValueError:
-                raise ValueError(f"Could not parse the coefficient {str_coef}")
+            except ValueError as e:
+                raise ValueError(f"Could not parse the coefficient {str_coef}") from e
 
         op = sI() * coef
         if str_op == "I":
-            assert isinstance(op, PauliTerm)
+            if not isinstance(op, PauliTerm):
+                raise ValueError(f"Expected operation to be PauliTerm, got {type(op)}.")
             return op
 
         # parse the operator
@@ -474,18 +471,17 @@ def from_compact_str(cls, str_pauli_term: str) -> "PauliTerm":
         for factor in re.finditer(r"([XYZ])(\d+)", str_op):
             op *= cls(factor.group(1), int(factor.group(2)))
 
-        assert isinstance(op, PauliTerm)
+        if not isinstance(op, PauliTerm):
+            raise ValueError(f"Expected operation to be PauliTerm, got {type(op)}.")
         return op
 
     def pauli_string(self, qubits: Optional[Iterable[int]] = None) -> str:
-        """
-        Return a string representation of this PauliTerm without its coefficient and with
-        implicit qubit indices.
+        """Return a string representation of this PauliTerm without its coefficient and with implicit qubit indices.
 
         If an iterable of qubits is provided, each character in the resulting string represents
         a Pauli operator on the corresponding qubit.
 
-        >>> p = PauliTerm("X", 0) * PauliTerm("Y", 1, 1.j)
+        >>> p = PauliTerm("X", 0) * PauliTerm("Y", 1, 1.0j)
         >>> p.pauli_string()
         'XY'
         >>> p.pauli_string(qubits=[0])
@@ -496,7 +492,6 @@ def pauli_string(self, qubits: Optional[Iterable[int]] = None) -> str:
         :param iterable of qubits: The iterable of qubits to represent, given as ints.
         :return: The string representation of this PauliTerm, sans coefficient
         """
-
         if qubits is None:
             return "".join(self._ops.values())
         return "".join(self[q] for q in qubits)
@@ -504,22 +499,17 @@ def pauli_string(self, qubits: Optional[Iterable[int]] = None) -> str:
 
 # For convenience, a shorthand for several operators.
 def ID() -> PauliTerm:
-    """
-    The identity operator.
-    """
+    """Identity operator."""
     return PauliTerm("I", 0, 1)
 
 
 def ZERO() -> PauliTerm:
-    """
-    The zero operator.
-    """
+    """Zero operator."""
     return PauliTerm("I", 0, 0)
 
 
 def sI(q: Optional[int] = None) -> PauliTerm:
-    """
-    A function that returns the identity operator, optionally on a particular qubit.
+    """Return the identity operator, optionally on a particular qubit.
 
     This can be specified without a qubit.
 
@@ -530,8 +520,7 @@ def sI(q: Optional[int] = None) -> PauliTerm:
 
 
 def sX(q: int) -> PauliTerm:
-    """
-    A function that returns the sigma_X operator on a particular qubit.
+    """Return the sigma_X operator on a particular qubit.
 
     :param qubit_index: The index of the qubit
     :returns: A PauliTerm object
@@ -540,8 +529,7 @@ def sX(q: int) -> PauliTerm:
 
 
 def sY(q: int) -> PauliTerm:
-    """
-    A function that returns the sigma_Y operator on a particular qubit.
+    """Return the sigma_Y operator on a particular qubit.
 
     :param qubit_index: The index of the qubit
     :returns: A PauliTerm object
@@ -550,8 +538,7 @@ def sY(q: int) -> PauliTerm:
 
 
 def sZ(q: int) -> PauliTerm:
-    """
-    A function that returns the sigma_Z operator on a particular qubit.
+    """Return the sigma_Z operator on a particular qubit.
 
     :param qubit_index: The index of the qubit
     :returns: A PauliTerm object
@@ -560,8 +547,7 @@ def sZ(q: int) -> PauliTerm:
 
 
 def term_with_coeff(term: PauliTerm, coeff: ExpressionDesignator) -> PauliTerm:
-    """
-    Change the coefficient of a PauliTerm.
+    """Change the coefficient of a PauliTerm.
 
     :param term: A PauliTerm object
     :param coeff: The coefficient to set on the PauliTerm
@@ -575,11 +561,12 @@ def term_with_coeff(term: PauliTerm, coeff: ExpressionDesignator) -> PauliTerm:
     return new_pauli
 
 
-class PauliSum(object):
+class PauliSum:
     """A sum of one or more PauliTerms."""
 
     def __init__(self, terms: Sequence[PauliTerm]):
-        """
+        """Initialize a PauliSum with a list of PauliTerms.
+
         :param Sequence terms: A Sequence of PauliTerms.
         """
         if not (isinstance(terms, Sequence) and all([isinstance(term, PauliTerm) for term in terms])):
@@ -594,10 +581,10 @@ def __init__(self, terms: Sequence[PauliTerm]):
     def _from_rs_pauli_sum(cls, pauli_sum: quil_rs.PauliSum) -> "PauliSum":
         return cls([PauliTerm._from_rs_pauli_term(term) for term in pauli_sum.terms])
 
-    def _to_rs_pauli_sum(self, arguments: Optional[List[PauliTargetDesignator]] = None) -> quil_rs.PauliSum:
-        rs_arguments: List[str]
+    def _to_rs_pauli_sum(self, arguments: Optional[list[PauliTargetDesignator]] = None) -> quil_rs.PauliSum:
+        rs_arguments: list[str]
         if arguments is None:
-            argument_set: Dict[str, None] = {}
+            argument_set: dict[str, None] = {}
             for term_arguments in [term.get_qubits() for term in self.terms]:
                 argument_set.update({str(arg): None for arg in term_arguments})
             rs_arguments = list(argument_set.keys())
@@ -613,7 +600,7 @@ def __eq__(self, other: object) -> bool:
         :return: True if other is equivalent to this PauliSum, False otherwise.
         """
         if not isinstance(other, (PauliTerm, PauliSum)):
-            raise TypeError("Can't compare PauliSum with object of type {}.".format(type(other)))
+            raise TypeError(f"Can't compare PauliSum with object of type {type(other)}.")
         elif isinstance(other, PauliTerm):
             return self == PauliSum([other])
         elif len(self.terms) != len(other.terms):
@@ -622,31 +609,31 @@ def __eq__(self, other: object) -> bool:
         return set(self.terms) == set(other.terms)
 
     def __hash__(self) -> int:
+        """Return a hash value for the sum."""
         return hash(frozenset(self.terms))
 
     def __repr__(self) -> str:
+        """Return a string representation for the sum."""
         return " + ".join([str(term) for term in self.terms])
 
     def __len__(self) -> int:
-        """
-        The length of the PauliSum is the number of PauliTerms in the sum.
-        """
+        """Return the number of PauliTerms in the sum."""
         return len(self.terms)
 
     def __getitem__(self, item: int) -> PauliTerm:
-        """
+        """Get the PauliTerm at the given position the sum.
+
         :param item: The index of the term in the sum to return
         :return: The PauliTerm at the index-th position in the PauliSum
         """
         return self.terms[item]
 
     def __iter__(self) -> Iterator[PauliTerm]:
+        """Iterate over the PauliTerms in the sum."""
         return self.terms.__iter__()
 
     def __mul__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum":
-        """
-        Multiplies together this PauliSum with PauliSum, PauliTerm or Number objects. The new term
-        is then simplified according to the Pauli Algebra rules.
+        """Multiply and simplify this PauliSum with another PauliSum, PauliTerm or Number object.
 
         :param other: a PauliSum, PauliTerm or Number object
         :return: A new PauliSum object given by the multiplication.
@@ -654,32 +641,31 @@ def __mul__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "Pauli
         if not isinstance(other, (Expression, Number, PauliTerm, PauliSum)):
             raise ValueError("Cannot multiply PauliSum by term that is not a Number, PauliTerm, or PauliSum")
 
-        other_terms: List[Union[PauliTerm, ExpressionDesignator]] = []
+        other_terms: list[Union[PauliTerm, ExpressionDesignator]] = []
         if isinstance(other, PauliSum):
             other_terms += other.terms
         else:
             other_terms += [other]
 
         new_terms = [lterm * rterm for lterm, rterm in product(self.terms, other_terms)]
-        new_sum = PauliSum(cast(List[PauliTerm], new_terms))
+        new_sum = PauliSum(cast(list[PauliTerm], new_terms))
         return new_sum.simplify()
 
     def __rmul__(self, other: ExpressionDesignator) -> "PauliSum":
-        """
-        Multiples together this PauliSum with PauliSum, PauliTerm or Number objects. The new term
-        is then simplified according to the Pauli Algebra rules.
+        """Multiply and simplify this PauliSum with another PauliSum, PauliTerm or Number object.
 
         :param other: a PauliSum, PauliTerm or Number object
         :return: A new PauliSum object given by the multiplication.
         """
-        assert isinstance(other, Number)
+        if not isinstance(other, Number):
+            raise TypeError(f"Expected a Number object, got {type(other)}")
         new_terms = [term.copy() for term in self.terms]
         for term in new_terms:
             term.coefficient *= other
         return PauliSum(new_terms).simplify()
 
     def __pow__(self, power: int) -> "PauliSum":
-        """Raises this PauliSum to power.
+        """Raise this PauliSum to power.
 
         :param power: The power to raise this PauliSum to.
         :return: The power-th power of this PauliSum.
@@ -703,9 +689,7 @@ def __pow__(self, power: int) -> "PauliSum":
         return result
 
     def __add__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum":
-        """
-        Adds together this PauliSum with PauliSum, PauliTerm or Number objects. The new term
-        is then simplified according to the Pauli Algebra rules.
+        """Add and simplify this PauliSum with another PauliSum, PauliTerm or Number objects.
 
         :param other: a PauliSum, PauliTerm or Number object
         :return: A new PauliSum object given by the addition.
@@ -724,20 +708,17 @@ def __add__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "Pauli
         return new_sum.simplify()
 
     def __radd__(self, other: ExpressionDesignator) -> "PauliSum":
-        """
-        Adds together this PauliSum with a Number object. The new term
-        is then simplified according to the Pauli Algebra rules.
+        """Add and simplify this PauliSum with another PauliSum, PauliTerm or Number object.
 
         :param other: A Number
         :return: A new PauliSum object given by the addition.
         """
-        assert isinstance(other, Number)
+        if not isinstance(other, Number):
+            raise TypeError(f"Expected a Number object, got {type(other)}")
         return self + other
 
     def __sub__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum":
-        """
-        Finds the difference of this PauliSum with PauliSum, PauliTerm or Number objects. The new
-        term is then simplified according to the Pauli Algebra rules.
+        """Subtract and simplify this PauliSum with another PauliSum, PauliTerm or Number object.
 
         :param other: a PauliSum, PauliTerm or Number object
         :return: A new PauliSum object given by the subtraction.
@@ -745,36 +726,29 @@ def __sub__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "Pauli
         return self + -1.0 * other
 
     def __rsub__(self, other: Union[PauliDesignator, ExpressionDesignator]) -> "PauliSum":
-        """
-        Finds the different of this PauliSum with PauliSum, PauliTerm or Number objects. The new
-        term is then simplified according to the Pauli Algebra rules.
+        """Subtract and simplify this PauliSum with another PauliSum, PauliTerm or Number object.
 
         :param other: a PauliSum, PauliTerm or Number object
         :return: A new PauliSum object given by the subtraction.
         """
         return other + -1.0 * self
 
-    def get_qubits(self) -> List[PauliTargetDesignator]:
-        """
-        The support of all the operators in the PauliSum object.
+    def get_qubits(self) -> list[PauliTargetDesignator]:
+        """Get the qubits that this PauliSum operates on.
 
         :returns: A list of all the qubits in the sum of terms.
         """
-        all_qubits: Set[PauliTargetDesignator] = set()
+        all_qubits: set[PauliTargetDesignator] = set()
         for term in self.terms:
             all_qubits.update(term.get_qubits())
         return _convert_to_py_qubits(set(all_qubits))
 
     def simplify(self) -> "PauliSum":
-        """
-        Simplifies the sum of Pauli operators according to Pauli algebra rules.
-        """
+        """Simplify the sum of Pauli operators according to Pauli algebra rules."""
         return simplify_pauli_sum(self)
 
-    def get_programs(self) -> Tuple[List[Program], np.ndarray]:
-        """
-        Get a Pyquil Program corresponding to each term in the PauliSum and a coefficient
-        for each program
+    def get_programs(self) -> tuple[list[Program], np.ndarray]:
+        """Get a Pyquil Program corresponding to each term in the PauliSum and a coefficient for each program.
 
         :return: (programs, coefficients)
         """
@@ -783,9 +757,9 @@ def get_programs(self) -> Tuple[List[Program], np.ndarray]:
         return programs, coefficients
 
     def compact_str(self) -> str:
-        """A string representation of the PauliSum that is more compact than ``str(pauli_sum)``
+        """Return a string representation of the PauliSum that is more compact than ``str(pauli_sum)``.
 
-        >>> pauli_sum = 2.0 * sX(1)* sZ(2) + 1.5 * sY(2)
+        >>> pauli_sum = 2.0 * sX(1) * sZ(2) + 1.5 * sY(2)
         >>> str(pauli_sum)
         '(2+0j)*X1*Z2 + (1.5+0j)*Y2'
         >>> pauli_sum.compact_str()
@@ -795,7 +769,7 @@ def compact_str(self) -> str:
 
     @classmethod
     def from_compact_str(cls, str_pauli_sum: str) -> "PauliSum":
-        """Construct a PauliSum from the result of str(pauli_sum)"""
+        """Construct a PauliSum from the result of str(pauli_sum)."""
         # split str_pauli_sum only at "+" outside of parenthesis to allow
         # e.g. "0.5*X0 + (0.5+0j)*Z2"
         str_terms = re.split(r"\+(?![^(]*\))", str_pauli_sum)
@@ -805,16 +779,14 @@ def from_compact_str(cls, str_pauli_sum: str) -> "PauliSum":
 
 
 def simplify_pauli_sum(pauli_sum: PauliSum) -> PauliSum:
-    """
-    Simplify the sum of Pauli operators according to Pauli algebra rules.
+    """Simplify the sum of Pauli operators according to Pauli algebra rules.
 
     Warning: The simplified expression may re-order pauli operations, and may
     impact the observed performance when running on the QPU.
     """
-
     # You might want to use a defaultdict(list) here, but don't because
     # we want to do our best to preserve the order of terms.
-    like_terms: Dict[Hashable, List[PauliTerm]] = OrderedDict()
+    like_terms: dict[Hashable, list[PauliTerm]] = OrderedDict()
     for term in pauli_sum.terms:
         key = term.operations_as_set()
         if key in like_terms:
@@ -835,8 +807,8 @@ def simplify_pauli_sum(pauli_sum: PauliSum) -> PauliSum:
 
 
 def check_commutation(pauli_list: Sequence[PauliTerm], pauli_two: PauliTerm) -> bool:
-    """
-    Check if commuting a PauliTerm commutes with a list of other terms by natural calculation.
+    """Check if commuting a PauliTerm commutes with a list of other terms by natural calculation.
+
     Uses the result in Section 3 of arXiv:1405.5749v2, modified slightly here to check for the
     number of anti-coincidences (which must always be even for commuting PauliTerms)
     instead of the no. of coincidences, as in the paper.
@@ -861,8 +833,8 @@ def coincident_parity(p1: PauliTerm, p2: PauliTerm) -> bool:
     return True
 
 
-def commuting_sets(pauli_terms: PauliSum) -> List[List[PauliTerm]]:
-    """Gather the Pauli terms of pauli_terms variable into commuting sets
+def commuting_sets(pauli_terms: PauliSum) -> list[list[PauliTerm]]:
+    """Gather the Pauli terms of pauli_terms variable into commuting sets.
 
     Uses algorithm defined in (Raeisi, Wiebe, Sanders, arXiv:1108.4318, 2011)
     to find commuting sets. Except uses commutation check from arXiv:1405.5749v2
@@ -870,7 +842,6 @@ def commuting_sets(pauli_terms: PauliSum) -> List[List[PauliTerm]]:
     :param pauli_terms: A PauliSum object
     :returns: List of lists where each list contains a commuting set
     """
-
     m_terms = len(pauli_terms.terms)
     m_s = 1
     groups = []
@@ -889,8 +860,7 @@ def commuting_sets(pauli_terms: PauliSum) -> List[List[PauliTerm]]:
 
 
 def is_identity(term: PauliDesignator) -> bool:
-    """
-    Tests to see if a PauliTerm or PauliSum is a scalar multiple of identity
+    """Return True if PauliTerm or PauliSum is a scalar multiple of identity, False otherwise.
 
     :param term: Either a PauliTerm or PauliSum
     :returns: True if the PauliTerm or PauliSum is a scalar multiple of identity, False otherwise
@@ -904,8 +874,7 @@ def is_identity(term: PauliDesignator) -> bool:
 
 
 def exponentiate(term: PauliTerm) -> Program:
-    """
-    Creates a pyQuil program that simulates the unitary evolution exp(-1j * term)
+    """Create a pyQuil program that simulates the unitary evolution exp(-1j * term).
 
     :param term: A pauli term to exponentiate
     :returns: A Program object
@@ -913,14 +882,14 @@ def exponentiate(term: PauliTerm) -> Program:
     return exponential_map(term)(1.0)
 
 
-def exponential_map(term: PauliTerm) -> Callable[[Union[float, MemoryReference]], Program]:
-    """
-    Returns a function f(alpha) that constructs the Program corresponding to exp(-1j*alpha*term).
+def exponential_map(term: PauliTerm) -> Callable[[float], Program]:
+    """Return a function f(alpha) that constructs the Program corresponding to exp(-1j*alpha*term).
 
     :param term: A pauli term to exponentiate
     :returns: A function that takes an angle parameter and returns a program.
     """
-    assert isinstance(term.coefficient, Complex)
+    if not isinstance(term.coefficient, Complex):
+        raise TypeError(f"PauliTerm coefficient type must be complex, got {type(term.coefficient)}")
 
     if not np.isclose(np.imag(term.coefficient), 0.0):
         raise TypeError("PauliTerm coefficient must be real")
@@ -946,10 +915,10 @@ def exp_wrap(param: float) -> Program:
 
 def exponentiate_commuting_pauli_sum(
     pauli_sum: PauliSum,
-) -> Callable[[Union[float, MemoryReference]], Program]:
-    """
-    Returns a function that maps all substituent PauliTerms and sums them into a program. NOTE: Use
-    this function with care. Substituent PauliTerms should commute.
+) -> Callable[[float], Program]:
+    """Return a function that maps all substituent PauliTerms and sums them into a program.
+
+    NOTE: Use this function with care. Substituent PauliTerms should commute.
 
     :param pauli_sum: PauliSum to exponentiate.
     :returns: A function that parametrizes the exponential.
@@ -959,7 +928,7 @@ def exponentiate_commuting_pauli_sum(
 
     fns = [exponential_map(term) for term in pauli_sum]
 
-    def combined_exp_wrap(param: Union[float, MemoryReference]) -> Program:
+    def combined_exp_wrap(param: float) -> Program:
         return Program([f(param) for f in fns])
 
     return combined_exp_wrap
@@ -968,10 +937,10 @@ def combined_exp_wrap(param: Union[float, MemoryReference]) -> Program:
 def exponentiate_pauli_sum(
     pauli_sum: Union[PauliSum, PauliTerm],
 ) -> NDArray[np.complex_]:
-    r"""
-    Exponentiates a sequence of PauliTerms, which may or may not commute. The Pauliterms must
-    have fixed (non-parametric) coefficients. The coefficients are interpreted in cycles
-    rather than radians or degrees.
+    r"""Exponentiate a sequence of PauliTerms, which may or may not commute.
+
+    The Pauliterms must have fixed (non-parametric) coefficients. The coefficients are interpreted in cycles rather than
+    radians or degrees.
 
     e^{-i\pi\sum_i \theta_i P_i}
 
@@ -981,23 +950,23 @@ def exponentiate_pauli_sum(
 
     >>> from numpy import pi
     >>> phi = pi
-    >>> coeff = phi/(-4*pi) # -0.25
-    >>> hamiltonian = PauliTerm("Z", 0) * PauliTerm("Z", 1) - 1*PauliTerm("Z", 0) - 1*PauliTerm("Z", 1)
-    >>> exponentiate_pauli_sum(coeff*hamiltonian)
+    >>> coeff = phi / (-4 * pi)  # -0.25
+    >>> hamiltonian = PauliTerm("Z", 0) * PauliTerm("Z", 1) - 1 * PauliTerm("Z", 0) - 1 * PauliTerm("Z", 1)
+    >>> exponentiate_pauli_sum(coeff * hamiltonian)
     array([[...]])
 
     To produce the Quil XY(theta) gate, you can use:
 
-    >>> theta = pi/2
-    >>> coeff = theta/(-4*pi)
+    >>> theta = pi / 2
+    >>> coeff = theta / (-4 * pi)
     >>> hamiltonian = PauliTerm("X", 0) * PauliTerm("X", 1) + PauliTerm("Y", 0) * PauliTerm("Y", 1)
-    >>> exponentiate_pauli_sum(coeff*hamiltonian)
+    >>> exponentiate_pauli_sum(coeff * hamiltonian)
     array([[...]])
 
     A global phase is applied to the unitary such that the [0,0] element is always real.
 
     :param pauli_sum: PauliSum to exponentiate.
-    :returns: The matrix exponetial of the PauliSum
+    :returns: The matrix exponent of the PauliSum
     """
     if isinstance(pauli_sum, PauliTerm):
         pauli_sum = PauliSum([pauli_sum])
@@ -1011,13 +980,15 @@ def exponentiate_pauli_sum(
 
     qubits = pauli_sum.get_qubits()
     for q in qubits:
-        assert isinstance(q, int)
+        if not isinstance(q, int):
+            raise ValueError("PauliSum must be defined on qubits, not placeholders.")
     qubits.sort()
 
     matrices = []
     for term in pauli_sum.terms:
         coeff = term.coefficient
-        assert isinstance(coeff, Number)
+        if not isinstance(coeff, Number):
+            raise ValueError("PauliSum must have fixed coefficients.")
         qubit_paulis = {qubit: pauli for qubit, pauli in term.operations_as_set()}
         paulis = [qubit_paulis[q] if q in qubit_paulis else "I" for q in qubits]
         matrix = float(np.real(coeff)) * reduce(np.kron, [pauli_matrices[p] for p in paulis])
@@ -1028,9 +999,9 @@ def exponentiate_pauli_sum(
 
 
 def _exponentiate_general_case(pauli_term: PauliTerm, param: float) -> Program:
-    """
-    Returns a Quil (Program()) object corresponding to the exponential of
-    the pauli_term object, i.e. exp[-1.0j * param * pauli_term]
+    """Return a Quil (Program()) object corresponding to the exponential of the pauli_term object.
+
+    For example, exp[-1.0j * param * pauli_term]
 
     :param pauli_term: A PauliTerm to exponentiate
     :param param: scalar, non-complex, value
@@ -1051,7 +1022,8 @@ def reverse_hack(p: Program) -> Program:
     highest_target_index = None
 
     for index, op in pauli_term:
-        assert isinstance(index, (int, QubitPlaceholder))
+        if not isinstance(index, (int, QubitPlaceholder)):
+            raise ValueError("PauliTerm indices must be qubits or qubit placeholders, got {type(index)}.")
         if "X" == op:
             change_to_z_basis.inst(H(index))
             change_to_original_basis.inst(H(index))
@@ -1070,7 +1042,8 @@ def reverse_hack(p: Program) -> Program:
     # building rotation circuit
     quil_prog += change_to_z_basis
     quil_prog += cnot_seq
-    assert isinstance(pauli_term.coefficient, Complex) and highest_target_index is not None
+    if not isinstance(pauli_term.coefficient, Complex) or highest_target_index is None:
+        raise ValueError("PauliTerm coefficient must be complex and highest_target_index must be set.")
     quil_prog.inst(RZ(2.0 * pauli_term.coefficient * param, highest_target_index))
     quil_prog += reverse_hack(cnot_seq)
     quil_prog += change_to_original_basis
@@ -1078,9 +1051,8 @@ def reverse_hack(p: Program) -> Program:
     return quil_prog
 
 
-def suzuki_trotter(trotter_order: int, trotter_steps: int) -> List[Tuple[float, int]]:
-    """
-    Generate trotterization coefficients for a given number of Trotter steps.
+def suzuki_trotter(trotter_order: int, trotter_steps: int) -> list[tuple[float, int]]:
+    """Generate trotterization coefficients for a given number of Trotter steps.
 
     U = exp(A + B) is approximated as exp(w1*o1)exp(w2*o2)... This method returns
     a list [(w1, o1), (w2, o2), ... , (wm, om)] of tuples where o=0 corresponds
@@ -1097,7 +1069,7 @@ def suzuki_trotter(trotter_order: int, trotter_steps: int) -> List[Tuple[float,
     """
     p1 = p2 = p4 = p5 = 1.0 / (4 - (4 ** (1.0 / 3)))
     p3 = 1 - 4 * p1
-    trotter_dict: Dict[int, List[Tuple[float, int]]] = {
+    trotter_dict: dict[int, list[tuple[float, int]]] = {
         1: [(1, 0), (1, 1)],
         2: [(0.5, 0), (1, 1), (0.5, 0)],
         3: [
@@ -1133,18 +1105,19 @@ def suzuki_trotter(trotter_order: int, trotter_steps: int) -> List[Tuple[float,
 
 
 def is_zero(pauli_object: PauliDesignator) -> bool:
-    """
-    Tests to see if a PauliTerm or PauliSum is zero.
+    """Return True if a PauliTerm or PauliSum is zero, False otherwise.
 
     :param pauli_object: Either a PauliTerm or PauliSum
     :returns: True if PauliTerm is zero, False otherwise
     """
     if isinstance(pauli_object, PauliTerm):
-        assert isinstance(pauli_object.coefficient, Complex)
+        if not isinstance(pauli_object.coefficient, Complex):
+            raise ValueError("PauliTerm coefficient must be fixed and complex.")
         return bool(np.isclose(pauli_object.coefficient, 0))
     elif isinstance(pauli_object, PauliSum):
-        assert isinstance(pauli_object.terms[0].coefficient, Complex)
-        return len(pauli_object.terms) == 1 and np.isclose(pauli_object.terms[0].coefficient, 0)
+        if not isinstance(pauli_object.terms[0].coefficient, Complex):
+            raise ValueError("PauliTerm coefficient must be fixed and complex.")
+        return len(pauli_object.terms) == 1 and bool(np.isclose(pauli_object.terms[0].coefficient, 0))
     else:
         raise TypeError("is_zero only checks PauliTerms and PauliSum objects!")
 
@@ -1155,9 +1128,7 @@ def trotterize(
     trotter_order: int = 1,
     trotter_steps: int = 1,
 ) -> Program:
-    """
-    Create a Quil program that approximates exp( (A + B)t) where A and B are
-    PauliTerm operators.
+    """Create a Quil program that approximates exp( (A + B)t) where A and B are PauliTerm operators.
 
     :param first_pauli_term: PauliTerm denoted `A`
     :param second_pauli_term: PauliTerm denoted `B`
@@ -1168,7 +1139,6 @@ def trotterize(
 
     :return: Quil program
     """
-
     if not (1 <= trotter_order < 5):
         raise ValueError("trotterize only accepts trotter_order in {1, 2, 3, 4}.")
 
diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py
index dcb970284..72322175a 100644
--- a/pyquil/pyqvm.py
+++ b/pyquil/pyqvm.py
@@ -1,3 +1,5 @@
+"""A pure Python implementation of the Quantum Virtual Machine (QVM)."""
+
 ##############################################################################
 # Copyright 2018 Rigetti Computing
 #
@@ -15,48 +17,49 @@
 ##############################################################################
 import logging
 from abc import ABC, abstractmethod
-from typing import Dict, List, Optional, Sequence, Type, Union, Any, Iterable
+from collections.abc import Iterable, Sequence
+from typing import Any, Optional, Union
 
 import numpy as np
 from numpy.random.mtrand import RandomState
-from qcs_sdk import ResultData, ExecutionData, RegisterData
+from qcs_sdk import ExecutionData, RegisterData, ResultData
 from qcs_sdk.qvm import QVMResultData
 
-from pyquil.api import QAM, QuantumExecutable, QAMExecutionResult, MemoryMap
-from pyquil.paulis import PauliTerm, PauliSum
+from pyquil.api import QAM, MemoryMap, QAMExecutionResult, QuantumExecutable
+from pyquil.paulis import PauliSum, PauliTerm
 from pyquil.quil import Program
 from pyquil.quilatom import Label, LabelPlaceholder, MemoryReference
 from pyquil.quilbase import (
-    Gate,
-    Measurement,
-    ResetQubit,
-    DefGate,
-    JumpTarget,
-    JumpWhen,
-    JumpUnless,
-    Halt,
-    Wait,
-    Reset,
-    Nop,
-    UnaryClassicalInstruction,
-    ClassicalNeg,
-    ClassicalNot,
-    LogicalBinaryOp,
-    ClassicalAnd,
-    ClassicalInclusiveOr,
-    ClassicalExclusiveOr,
     ArithmeticBinaryOp,
     ClassicalAdd,
-    ClassicalSub,
-    ClassicalMul,
+    ClassicalAnd,
     ClassicalDiv,
-    ClassicalMove,
     ClassicalExchange,
-    Jump,
-    Pragma,
+    ClassicalExclusiveOr,
+    ClassicalInclusiveOr,
+    ClassicalMove,
+    ClassicalMul,
+    ClassicalNeg,
+    ClassicalNot,
+    ClassicalSub,
     Declare,
+    DefGate,
     DefGateByPaulis,
     DefPermutationGate,
+    Gate,
+    Halt,
+    Jump,
+    JumpTarget,
+    JumpUnless,
+    JumpWhen,
+    LogicalBinaryOp,
+    Measurement,
+    Nop,
+    Pragma,
+    Reset,
+    ResetQubit,
+    UnaryClassicalInstruction,
+    Wait,
 )
 
 log = logging.getLogger(__name__)
@@ -65,10 +68,11 @@
 
 
 class AbstractQuantumSimulator(ABC):
+    """An abstract interface for a quantum simulator."""
+
     @abstractmethod
     def __init__(self, n_qubits: int, rs: Optional[RandomState]):
-        """
-        Initialize.
+        """Initialize.
 
         :param n_qubits: Number of qubits to simulate.
         :param rs: a RandomState (shared with the owning :py:class:`PyQVM`) for
@@ -77,16 +81,14 @@ def __init__(self, n_qubits: int, rs: Optional[RandomState]):
 
     @abstractmethod
     def do_gate(self, gate: Gate) -> "AbstractQuantumSimulator":
-        """
-        Perform a gate.
+        """Perform a gate.
 
         :return: ``self`` to support method chaining.
         """
 
     @abstractmethod
     def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "AbstractQuantumSimulator":
-        """
-        Apply an arbitrary unitary; not necessarily a named gate.
+        """Apply an arbitrary unitary; not necessarily a named gate.
 
         :param matrix: The unitary matrix to apply. No checks are done
         :param qubits: A list of qubits to apply the unitary to.
@@ -94,8 +96,7 @@ def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "Abstract
         """
 
     def do_program(self, program: Program) -> "AbstractQuantumSimulator":
-        """
-        Perform a sequence of gates contained within a program.
+        """Perform a sequence of gates contained within a program.
 
         :param program: The program
         :return: self
@@ -108,16 +109,14 @@ def do_program(self, program: Program) -> "AbstractQuantumSimulator":
 
     @abstractmethod
     def do_measurement(self, qubit: int) -> int:
-        """
-        Measure a qubit and collapse the wavefunction
+        """Measure a qubit and collapse the wavefunction.
 
         :return: The measurement result. A 1 or a 0.
         """
 
     @abstractmethod
     def expectation(self, operator: Union[PauliTerm, PauliSum]) -> complex:
-        """
-        Compute the expectation of an operator.
+        """Compute the expectation of an operator.
 
         :param operator: The operator
         :return: The operator's expectation value
@@ -125,25 +124,22 @@ def expectation(self, operator: Union[PauliTerm, PauliSum]) -> complex:
 
     @abstractmethod
     def reset(self) -> "AbstractQuantumSimulator":
-        """
-        Reset the wavefunction to the ``|000...00>`` state.
+        """Reset the wavefunction to the ``|000...00>`` state.
 
         :return: ``self`` to support method chaining.
         """
 
     @abstractmethod
     def sample_bitstrings(self, n_samples: int) -> np.ndarray:
-        """
-        Sample bitstrings from the current state.
+        """Sample bitstrings from the current state.
 
         :param n_samples: The number of bitstrings to sample
         :return: A numpy array of shape (n_samples, n_qubits)
         """
 
     @abstractmethod
-    def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: List[int]) -> "AbstractQuantumSimulator":
-        """
-        Apply noise that happens after each gate application.
+    def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: list[int]) -> "AbstractQuantumSimulator":
+        """Apply noise that happens after each gate application.
 
         WARNING! This is experimental and the signature of this interface will likely change.
 
@@ -155,15 +151,16 @@ def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: List[in
 
 
 class PyQVM(QAM["PyQVM"]):
+    """A pure python implementation of the Quantum Virtual Machine."""
+
     def __init__(
         self,
         n_qubits: int,
-        quantum_simulator_type: Optional[Type[AbstractQuantumSimulator]] = None,
+        quantum_simulator_type: Optional[type[AbstractQuantumSimulator]] = None,
         seed: Optional[int] = None,
-        post_gate_noise_probabilities: Optional[Dict[str, float]] = None,
+        post_gate_noise_probabilities: Optional[dict[str, float]] = None,
     ):
-        """
-        PyQuil's built-in Quil virtual machine.
+        """PyQuil's built-in Quil virtual machine.
 
         This class implements common control flow and plumbing and dispatches the "actual" work to
         quantum simulators like ReferenceWavefunctionSimulator, ReferenceDensitySimulator,
@@ -192,7 +189,7 @@ def __init__(
                 quantum_simulator_type = ReferenceDensitySimulator
 
         self.n_qubits = n_qubits
-        self.ram: Dict[str, List[Union[float, int]]] = {}
+        self.ram: dict[str, list[Union[float, int]]] = {}
 
         if post_gate_noise_probabilities is None:
             post_gate_noise_probabilities = {}
@@ -200,10 +197,10 @@ def __init__(
 
         self.program: Optional[Program] = None
         self.program_counter: int = 0
-        self.defined_gates: Dict[str, np.ndarray] = dict()
+        self.defined_gates: dict[str, np.ndarray] = dict()
 
         # private implementation details
-        self._qubit_to_ram: Optional[Dict[int, int]] = None
+        self._qubit_to_ram: Optional[dict[int, int]] = None
         self._ro_size: Optional[int] = None
         self._memory_results = {}  # type: ignore
 
@@ -213,7 +210,8 @@ def __init__(
 
     def _extract_defined_gates(self) -> None:
         self.defined_gates = dict()
-        assert self.program is not None
+        if self.program is None:
+            raise RuntimeError("No program loaded into PyQVM.")
         for dg in self.program.defined_gates:
             if dg.parameters is not None and len(dg.parameters) > 0:
                 raise NotImplementedError("PyQVM does not support parameterized DEFGATEs")
@@ -223,15 +221,16 @@ def _extract_defined_gates(self) -> None:
 
     def execute_with_memory_map_batch(
         self, executable: QuantumExecutable, memory_maps: Iterable[MemoryMap], **__: Any
-    ) -> List["PyQVM"]:
+    ) -> list["PyQVM"]:
+        """Operation is not supported by PyQVM as the state of the instance is reset at the start of each execution."""
         raise NotImplementedError(
             "PyQVM does not support batch execution as the state of the instance is reset at the start of each execute."
         )
 
     def execute(self, executable: QuantumExecutable, memory_map: Optional[MemoryMap] = None, **__: Any) -> "PyQVM":
-        """
-        Execute a program on the PyQVM. Note that the state of the instance is reset on each
-        call to ``execute``.
+        """Execute a program on the PyQVM.
+
+        Note that the state of the instance is reset on each call to ``execute``.
 
         :return: ``self`` to support method chaining.
         """
@@ -265,12 +264,13 @@ def execute(self, executable: QuantumExecutable, memory_map: Optional[MemoryMap]
         return self
 
     def get_result(self, execute_response: "PyQVM") -> QAMExecutionResult:
-        """
-        Return results from the PyQVM according to the common QAM API. Note that while the
-        ``execute_response`` is not used, it's accepted in order to conform to that API; it's
+        """Return results from the PyQVM according to the common QAM API.
+
+        Note that while the ``execute_response`` is not used, it's accepted in order to conform to that API; it's
         unused because the PyQVM, unlike other QAM's, is itself stateful.
         """
-        assert self.program is not None
+        if self.program is None:
+            raise RuntimeError("No program loaded into PyQVM.")
         result_data = QVMResultData.from_memory_map(
             {key: RegisterData(matrix.tolist()) for key, matrix in self._memory_results.items()}
         )
@@ -282,18 +282,19 @@ def get_result(self, execute_response: "PyQVM") -> QAMExecutionResult:
         )
 
     def read_memory(self, *, region_name: str) -> np.ndarray:
-        assert self._memory_results is not None
+        """Read memory from the PyQVM."""
+        if self._memory_results is None:
+            raise ValueError("No memory results available.")
         return np.asarray(self._memory_results[region_name])
 
     def find_label(self, label: Union[Label, LabelPlaceholder]) -> int:
-        """
-        Helper function that iterates over the program and looks for a JumpTarget that has a
-        Label matching the input label.
+        """Iterate over the program and find a JumpTarget that has a Label matching the input label.
 
         :param label: Label object to search for in program
         :return: Program index where ``label`` is found
         """
-        assert self.program is not None
+        if self.program is None:
+            raise RuntimeError("No program loaded into PyQVM.")
         for index, action in enumerate(self.program):
             if isinstance(action, JumpTarget):
                 if label == action.label:
@@ -302,8 +303,7 @@ def find_label(self, label: Union[Label, LabelPlaceholder]) -> int:
         raise RuntimeError("Improper program - Jump Target not found in the input program!")
 
     def transition(self) -> bool:
-        """
-        Implements a QAM-like transition.
+        """Perform a QAM-like transition.
 
         This function assumes ``program`` and ``program_counter`` instance variables are set
         appropriately, and that the wavefunction simulator and classical memory ``ram`` instance
@@ -311,7 +311,8 @@ def transition(self) -> bool:
 
         :return: whether the QAM should halt after this transition.
         """
-        assert self.program is not None
+        if self.program is None:
+            raise RuntimeError("No program loaded into PyQVM.")
         instruction = self.program[self.program_counter]
 
         if isinstance(instruction, Gate):
@@ -332,7 +333,8 @@ def transition(self) -> bool:
         elif isinstance(instruction, Measurement):
             measured_val = self.wf_simulator.do_measurement(qubit=instruction.get_qubit_indices().pop())
             meas_reg: Optional[MemoryReference] = instruction.classical_reg
-            assert meas_reg is not None
+            if meas_reg is None:
+                raise ValueError("Measurement instruction must have a classical register.")
             self.ram[meas_reg.name][meas_reg.offset] = measured_val
             self.program_counter += 1
 
@@ -360,10 +362,11 @@ def transition(self) -> bool:
         elif isinstance(instruction, (JumpWhen, JumpUnless)):
             # JumpWhen/Unless; check classical reg
             jump_reg: Optional[MemoryReference] = instruction.condition
-            assert jump_reg is not None
+            if jump_reg is None:
+                raise ValueError("JumpWhen/Unless instruction must have a classical register.")
             cond = self.ram[jump_reg.name][jump_reg.offset]
             if not isinstance(cond, (bool, np.bool_, np.int8, int)):
-                raise ValueError("{} requires a data type of BIT; not {}".format(type(instruction), type(cond)))
+                raise ValueError(f"{type(instruction)} requires a data type of BIT; not {type(cond)}")
             dest_index = self.find_label(instruction.target)
             if isinstance(instruction, JumpWhen):
                 jump_if_cond = True
@@ -385,11 +388,11 @@ def transition(self) -> bool:
             old = self.ram[target.name][target.offset]
             if isinstance(instruction, ClassicalNeg):
                 if not isinstance(old, (int, float, np.int_, np.float_)):
-                    raise ValueError("NEG requires a data type of REAL or INTEGER; not {}".format(type(old)))
+                    raise ValueError(f"NEG requires a data type of REAL or INTEGER; not {type(old)}")
                 self.ram[target.name][target.offset] *= -1
             elif isinstance(instruction, ClassicalNot):
                 if not isinstance(old, (bool, np.bool_)):
-                    raise ValueError("NOT requires a data type of BIT; not {}".format(type(old)))
+                    raise ValueError(f"NOT requires a data type of BIT; not {type(old)}")
                 self.ram[target.name][target.offset] = not old
             else:
                 raise TypeError("Invalid UnaryClassicalInstruction")
@@ -406,13 +409,16 @@ def transition(self) -> bool:
                 right_val = instruction.right
 
             if isinstance(instruction, ClassicalAnd):
-                assert isinstance(left_val, int) and isinstance(right_val, int)
+                if not isinstance(left_val, int) or not isinstance(right_val, int):
+                    raise ValueError("AND requires a data type of INTEGER; not {type(left_val)} and {type(right_val)}")
                 new_val: Union[int, float] = left_val & right_val
             elif isinstance(instruction, ClassicalInclusiveOr):
-                assert isinstance(left_val, int) and isinstance(right_val, int)
+                if not isinstance(left_val, int) or not isinstance(right_val, int):
+                    raise ValueError("OR requires a data type of INTEGER; not {type(left_val)} and {type(right_val)}")
                 new_val = left_val | right_val
             elif isinstance(instruction, ClassicalExclusiveOr):
-                assert isinstance(left_val, int) and isinstance(right_val, int)
+                if not isinstance(left_val, int) or not isinstance(right_val, int):
+                    raise ValueError("XOR requires a data type of INTEGER; not {type(left_val)} and {type(right_val)}")
                 new_val = left_val ^ right_val
             elif isinstance(instruction, ClassicalAdd):
                 new_val = left_val + right_val
@@ -425,7 +431,7 @@ def transition(self) -> bool:
             elif isinstance(instruction, ClassicalMove):
                 new_val = right_val
             else:
-                raise ValueError("Unknown BinaryOp {}".format(type(instruction)))
+                raise ValueError(f"Unknown BinaryOp {type(instruction)}")
             self.ram[left_ind.name][left_ind.offset] = new_val
             self.program_counter += 1
 
@@ -461,16 +467,18 @@ def transition(self) -> bool:
         elif isinstance(instruction, Halt):
             return True
         else:
-            raise ValueError("Unsupported instruction type: {}".format(instruction))
+            raise ValueError(f"Unsupported instruction type: {instruction}")
 
         # return HALTED (i.e. program_counter is end of program)
-        assert self.program is not None
+        if self.program is None:
+            raise ValueError("No program loaded into PyQVM.")
         return self.program_counter == len(self.program)
 
     def _execute_program(self) -> "PyQVM":
         self.program_counter = 0
 
-        assert self.program is not None
+        if self.program is None:
+            raise ValueError("No program loaded into PyQVM.")
         halted = len(self.program) == 0
         while not halted:
             halted = self.transition()
@@ -478,8 +486,7 @@ def _execute_program(self) -> "PyQVM":
         return self
 
     def execute_once(self, program: Program) -> "PyQVM":
-        """
-        Execute one outer loop of a program on the PyQVM without re-initializing its state.
+        """Execute one outer loop of a program on the PyQVM without re-initializing its state.
 
         Note that the PyQVM is stateful. Subsequent calls to :py:func:`execute_once` will not
         automatically reset the wavefunction or the classical RAM. If this is desired,
diff --git a/pyquil/quantum_processor/__init__.py b/pyquil/quantum_processor/__init__.py
index c9caff295..0f69b78d2 100644
--- a/pyquil/quantum_processor/__init__.py
+++ b/pyquil/quantum_processor/__init__.py
@@ -1,8 +1,14 @@
+"""Models and functions for working with quantum processors."""
+
 __all__ = [
     "AbstractQuantumProcessor",
+    "CompilerQuantumProcessor",
+    "NxQuantumProcessor",
+    "QCSQuantumProcessor",
+    "get_qcs_quantum_processor",
 ]
 
 from pyquil.quantum_processor._base import AbstractQuantumProcessor
+from pyquil.quantum_processor.compiler import CompilerQuantumProcessor
 from pyquil.quantum_processor.graph import NxQuantumProcessor
 from pyquil.quantum_processor.qcs import QCSQuantumProcessor, get_qcs_quantum_processor
-from pyquil.quantum_processor.compiler import CompilerQuantumProcessor
diff --git a/pyquil/quantum_processor/_base.py b/pyquil/quantum_processor/_base.py
index 3b0a154e2..161a9f6c3 100644
--- a/pyquil/quantum_processor/_base.py
+++ b/pyquil/quantum_processor/_base.py
@@ -14,32 +14,26 @@
 #    limitations under the License.
 ##############################################################################
 from abc import ABC, abstractmethod
+
 import networkx as nx
-from typing import List
+
 from pyquil.external.rpcq import CompilerISA
 
 
 class AbstractQuantumProcessor(ABC):
-    """
-    A generic interface describing the qubits, topology, and compiler representation
-    of any arbitrary quantum_processor class.
-    """
+    """Interface describing the qubits, topology, and compiler representation of an arbitrary quantum processor."""
 
     @abstractmethod
-    def qubits(self) -> List[int]:
-        """
-        A sorted list of qubits in the quantum_processor topology.
-        """
+    def qubits(self) -> list[int]:
+        """Sort the qubits in the quantum_processor topology into a list."""
 
     @abstractmethod
     def qubit_topology(self) -> nx.Graph:
-        """
-        The connectivity of qubits in this quantum_processor given as a NetworkX graph.
-        """
+        """Return a NetworkX graph that represents the connectivity of qubits in this quantum_processor."""
 
     @abstractmethod
     def to_compiler_isa(self) -> CompilerISA:
-        """
-        Construct an ISA suitable for targeting by compilation.
+        """Construct an ISA suitable for targeting by compilation.
+
         This will raise an exception if the requested ISA is not supported by the quantum_processor.
         """
diff --git a/pyquil/quantum_processor/compiler.py b/pyquil/quantum_processor/compiler.py
index 43d84f192..0f207a499 100644
--- a/pyquil/quantum_processor/compiler.py
+++ b/pyquil/quantum_processor/compiler.py
@@ -1,26 +1,29 @@
-from pyquil.quantum_processor._base import AbstractQuantumProcessor
-from typing import List
-from pyquil.quantum_processor.transformers import compiler_isa_to_graph
-from pyquil.external.rpcq import CompilerISA
+"""An implementation of an AbstractQuantumProcessor initialized with a user constructed ``CompilerISA``."""
 
 import networkx as nx
 
+from pyquil.external.rpcq import CompilerISA
+from pyquil.quantum_processor._base import AbstractQuantumProcessor
+from pyquil.quantum_processor.transformers import compiler_isa_to_graph
+
 
 class CompilerQuantumProcessor(AbstractQuantumProcessor):
-    """
-    An AbstractQuantumProcessor initialized with a user constructed ``CompilerISA``.
-    """
+    """An AbstractQuantumProcessor initialized with a user constructed ``CompilerISA``."""
 
     _isa: CompilerISA
 
     def __init__(self, isa: CompilerISA) -> None:
+        """Initialize a CompilerQuantumProcessor with a given ``CompilerISA``."""
         self._isa = isa
 
     def qubit_topology(self) -> nx.Graph:
+        """Return a NetworkX graph that represents the connectivity of qubits in this quantum_processor."""
         return compiler_isa_to_graph(self._isa)
 
     def to_compiler_isa(self) -> CompilerISA:
+        """Return the CompilerISA that this quantum_processor is initialized with."""
         return self._isa
 
-    def qubits(self) -> List[int]:
+    def qubits(self) -> list[int]:
+        """Return the qubits in the quantum_processor topology as a sorted list."""
         return sorted([int(node_id) for node_id, node in self._isa.qubits.items()])
diff --git a/pyquil/quantum_processor/graph.py b/pyquil/quantum_processor/graph.py
index 6d0d5843a..d9eb49287 100644
--- a/pyquil/quantum_processor/graph.py
+++ b/pyquil/quantum_processor/graph.py
@@ -1,16 +1,17 @@
-from typing import Any, Tuple
+"""An implementation of an AbstractQuantumProcessor based on a NetworkX graph topology."""
 
-from pyquil.quantum_processor._base import AbstractQuantumProcessor
-from pyquil.quantum_processor.transformers import graph_to_compiler_isa
-from typing import List, Optional
-from pyquil.external.rpcq import CompilerISA
+from typing import Any, Optional
 
 import networkx as nx
 
+from pyquil.external.rpcq import CompilerISA
+from pyquil.quantum_processor._base import AbstractQuantumProcessor
+from pyquil.quantum_processor.transformers import graph_to_compiler_isa
+
 
 class NxQuantumProcessor(AbstractQuantumProcessor):
-    """
-    An AbstractQuantumProcessor initialized with a user constructed NetworkX graph topology.
+    """An AbstractQuantumProcessor initialized with a user constructed NetworkX graph topology.
+
     Notably, this class is able to serialize a ``CompilerISA`` based on the
     graph topology and the configured 1Q and 2Q gates.
     """
@@ -18,11 +19,10 @@ class NxQuantumProcessor(AbstractQuantumProcessor):
     def __init__(
         self,
         topology: nx.Graph,
-        gates_1q: Optional[List[str]] = None,
-        gates_2q: Optional[List[str]] = None,
+        gates_1q: Optional[list[str]] = None,
+        gates_2q: Optional[list[str]] = None,
     ) -> None:
-        """
-        Initialize a new NxQuantumProcessor.
+        """Initialize a new NxQuantumProcessor.
 
         :param topology: The graph topology of the quantum_processor.
         :param gates_1q: A list of 1Q gate names supported by all qubits in the quantum_processor.
@@ -33,19 +33,20 @@ def __init__(
         self.gates_2q = gates_2q
 
     def qubit_topology(self) -> nx.Graph:
+        """Return the NetworkX graph that represents the connectivity of qubits in this quantum_processor."""
         return self.topology
 
     def to_compiler_isa(self) -> CompilerISA:
-        """
-        Generate a ``CompilerISA`` object based on a NetworkX graph and the
-        ``gates_1q`` and ``gates_2q`` with which the quantum_processor was initialized.
+        """Generate a ``CompilerISA`` object based on the NetworkX graph, ``gates_1q``, and ``gates_2q``.
 
         May raise ``GraphGateError`` if the specified gates are not supported.
         """
         return graph_to_compiler_isa(self.topology, gates_1q=self.gates_1q, gates_2q=self.gates_2q)
 
-    def qubits(self) -> List[int]:
+    def qubits(self) -> list[int]:
+        """Return a sorted list of qubits in the quantum_processor."""
         return sorted(self.topology.nodes)
 
-    def edges(self) -> List[Tuple[Any, ...]]:
+    def edges(self) -> list[tuple[Any, ...]]:
+        """Return a sorted list of edges in the quantum_processor."""
         return sorted(tuple(sorted(pair)) for pair in self.topology.edges)
diff --git a/pyquil/quantum_processor/qcs.py b/pyquil/quantum_processor/qcs.py
index 148dcf6ca..f00101020 100644
--- a/pyquil/quantum_processor/qcs.py
+++ b/pyquil/quantum_processor/qcs.py
@@ -1,7 +1,8 @@
-from typing import List, Optional
+"""An implementation of AbstractQuantumProcessor based on an InstructionSetArchitecture returned from the QCS API."""
 
-import networkx as nx
+from typing import Optional
 
+import networkx as nx
 from qcs_sdk import QCSClient
 from qcs_sdk.qpu.isa import InstructionSetArchitecture, get_instruction_set_architecture
 
@@ -12,10 +13,9 @@
 
 
 class QCSQuantumProcessor(AbstractQuantumProcessor):
-    """
-    An AbstractQuantumProcessor initialized with an ``InstructionSetArchitecture`` returned
-    from the QCS API. Notably, this class is able to serialize a ``CompilerISA`` based
-    on the architecture instructions.
+    """An AbstractQuantumProcessor initialized with an ``InstructionSetArchitecture`` returned from the QCS API.
+
+    Notably, this class is able to serialize a ``CompilerISA`` based on the architecture instructions.
     """
 
     quantum_processor_id: str
@@ -28,31 +28,34 @@ def __init__(
         isa: InstructionSetArchitecture,
         noise_model: Optional[NoiseModel] = None,
     ):
-        """
-        Initialize a new QCSQuantumProcessor.
+        """Initialize a new QCSQuantumProcessor.
 
         :param quantum_processor_id: The id of the quantum processor.
         :param isa: The QCS API ``InstructionSetArchitecture``.
         :param noise_model: An optional ``NoiseModel`` for configuring a noisy quantum_processor on the ``QVM``.
         """
-
         self.quantum_processor_id = quantum_processor_id
         self._isa = isa
         self.noise_model = noise_model
 
-    def qubits(self) -> List[int]:
+    def qubits(self) -> list[int]:
+        """Return the qubits in the quantum_processor topology."""
         return sorted(node.node_id for node in self._isa.architecture.nodes)
 
     def qubit_topology(self) -> nx.Graph:
+        """Return the qubit topology as a NetworkX graph."""
         return qcs_isa_to_graph(self._isa)
 
     def to_compiler_isa(self) -> CompilerISA:
+        """Return a CompilerISA representation of the quantum_processor's InstructionSetArchitecture."""
         return qcs_isa_to_compiler_isa(self._isa)
 
     def __str__(self) -> str:
-        return "<QCSQuantumProcessor {}>".format(self.quantum_processor_id)
+        """Return a string representation of the quantum_processor."""
+        return f"<QCSQuantumProcessor {self.quantum_processor_id}>"
 
     def __repr__(self) -> str:
+        """Return a string representation of the quantum_processor."""
         return str(self)
 
 
@@ -61,9 +64,7 @@ def get_qcs_quantum_processor(
     client_configuration: Optional[QCSClient] = None,
     timeout: float = 10.0,
 ) -> QCSQuantumProcessor:
-    """
-    Retrieve an instruction set architecture for the specified ``quantum_processor_id`` and initialize a
-    ``QCSQuantumProcessor`` with it.
+    """Retrieve an instruction set architecture for the specified QPU ID and initialize a ``QCSQuantumProcessor`` with it.
 
     :param quantum_processor_id: QCS ID for the quantum processor.
     :param timeout: Time limit for request, in seconds.
diff --git a/pyquil/quantum_processor/transformers/__init__.py b/pyquil/quantum_processor/transformers/__init__.py
index e85c6043c..28174ea22 100644
--- a/pyquil/quantum_processor/transformers/__init__.py
+++ b/pyquil/quantum_processor/transformers/__init__.py
@@ -1,7 +1,18 @@
+"""Various transformers for quantum processor ISA representations."""
+
+__all__ = [
+    "compiler_isa_to_graph",
+    "graph_to_compiler_isa",
+    "QCSISAParseError",
+    "qcs_isa_to_compiler_isa",
+    "qcs_isa_to_graph",
+    "GraphGateError",
+]
+
+from pyquil.quantum_processor.transformers.compiler_isa_to_graph import compiler_isa_to_graph
+from pyquil.quantum_processor.transformers.graph_to_compiler_isa import GraphGateError, graph_to_compiler_isa
 from pyquil.quantum_processor.transformers.qcs_isa_to_compiler_isa import (
-    qcs_isa_to_compiler_isa,
     QCSISAParseError,
+    qcs_isa_to_compiler_isa,
 )
 from pyquil.quantum_processor.transformers.qcs_isa_to_graph import qcs_isa_to_graph
-from pyquil.quantum_processor.transformers.compiler_isa_to_graph import compiler_isa_to_graph
-from pyquil.quantum_processor.transformers.graph_to_compiler_isa import graph_to_compiler_isa, GraphGateError
diff --git a/pyquil/quantum_processor/transformers/compiler_isa_to_graph.py b/pyquil/quantum_processor/transformers/compiler_isa_to_graph.py
index ed8d023d1..ce9063faa 100644
--- a/pyquil/quantum_processor/transformers/compiler_isa_to_graph.py
+++ b/pyquil/quantum_processor/transformers/compiler_isa_to_graph.py
@@ -1,10 +1,10 @@
-from pyquil.external.rpcq import CompilerISA
+"""Transform a ``CompilerISA`` to an ``nx.Graph``."""
 
 import networkx as nx
 
+from pyquil.external.rpcq import CompilerISA
+
 
 def compiler_isa_to_graph(compiler_isa: CompilerISA) -> nx.Graph:
-    """
-    Generate an ``nx.Graph`` based on the qubits and edges of any ``CompilerISA``.
-    """
+    """Generate an ``nx.Graph`` based on the qubits and edges of any ``CompilerISA``."""
     return nx.from_edgelist([int(i) for i in edge.ids] for edge in compiler_isa.edges.values())
diff --git a/pyquil/quantum_processor/transformers/graph_to_compiler_isa.py b/pyquil/quantum_processor/transformers/graph_to_compiler_isa.py
index 785ed8d87..ae0b1a31c 100644
--- a/pyquil/quantum_processor/transformers/graph_to_compiler_isa.py
+++ b/pyquil/quantum_processor/transformers/graph_to_compiler_isa.py
@@ -1,20 +1,20 @@
-from typing import Union
+"""Transformers for converting between NetworkX graphs and CompilerISAs."""
 
+from typing import Optional, Union, cast
+
+import networkx as nx
 import numpy as np
-from typing import List, Optional, cast
+
 from pyquil.external.rpcq import (
+    CompilerISA,
     GateInfo,
     MeasureInfo,
     Supported1QGate,
     Supported2QGate,
-    CompilerISA,
-    add_qubit,
     add_edge,
+    add_qubit,
 )
 
-import networkx as nx
-
-
 DEFAULT_1Q_GATES = [
     Supported1QGate.I,
     Supported1QGate.RX,
@@ -28,10 +28,10 @@
 
 
 def graph_to_compiler_isa(
-    graph: nx.Graph, gates_1q: Optional[List[str]] = None, gates_2q: Optional[List[str]] = None
+    graph: nx.Graph, gates_1q: Optional[list[str]] = None, gates_2q: Optional[list[str]] = None
 ) -> CompilerISA:
-    """
-    Generate an ``CompilerISA`` object from a NetworkX graph and list of 1Q and 2Q gates.
+    """Generate an ``CompilerISA`` object from a NetworkX graph and list of 1Q and 2Q gates.
+
     May raise ``GraphGateError`` if the specified gates are not supported.
 
     :param graph: The graph topology of the quantum_processor.
@@ -67,85 +67,83 @@ def graph_to_compiler_isa(
 
 
 def compiler_isa_to_graph(compiler_isa: CompilerISA) -> nx.Graph:
-    """
-    Generate an ``nx.Graph`` based on the qubits and edges of any ``CompilerISA``.
-    """
+    """Generate an ``nx.Graph`` based on the qubits and edges of any ``CompilerISA``."""
     return nx.from_edgelist([int(i) for i in edge.ids] for edge in compiler_isa.edges.values())
 
 
 class GraphGateError(ValueError):
-    """
-    Signals an error when creating a ``CompilerISA`` from an ``nx.Graph``.
+    """Signals an error when creating a ``CompilerISA`` from an ``nx.Graph``.
+
     This may raise as a consequence of unsupported gates.
     """
 
     pass
 
 
-def _make_i_gates() -> List[GateInfo]:
+def _make_i_gates() -> list[GateInfo]:
     return [GateInfo(operator=Supported1QGate.I, parameters=[], arguments=["_"])]
 
 
-def _make_measure_gates() -> List[MeasureInfo]:
+def _make_measure_gates() -> list[MeasureInfo]:
     return [
         MeasureInfo(operator=Supported1QGate.MEASURE, qubit="_", target="_"),
         MeasureInfo(operator=Supported1QGate.MEASURE, qubit="_", target=None),
     ]
 
 
-def _make_rx_gates() -> List[GateInfo]:
+def _make_rx_gates() -> list[GateInfo]:
     gates = []
     for param in [0.0, np.pi, -np.pi, np.pi / 2, -np.pi / 2]:
         gates.append(GateInfo(operator=Supported1QGate.RX, parameters=[param], arguments=["_"]))
     return gates
 
 
-def _make_rz_gates() -> List[GateInfo]:
+def _make_rz_gates() -> list[GateInfo]:
     return [GateInfo(operator=Supported1QGate.RZ, parameters=["theta"], arguments=["_"])]
 
 
-def _make_wildcard_1q_gates() -> List[GateInfo]:
+def _make_wildcard_1q_gates() -> list[GateInfo]:
     return [GateInfo(operator="_", parameters=["_"], arguments=["_"])]
 
 
 def _transform_qubit_operation_to_gates(
     operation_name: str,
-) -> List[Union[GateInfo, MeasureInfo]]:
+) -> list[Union[GateInfo, MeasureInfo]]:
     if operation_name == Supported1QGate.I:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_i_gates())
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_i_gates())
     elif operation_name == Supported1QGate.RX:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_rx_gates())
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_rx_gates())
     elif operation_name == Supported1QGate.RZ:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_rz_gates())
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_rz_gates())
     elif operation_name == Supported1QGate.MEASURE:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_measure_gates())
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_measure_gates())
     elif operation_name == Supported1QGate.WILDCARD:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_wildcard_1q_gates())
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_wildcard_1q_gates())
     else:
-        raise GraphGateError("Unsupported graph qubit operation: {}".format(operation_name))
+        raise GraphGateError(f"Unsupported graph qubit operation: {operation_name}")
 
 
-def _make_cz_gates() -> List[GateInfo]:
+def _make_cz_gates() -> list[GateInfo]:
     return [GateInfo(operator=Supported2QGate.CZ, parameters=[], arguments=["_", "_"])]
 
 
-def _make_iswap_gates() -> List[GateInfo]:
+def _make_iswap_gates() -> list[GateInfo]:
     return [GateInfo(operator=Supported2QGate.ISWAP, parameters=[], arguments=["_", "_"])]
 
 
-def _make_cphase_gates() -> List[GateInfo]:
+def _make_cphase_gates() -> list[GateInfo]:
     return [GateInfo(operator=Supported2QGate.CPHASE, parameters=["theta"], arguments=["_", "_"])]
 
 
-def _make_xy_gates() -> List[GateInfo]:
+def _make_xy_gates() -> list[GateInfo]:
     return [GateInfo(operator=Supported2QGate.XY, parameters=["theta"], arguments=["_", "_"])]
 
 
-def _make_wildcard_2q_gates() -> List[GateInfo]:
+def _make_wildcard_2q_gates() -> list[GateInfo]:
     return [GateInfo(operator="_", parameters=["_"], arguments=["_", "_"])]
 
 
-def _transform_edge_operation_to_gates(operation_name: str) -> List[GateInfo]:
+def _transform_edge_operation_to_gates(operation_name: str) -> list[GateInfo]:
     if operation_name == Supported2QGate.CZ:
         return _make_cz_gates()
     elif operation_name == Supported2QGate.ISWAP:
@@ -157,4 +155,4 @@ def _transform_edge_operation_to_gates(operation_name: str) -> List[GateInfo]:
     elif operation_name == Supported2QGate.WILDCARD:
         return _make_wildcard_2q_gates()
     else:
-        raise GraphGateError("Unsupported graph edge operation: {}".format(operation_name))
+        raise GraphGateError(f"Unsupported graph edge operation: {operation_name}")
diff --git a/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py b/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py
index d1410161e..c26b0c484 100644
--- a/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py
+++ b/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py
@@ -1,30 +1,37 @@
+"""Transforms a QCS ``InstructionSetArchitecture`` into a ``CompilerISA``."""
+
 from collections import defaultdict
-from typing import List, Union, cast, DefaultDict, Set, Sequence, Optional
+from collections.abc import Sequence
+from typing import Optional, Union, cast
 
 import numpy as np
-from qcs_sdk.qpu.isa import InstructionSetArchitecture, Characteristic, Operation
+from qcs_sdk.qpu.isa import Characteristic, InstructionSetArchitecture, Operation
 
-from pyquil.external.rpcq import CompilerISA, add_edge, add_qubit, get_qubit, get_edge
 from pyquil.external.rpcq import (
+    CompilerISA,
     GateInfo,
     MeasureInfo,
     Supported1QGate,
     Supported2QGate,
+    add_edge,
+    add_qubit,
+    get_edge,
+    get_qubit,
     make_edge_id,
 )
 
 
 class QCSISAParseError(ValueError):
-    """
-    Signals an error when creating a ``CompilerISA`` due to the operators
-    in the QCS ``InstructionSetArchitecture``. This may raise as a consequence
-    of unsupported gates as well as missing nodes or edges.
+    """Signals an error when creating a ``CompilerISA`` due to the operators in the QCS ``InstructionSetArchitecture``.
+
+    This may raise as a consequence of unsupported gates as well as missing nodes or edges.
     """
 
     pass
 
 
 def qcs_isa_to_compiler_isa(isa: InstructionSetArchitecture) -> CompilerISA:
+    """Transform a QCS ``InstructionSetArchitecture`` into a ``CompilerISA``."""
     device = CompilerISA()
     for node in isa.architecture.nodes:
         add_qubit(device, node.node_id)
@@ -32,8 +39,8 @@ def qcs_isa_to_compiler_isa(isa: InstructionSetArchitecture) -> CompilerISA:
     for edge in isa.architecture.edges:
         add_edge(device, edge.node_ids[0], edge.node_ids[1])
 
-    qubit_operations_seen: DefaultDict[int, Set[str]] = defaultdict(set)
-    edge_operations_seen: DefaultDict[str, Set[str]] = defaultdict(set)
+    qubit_operations_seen: defaultdict[int, set[str]] = defaultdict(set)
+    edge_operations_seen: defaultdict[str, set[str]] = defaultdict(set)
     for operation in isa.instructions:
         for site in operation.sites:
             if operation.node_count == 1:
@@ -82,7 +89,7 @@ def qcs_isa_to_compiler_isa(isa: InstructionSetArchitecture) -> CompilerISA:
                 operation_edge.gates.extend(_transform_edge_operation_to_gates(operation.name, site.characteristics))
 
             else:
-                raise QCSISAParseError("unexpected operation node count: {}".format(operation.node_count))
+                raise QCSISAParseError(f"unexpected operation node count: {operation.node_count}")
     for qubit in device.qubits.values():
         if len(qubit.gates) == 0:
             qubit.dead = True
@@ -116,7 +123,7 @@ def qcs_isa_to_compiler_isa(isa: InstructionSetArchitecture) -> CompilerISA:
 }
 
 
-def _make_measure_gates(node_id: int, characteristics: Sequence[Characteristic]) -> List[MeasureInfo]:
+def _make_measure_gates(node_id: int, characteristics: Sequence[Characteristic]) -> list[MeasureInfo]:
     duration = _operation_names_to_compiler_duration_default[Supported1QGate.MEASURE]
     fidelity = _operation_names_to_compiler_fidelity_default[Supported1QGate.MEASURE]
     for characteristic in characteristics:
@@ -142,7 +149,7 @@ def _make_measure_gates(node_id: int, characteristics: Sequence[Characteristic])
     ]
 
 
-def _make_rx_gates(node_id: int, benchmarks: Sequence[Operation]) -> List[GateInfo]:
+def _make_rx_gates(node_id: int, benchmarks: Sequence[Operation]) -> list[GateInfo]:
     default_duration = _operation_names_to_compiler_duration_default[Supported1QGate.RX]
     default_fidelity = _operation_names_to_compiler_fidelity_default[Supported1QGate.RX]
 
@@ -172,7 +179,7 @@ def _make_rx_gates(node_id: int, benchmarks: Sequence[Operation]) -> List[GateIn
     return gates
 
 
-def _make_rz_gates(node_id: int) -> List[GateInfo]:
+def _make_rz_gates(node_id: int) -> list[GateInfo]:
     return [
         GateInfo(
             operator=Supported1QGate.RZ,
@@ -207,7 +214,7 @@ def _get_frb_sim_1q(node_id: int, benchmarks: Sequence[Operation]) -> Optional[f
     return site.value
 
 
-def _make_wildcard_1q_gates(node_id: int) -> List[GateInfo]:
+def _make_wildcard_1q_gates(node_id: int) -> list[GateInfo]:
     return [
         GateInfo(
             operator="_",
@@ -224,22 +231,22 @@ def _transform_qubit_operation_to_gates(
     node_id: int,
     characteristics: Sequence[Characteristic],
     benchmarks: Sequence[Operation],
-) -> List[Union[GateInfo, MeasureInfo]]:
+) -> list[Union[GateInfo, MeasureInfo]]:
     if operation_name == Supported1QGate.RX:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_rx_gates(node_id, benchmarks))
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_rx_gates(node_id, benchmarks))
     elif operation_name == Supported1QGate.RZ:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_rz_gates(node_id))
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_rz_gates(node_id))
     elif operation_name == Supported1QGate.MEASURE:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_measure_gates(node_id, characteristics))
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_measure_gates(node_id, characteristics))
     elif operation_name == Supported1QGate.WILDCARD:
-        return cast(List[Union[GateInfo, MeasureInfo]], _make_wildcard_1q_gates(node_id))
+        return cast(list[Union[GateInfo, MeasureInfo]], _make_wildcard_1q_gates(node_id))
     elif operation_name in {"I", "RESET"}:
         return []
     else:
-        raise QCSISAParseError("Unsupported qubit operation: {}".format(operation_name))
+        raise QCSISAParseError(f"Unsupported qubit operation: {operation_name}")
 
 
-def _make_cz_gates(characteristics: Sequence[Characteristic]) -> List[GateInfo]:
+def _make_cz_gates(characteristics: Sequence[Characteristic]) -> list[GateInfo]:
     default_duration = _operation_names_to_compiler_duration_default[Supported2QGate.CZ]
     default_fidelity = _operation_names_to_compiler_fidelity_default[Supported2QGate.CZ]
 
@@ -260,7 +267,7 @@ def _make_cz_gates(characteristics: Sequence[Characteristic]) -> List[GateInfo]:
     ]
 
 
-def _make_iswap_gates(characteristics: Sequence[Characteristic]) -> List[GateInfo]:
+def _make_iswap_gates(characteristics: Sequence[Characteristic]) -> list[GateInfo]:
     default_duration = _operation_names_to_compiler_duration_default[Supported2QGate.ISWAP]
     default_fidelity = _operation_names_to_compiler_fidelity_default[Supported2QGate.ISWAP]
 
@@ -281,7 +288,7 @@ def _make_iswap_gates(characteristics: Sequence[Characteristic]) -> List[GateInf
     ]
 
 
-def _make_cphase_gates(characteristics: Sequence[Characteristic]) -> List[GateInfo]:
+def _make_cphase_gates(characteristics: Sequence[Characteristic]) -> list[GateInfo]:
     default_duration = _operation_names_to_compiler_duration_default[Supported2QGate.CPHASE]
     default_fidelity = _operation_names_to_compiler_fidelity_default[Supported2QGate.CPHASE]
 
@@ -302,7 +309,7 @@ def _make_cphase_gates(characteristics: Sequence[Characteristic]) -> List[GateIn
     ]
 
 
-def _make_xy_gates(characteristics: Sequence[Characteristic]) -> List[GateInfo]:
+def _make_xy_gates(characteristics: Sequence[Characteristic]) -> list[GateInfo]:
     default_duration = _operation_names_to_compiler_duration_default[Supported2QGate.XY]
     default_fidelity = _operation_names_to_compiler_fidelity_default[Supported2QGate.XY]
 
@@ -323,7 +330,7 @@ def _make_xy_gates(characteristics: Sequence[Characteristic]) -> List[GateInfo]:
     ]
 
 
-def _make_wildcard_2q_gates() -> List[GateInfo]:
+def _make_wildcard_2q_gates() -> list[GateInfo]:
     return [
         GateInfo(
             operator="_",
@@ -338,7 +345,7 @@ def _make_wildcard_2q_gates() -> List[GateInfo]:
 def _transform_edge_operation_to_gates(
     operation_name: str,
     characteristics: Sequence[Characteristic],
-) -> List[GateInfo]:
+) -> list[GateInfo]:
     if operation_name == Supported2QGate.CZ:
         return _make_cz_gates(characteristics)
     elif operation_name == Supported2QGate.ISWAP:
@@ -350,4 +357,4 @@ def _transform_edge_operation_to_gates(
     elif operation_name == Supported2QGate.WILDCARD:
         return _make_wildcard_2q_gates()
     else:
-        raise QCSISAParseError("Unsupported edge operation: {}".format(operation_name))
+        raise QCSISAParseError(f"Unsupported edge operation: {operation_name}")
diff --git a/pyquil/quantum_processor/transformers/qcs_isa_to_graph.py b/pyquil/quantum_processor/transformers/qcs_isa_to_graph.py
index 3d7b5e7b9..141d77db9 100644
--- a/pyquil/quantum_processor/transformers/qcs_isa_to_graph.py
+++ b/pyquil/quantum_processor/transformers/qcs_isa_to_graph.py
@@ -1,6 +1,9 @@
+"""Convert a QCS Instruction Set Architecture to a NetworkX graph."""
+
 import networkx as nx
 from qcs_sdk.qpu.isa import InstructionSetArchitecture
 
 
 def qcs_isa_to_graph(isa: InstructionSetArchitecture) -> nx.Graph:
+    """Convert a QCS Instruction Set Architecture to a NetworkX graph."""
     return nx.from_edgelist(edge.node_ids for edge in isa.architecture.edges)
diff --git a/pyquil/quil.py b/pyquil/quil.py
index b741382d2..762da7273 100644
--- a/pyquil/quil.py
+++ b/pyquil/quil.py
@@ -13,91 +13,82 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-"""
-Module for creating and defining Quil programs.
-"""
+"""Module for creating and defining Quil programs."""
+
+import functools
 import types
+import warnings
 from collections import defaultdict
+from collections.abc import Generator, Iterable, Iterator, Sequence
 from copy import deepcopy
-import functools
 from typing import (
     Any,
     Callable,
-    Dict,
-    Generator,
-    List,
-    Iterable,
-    Iterator,
     Optional,
-    Sequence,
-    Set,
-    Tuple,
     TypeVar,
     Union,
     cast,
 )
-import warnings
 
-from deprecated.sphinx import deprecated
 import numpy as np
-
+import quil.instructions as quil_rs
+from deprecated.sphinx import deprecated
 from qcs_sdk.compiler.quilc import NativeQuilMetadata
+from quil.program import CalibrationSet
+from quil.program import Program as RSProgram
 
 from pyquil.control_flow_graph import ControlFlowGraph
 from pyquil.gates import MEASURE, RESET
 from pyquil.noise import _check_kraus_ops, _create_kraus_pragmas, pauli_kraus_map
 from pyquil.quilatom import (
+    FormalArgument,
+    Frame,
     Label,
     LabelPlaceholder,
     MemoryReference,
     MemoryReferenceDesignator,
     Parameter,
     ParameterDesignator,
-    Frame,
     Qubit,
     QubitDesignator,
     QubitPlaceholder,
-    FormalArgument,
     format_parameter,
     unpack_classical_reg,
     unpack_qubit,
 )
 from pyquil.quilbase import (
-    DefGate,
-    Gate,
-    Measurement,
-    Pragma,
     AbstractInstruction,
-    Jump,
-    JumpTarget,
-    JumpUnless,
-    JumpWhen,
     Declare,
     DefCalibration,
     DefFrame,
+    DefGate,
     DefMeasureCalibration,
     DefWaveform,
-    _convert_to_rs_instruction,
-    _convert_to_rs_instructions,
-    _convert_to_py_instructions,
+    Gate,
+    Jump,
+    JumpTarget,
+    JumpUnless,
+    JumpWhen,
+    Measurement,
+    Pragma,
     _convert_to_py_instruction,
+    _convert_to_py_instructions,
     _convert_to_py_qubits,
+    _convert_to_rs_instruction,
+    _convert_to_rs_instructions,
 )
 from pyquil.quiltcalibrations import (
     CalibrationMatch,
     _convert_to_calibration_match,
 )
 
-from quil.program import CalibrationSet, Program as RSProgram
-import quil.instructions as quil_rs
-
 InstructionDesignator = Union[
     AbstractInstruction,
     quil_rs.Instruction,
     "Program",
     RSProgram,
     Sequence[Any],
-    Tuple[Any, ...],
+    tuple[Any, ...],
     str,  # required to be a pyquil program
     Generator[Any, Any, Any],
 ]
@@ -107,10 +98,10 @@
 
 
 def _invalidates_cached_properties(func: Callable[..., RetType]) -> Callable[..., RetType]:
-    """A decorator that will check a class for any cached properties and clear them.
+    """Check a class for any cached properties and clear them.
 
     This should be used on any `Program` method that changes the internal state of the program
-    so that the next call to proprety rebuilds the cache.
+    so that the next call to property rebuilds the cache.
     """
 
     @functools.wraps(func)
@@ -131,8 +122,7 @@ def wrapper(self: "Program", *args: Any, **kwargs: Any) -> RetType:
 
 
 class Program:
-    """
-    A list of pyQuil instructions that comprise a quantum program.
+    """A list of pyQuil instructions that comprise a quantum program.
 
     >>> from pyquil import Program
     >>> from pyquil.gates import H, CNOT
@@ -142,6 +132,7 @@ class Program:
     """
 
     def __init__(self, *instructions: InstructionDesignator):
+        """Initialize a Program."""
         self._program: RSProgram = RSProgram()
         self.inst(*instructions)
 
@@ -155,20 +146,20 @@ def __init__(self, *instructions: InstructionDesignator):
     # decorator to clear these caches.
 
     @functools.cached_property
-    def calibrations(self) -> List[DefCalibration]:
+    def calibrations(self) -> list[DefCalibration]:
         """A list of Quil-T calibration definitions."""
         return [DefCalibration._from_rs_calibration(cal) for cal in self._program.calibrations.calibrations]
 
     @functools.cached_property
-    def measure_calibrations(self) -> List[DefMeasureCalibration]:
-        """A list of measure calibrations"""
+    def measure_calibrations(self) -> list[DefMeasureCalibration]:
+        """A list of measure calibrations."""
         return [
             DefMeasureCalibration._from_rs_measure_calibration_definition(cal)
             for cal in self._program.calibrations.measure_calibrations
         ]
 
     @functools.cached_property
-    def waveforms(self) -> Dict[str, DefWaveform]:
+    def waveforms(self) -> dict[str, DefWaveform]:
         """A mapping from waveform names to their corresponding definitions."""
         return {
             name: DefWaveform._from_rs_waveform_definition(quil_rs.WaveformDefinition(name, waveform))
@@ -176,7 +167,7 @@ def waveforms(self) -> Dict[str, DefWaveform]:
         }
 
     @functools.cached_property
-    def frames(self) -> Dict[Frame, DefFrame]:
+    def frames(self) -> dict[Frame, DefFrame]:
         """A mapping from Quil-T frames to their definitions."""
         return {
             Frame._from_rs_frame_identifier(frame): DefFrame._from_rs_attribute_values(frame, attributes)
@@ -184,13 +175,12 @@ def frames(self) -> Dict[Frame, DefFrame]:
         }
 
     @functools.cached_property
-    def declarations(self) -> Dict[str, Declare]:
+    def declarations(self) -> dict[str, Declare]:
         """A mapping from declared region names to their declarations."""
         return {name: Declare._from_rs_declaration(inst) for name, inst in self._program.declarations.items()}
 
     def copy_everything_except_instructions(self) -> "Program":
-        """
-        Copy all the members that live on a Program object.
+        """Copy all the members that live on a Program object.
 
         :return: a new Program
         """
@@ -207,8 +197,7 @@ def copy_everything_except_instructions(self) -> "Program":
         return new_prog
 
     def copy(self) -> "Program":
-        """
-        Perform a deep copy of this program.
+        """Perform a deep copy of this program.
 
         :return: a new Program
         """
@@ -218,47 +207,42 @@ def copy(self) -> "Program":
         return new_program
 
     @property
-    def defined_gates(self) -> List[DefGate]:
-        """
-        A list of defined gates on the program.
-        """
+    def defined_gates(self) -> list[DefGate]:
+        """A list of defined gates on the program."""
         return [DefGate._from_rs_gate_definition(gate) for gate in self._program.gate_definitions.values()]
 
     @property
-    def instructions(self) -> List[AbstractInstruction]:
-        """
-        Fill in any placeholders and return a list of quil AbstractInstructions.
-        """
+    def instructions(self) -> list[AbstractInstruction]:
+        """Fill in any placeholders and return a list of quil AbstractInstructions."""
         return list(self.declarations.values()) + _convert_to_py_instructions(self._program.body_instructions)
 
     @instructions.setter
-    def instructions(self, instructions: List[AbstractInstruction]) -> None:
+    def instructions(self, instructions: list[AbstractInstruction]) -> None:
         new_program = self.copy_everything_except_instructions()
         new_program.inst(instructions)
         self._program = new_program._program
 
     @_invalidates_cached_properties
     def inst(self, *instructions: Union[InstructionDesignator, RSProgram]) -> "Program":
-        """
-        Mutates the Program object by appending new instructions.
+        """Mutates the Program object by appending new instructions.
 
         This function accepts a number of different valid forms, e.g.
 
             >>> from pyquil import Program
             >>> from pyquil.gates import H
             >>> p = Program()
-            >>> p.inst(H(0)) # A single instruction
+            >>> p.inst(H(0))  # A single instruction
             Program { ... }
-            >>> p.inst(H(0), H(1)) # Multiple instructions
+            >>> p.inst(H(0), H(1))  # Multiple instructions
             Program { ... }
-            >>> p.inst([H(0), H(1)]) # A list of instructions
+            >>> p.inst([H(0), H(1)])  # A list of instructions
             Program { ... }
-            >>> p.inst(H(i) for i in range(4)) # A generator of instructions
+            >>> p.inst(H(i) for i in range(4))  # A generator of instructions
             Program { ... }
-            >>> p.inst("H 0") # A string representing an instruction
+            >>> p.inst("H 0")  # A string representing an instruction
             Program { ... }
             >>> q = Program()
-            >>> p.inst(q) # Another program
+            >>> p.inst(q)  # Another program
             Program { ... }
 
         It can also be chained:
@@ -294,7 +278,7 @@ def inst(self, *instructions: Union[InstructionDesignator, RSProgram]) -> "Progr
                         else:
                             self.measure(instruction[1], instruction[2])
                     else:
-                        params: List[ParameterDesignator] = []
+                        params: list[ParameterDesignator] = []
                         possible_params = instruction[1]
                         rest: Sequence[Any] = instruction[2:]
                         if isinstance(possible_params, list):
@@ -316,8 +300,8 @@ def inst(self, *instructions: Union[InstructionDesignator, RSProgram]) -> "Progr
                 try:
                     instruction = quil_rs.Instruction(instruction)  # type: ignore
                     self.inst(instruction)
-                except ValueError:
-                    raise ValueError("Invalid instruction: {}, type: {}".format(instruction, type(instruction)))
+                except ValueError as e:
+                    raise ValueError(f"Invalid instruction: {instruction}, type: {type(instruction)}") from e
 
         return self
 
@@ -332,8 +316,7 @@ def with_loop(
         start_label: Union[Label, LabelPlaceholder],
         end_label: Union[Label, LabelPlaceholder],
     ) -> "Program":
-        """
-        Return a copy of the ``Program`` wrapped in a Quil loop that will execute ``num_iterations`` times.
+        r"""Return a copy of the ``Program`` wrapped in a Quil loop that will execute ``num_iterations`` times.
 
         This loop is implemented with Quil and should not be confused with the ``num_shots`` property set by
         :py:meth:`~pyquil.quil.Program.wrap_in_numshots_loop`. The latter is metadata on the program that
@@ -371,9 +354,9 @@ def with_loop(
 
     @_invalidates_cached_properties
     def resolve_placeholders(self) -> None:
-        """
-        Resolve all label and qubit placeholders in the program using a default resolver that will generate
-        a unique value for each placeholder within the scope of the program.
+        """Resolve all label and qubit placeholders in the program.
+
+        A default resolver will generate a unique value for each placeholder within the scope of the program.
         """
         self._program.resolve_placeholders()
 
@@ -383,9 +366,7 @@ def resolve_placeholders_with_custom_resolvers(
         label_resolver: Optional[Callable[[LabelPlaceholder], Optional[str]]] = None,
         qubit_resolver: Optional[Callable[[QubitPlaceholder], Optional[int]]] = None,
     ) -> None:
-        """
-        Resolve ``LabelPlaceholder``\\s and ``QubitPlaceholder``\\s within the program using a function
-        to provide resolved values for the placeholdres.
+        r"""Resolve ``LabelPlaceholder``\\s and ``QubitPlaceholder``\\s within the program using a function.
 
         If you provide ``label_resolver`` and/or ``qubit_resolver``, they will be used to resolve those values
         respectively. If your resolver returns `None` for a particular placeholder, it will not be replaced but
@@ -413,17 +394,12 @@ def rs_label_resolver(placeholder: quil_rs.TargetPlaceholder) -> Optional[str]:
 
     @_invalidates_cached_properties
     def resolve_qubit_placeholders(self) -> None:
-        """
-        Resolve all qubit placeholders in the program.
-        """
+        """Resolve all qubit placeholders in the program."""
         self._program.resolve_placeholders_with_custom_resolvers(target_resolver=lambda _: None)
 
     @_invalidates_cached_properties
-    def resolve_qubit_placeholders_with_mapping(self, qubit_mapping: Dict[QubitPlaceholder, int]) -> None:
-        """
-        Resolve all qubit placeholders in the program using a mapping of ``QubitPlaceholder``\\s to
-        the index they should resolve to.
-        """
+    def resolve_qubit_placeholders_with_mapping(self, qubit_mapping: dict[QubitPlaceholder, int]) -> None:
+        r"""Resolve all qubit placeholders using a mapping of ``QubitPlaceholder``\\s to the index they resolve to."""
 
         def qubit_resolver(placeholder: quil_rs.QubitPlaceholder) -> Optional[int]:
             return qubit_mapping.get(QubitPlaceholder(placeholder), None)
@@ -437,14 +413,12 @@ def label_resolver(_: quil_rs.TargetPlaceholder) -> None:
 
     @_invalidates_cached_properties
     def resolve_label_placeholders(self) -> None:
-        """
-        Resolve all label placeholders in the program.
-        """
+        """Resolve all label placeholders in the program."""
         self._program.resolve_placeholders_with_custom_resolvers(qubit_resolver=lambda _: None)
 
     def _add_instruction(self, instruction: quil_rs.Instruction) -> None:
-        """
-        A helper method that adds an instruction to the Program after normalizing to a `quil_rs.Instruction`.
+        """Add an instruction to the Program after normalizing to a `quil_rs.Instruction`.
+
         For backwards compatibility, it also prevents duplicate calibration, measurement, and gate definitions from
         being added. Users of ``Program`` should use ``inst`` or ``Program`` addition instead.
         """
@@ -467,7 +441,7 @@ def _add_instruction(self, instruction: quil_rs.Instruction) -> None:
                 existing_calibration.instructions != defcal.instructions
                 or existing_calibration.modifiers != defcal.modifiers
             ):
-                warnings.warn("Redefining calibration {}".format(defcal.name))
+                warnings.warn(f"Redefining calibration {defcal.name}", stacklevel=2)
                 current_calibrations = self._program.calibrations
                 new_calibrations = CalibrationSet(
                     current_calibrations.calibrations[:idx] + [defcal] + current_calibrations.calibrations[idx + 1 :],
@@ -489,7 +463,7 @@ def _add_instruction(self, instruction: quil_rs.Instruction) -> None:
                 self._program.add_instruction(instruction)
 
             else:
-                warnings.warn("Redefining DefMeasureCalibration {}".format(instruction))
+                warnings.warn(f"Redefining DefMeasureCalibration {instruction}", stacklevel=2)
                 current_calibrations = self._program.calibrations
                 new_calibrations = CalibrationSet(
                     current_calibrations.calibrations,
@@ -503,8 +477,7 @@ def _add_instruction(self, instruction: quil_rs.Instruction) -> None:
             self._program.add_instruction(instruction)
 
     def filter_instructions(self, predicate: Callable[[AbstractInstruction], bool]) -> "Program":
-        """
-        Return a new ``Program`` containing only the instructions for which ``predicate`` returns ``True``.
+        """Return a new ``Program`` containing only the instructions for which ``predicate`` returns ``True``.
 
         :param predicate: A function that takes an instruction and returns ``True`` if the instruction should not be
             removed from the program, ``False`` otherwise.
@@ -519,9 +492,7 @@ def rs_predicate(inst: quil_rs.Instruction) -> bool:
         return filtered_program
 
     def remove_quil_t_instructions(self) -> "Program":
-        """
-        Return a copy of the program with all Quil-T instructions removed.
-        """
+        """Return a copy of the program with all Quil-T instructions removed."""
         filtered_program = Program(self._program.filter_instructions(lambda inst: not inst.is_quil_t()))
         filtered_program.num_shots = self.num_shots
         return filtered_program
@@ -532,8 +503,7 @@ def gate(
         params: Sequence[ParameterDesignator],
         qubits: Sequence[Union[Qubit, QubitPlaceholder]],
     ) -> "Program":
-        """
-        Add a gate to the program.
+        """Add a gate to the program.
 
         .. note::
 
@@ -552,11 +522,10 @@ def gate(
     def defgate(
         self,
         name: str,
-        matrix: Union[List[List[Any]], np.ndarray, np.matrix],
-        parameters: Optional[List[Parameter]] = None,
+        matrix: Union[list[list[Any]], np.ndarray, np.matrix],
+        parameters: Optional[list[Parameter]] = None,
     ) -> "Program":
-        """
-        Define a new static gate.
+        """Define a new static gate.
 
         .. note::
 
@@ -574,8 +543,7 @@ def defgate(
         return self.inst(DefGate(name, matrix, parameters))
 
     def define_noisy_gate(self, name: str, qubit_indices: Sequence[int], kraus_ops: Sequence[Any]) -> "Program":
-        """
-        Overload a static ideal gate with a noisy one defined in terms of a Kraus map.
+        """Overload a static ideal gate with a noisy one defined in terms of a Kraus map.
 
         .. note::
 
@@ -595,10 +563,10 @@ def define_noisy_gate(self, name: str, qubit_indices: Sequence[int], kraus_ops:
         return self.inst(_create_kraus_pragmas(name, tuple(qubit_indices), kraus_ops))
 
     def define_noisy_readout(self, qubit: Union[int], p00: float, p11: float) -> "Program":
-        """
-        For this program define a classical bit flip readout error channel parametrized by
-        ``p00`` and ``p11``. This models the effect of thermal noise that corrupts the readout
-        signal **after** it has interrogated the qubit.
+        """For this program define a classical bit flip readout error channel parametrized by ``p00`` and ``p11``.
+
+        This models the effect of thermal noise that corrupts the readout signal **after** it has interrogated the
+        qubit.
 
         :param qubit: The qubit with noisy readout.
         :param p00: The probability of obtaining the measurement result 0 given that the qubit
@@ -623,17 +591,14 @@ def define_noisy_readout(self, qubit: Union[int], p00: float, p11: float) -> "Pr
         return self.inst(pragma)
 
     def no_noise(self) -> "Program":
-        """
-        Prevent a noisy gate definition from being applied to the immediately following Gate
-        instruction.
+        """Prevent a noisy gate definition from being applied to the immediately following Gate instruction.
 
         :return: Program
         """
         return self.inst(Pragma("NO-NOISE"))
 
     def measure(self, qubit: QubitDesignator, classical_reg: Optional[MemoryReferenceDesignator]) -> "Program":
-        """
-        Measures a qubit at qubit_index and puts the result in classical_reg
+        """Measures a qubit at qubit_index and puts the result in classical_reg.
 
         :param qubit: The qubit to measure.
         :param classical_reg: The classical register to measure into, or None.
@@ -643,8 +608,7 @@ def measure(self, qubit: QubitDesignator, classical_reg: Optional[MemoryReferenc
         return self.inst(MEASURE(qubit, classical_reg))
 
     def reset(self, qubit_index: Optional[int] = None) -> "Program":
-        """
-        Reset all qubits or just a specific qubit at qubit_index.
+        """Reset all qubits or just a specific qubit at qubit_index.
 
         :param qubit_index: The address of the qubit to reset.
             If None, reset all qubits.
@@ -653,11 +617,11 @@ def reset(self, qubit_index: Optional[int] = None) -> "Program":
         """
         return self.inst(RESET(qubit_index))
 
-    def measure_all(self, *qubit_reg_pairs: Tuple[QubitDesignator, Optional[MemoryReferenceDesignator]]) -> "Program":
-        """
-        Measures many qubits into their specified classical bits, in the order
-        they were entered. If no qubit/register pairs are provided, measure all qubits present in
-        the program into classical addresses of the same index.
+    def measure_all(self, *qubit_reg_pairs: tuple[QubitDesignator, Optional[MemoryReferenceDesignator]]) -> "Program":
+        """Measures many qubits into their specified classical bits, in the order they were entered.
+
+        If no qubit/register pairs are provided, measure all qubits present in the program into classical addresses of
+        the same index.
 
         :param qubit_reg_pairs: Tuples of qubit indices paired with classical bits.
         :return: The Quil Program with the appropriate measure instructions appended, e.g.
@@ -682,7 +646,7 @@ def measure_all(self, *qubit_reg_pairs: Tuple[QubitDesignator, Optional[MemoryRe
             if any(isinstance(q, FormalArgument) for q in qubits):
                 raise ValueError("Cannot call measure_all on a Program that contains FormalArguments.")
             # Help mypy determine that qubits does not contain any QubitPlaceholders.
-            qubit_inds = cast(List[int], qubits)
+            qubit_inds = cast(list[int], qubits)
             ro = self.declare("ro", "BIT", max(qubit_inds) + 1)
             for qi in qubit_inds:
                 self.inst(MEASURE(qi, ro[qi]))
@@ -692,15 +656,12 @@ def measure_all(self, *qubit_reg_pairs: Tuple[QubitDesignator, Optional[MemoryRe
         return self
 
     def prepend_instructions(self, instructions: Iterable[AbstractInstruction]) -> "Program":
-        """
-        Prepend instructions to the beginning of the program.
-        """
+        """Prepend instructions to the beginning of the program."""
         new_prog = Program(*instructions)
         return new_prog + self
 
     def while_do(self, classical_reg: MemoryReferenceDesignator, q_program: "Program") -> "Program":
-        """
-        While a classical register at index classical_reg is 1, loop q_program
+        """While a classical register at index classical_reg is 1, loop q_program.
 
         Equivalent to the following construction:
 
@@ -734,9 +695,7 @@ def if_then(
         if_program: "Program",
         else_program: Optional["Program"] = None,
     ) -> "Program":
-        """
-        If the classical register at index classical reg is 1, run if_program, else run
-        else_program.
+        """If the classical register at index classical reg is 1, run if_program, else run else_program.
 
         Equivalent to the following construction:
 
@@ -779,9 +738,9 @@ def declare(
         memory_type: str = "BIT",
         memory_size: int = 1,
         shared_region: Optional[str] = None,
-        offsets: Optional[Sequence[Tuple[int, str]]] = None,
+        offsets: Optional[Sequence[tuple[int, str]]] = None,
     ) -> MemoryReference:
-        """DECLARE a quil variable
+        """DECLARE a quil variable.
 
         This adds the declaration to the current program and returns a MemoryReference to the
         base (offset = 0) of the declared memory.
@@ -819,8 +778,7 @@ def declare(
         return MemoryReference(name=name, declared_size=memory_size)
 
     def wrap_in_numshots_loop(self, shots: int) -> "Program":
-        """
-        Wraps a Quil program in a loop that re-runs the same program many times.
+        """Wrap a Quil program in a loop that re-runs the same program many times.
 
         Note: this function is a prototype of what will exist in the future when users will
         be responsible for writing this loop instead of having it happen automatically.
@@ -831,9 +789,7 @@ def wrap_in_numshots_loop(self, shots: int) -> "Program":
         return self
 
     def out(self, *, calibrations: Optional[bool] = True) -> str:
-        """
-        Serializes the Quil program to a string suitable for submitting to the QVM or QPU.
-        """
+        """Serialize the Quil program to a string suitable for submitting to the QVM or QPU."""
         if calibrations:
             return self._program.to_quil()
         else:
@@ -845,10 +801,10 @@ def out(self, *, calibrations: Optional[bool] = True) -> str:
         version="4.0",
         reason="The indices flag will be removed. Use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
-        """
-        Returns all of the qubit indices used in this program, including gate applications and
-        allocated qubits. e.g.
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Return all of the qubit indices used in this program, including gate applications and allocated qubits.
+
+        For example:
 
             >>> from pyquil.gates import H
             >>> p = Program()
@@ -862,7 +818,7 @@ def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[in
             >>> len(p.get_qubits(indices=False))
             2
 
-        :param indices: Return qubit indices as integers intead of the
+        :param indices: Return qubit indices as integers instead of the
             wrapping :py:class:`Qubit` object
         :return: A set of all the qubit indices used in this program
         """
@@ -870,16 +826,15 @@ def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[in
             return self.get_qubit_indices()
         return set(_convert_to_py_qubits(self._program.get_used_qubits()))
 
-    def get_qubit_indices(self) -> Set[int]:
-        """
-        Returns the index of each qubit used in the program. Will raise an exception if any of the
-        qubits are placeholders.
+    def get_qubit_indices(self) -> set[int]:
+        """Return the index of each qubit used in the program.
+
+        Will raise an exception if any of the qubits are placeholders.
         """
         return {q.to_fixed() for q in self._program.get_used_qubits()}
 
     def match_calibrations(self, instr: AbstractInstruction) -> Optional[CalibrationMatch]:
-        """
-        Attempt to match a calibration to the provided instruction.
+        """Attempt to match a calibration to the provided instruction.
 
         Note: preference is given to later calibrations, i.e. in a program with
 
@@ -911,8 +866,7 @@ def match_calibrations(self, instr: AbstractInstruction) -> Optional[Calibration
         return None
 
     def get_calibration(self, instr: AbstractInstruction) -> Optional[Union[DefCalibration, DefMeasureCalibration]]:
-        """
-        Get the calibration corresponding to the provided instruction.
+        """Get the calibration corresponding to the provided instruction.
 
         :param instr: An instruction.
         :returns: A matching Quil-T calibration definition, if one exists.
@@ -926,10 +880,9 @@ def get_calibration(self, instr: AbstractInstruction) -> Optional[Union[DefCalib
     def calibrate(
         self,
         instruction: AbstractInstruction,
-        previously_calibrated_instructions: Optional[Set[AbstractInstruction]] = None,
-    ) -> List[AbstractInstruction]:
-        """
-        Expand an instruction into its calibrated definition.
+        previously_calibrated_instructions: Optional[set[AbstractInstruction]] = None,
+    ) -> list[AbstractInstruction]:
+        """Expand an instruction into its calibrated definition.
 
         If a calibration definition matches the provided instruction, then the definition
         body is returned with appropriate substitutions made for parameters and qubit
@@ -960,9 +913,7 @@ def calibrate(
         reason="This function always returns True and will be removed.",
     )
     def is_protoquil(self, quilt: bool = False) -> bool:
-        """
-        This function has been deprecated and will always return True.
-        """
+        """Return True, this method is deprecated."""
         return True
 
     @deprecated(
@@ -970,23 +921,18 @@ def is_protoquil(self, quilt: bool = False) -> bool:
         reason="This function always returns True and will be removed.",
     )
     def is_supported_on_qpu(self) -> bool:
-        """
-        This function has been deprecated and will always return True.
-        """
+        """Return True, this method is deprecated."""
         return True
 
     def dagger(self) -> "Program":
-        """
-        Creates the conjugate transpose of the Quil program. The program must
-        contain only gate applications.
+        """Create the conjugate transpose of the Quil program. The program must contain only gate applications.
 
         :return: The Quil program's inverse
         """
         return Program(self._program.dagger())
 
     def __add__(self, other: InstructionDesignator) -> "Program":
-        """
-        Concatenate two programs together, returning a new one.
+        """Concatenate two programs together, returning a new one.
 
         :param other: Another program or instruction to concatenate to this one.
         :return: A newly concatenated program.
@@ -997,61 +943,58 @@ def __add__(self, other: InstructionDesignator) -> "Program":
         return p
 
     def __iadd__(self, other: InstructionDesignator) -> "Program":
+        """Concatenate two programs together, by overwriting the left hand side."""
         self.inst(other)
         return self
 
     def __getitem__(self, index: Union[slice, int]) -> Union[AbstractInstruction, "Program"]:
-        """
-        Allows indexing into the program to get an action.
-
-        :param index: The action at the specified index.
-        :return:
-        """
+        """Get the instruction at the given index, or a Program from a slice."""
         return Program(self.instructions[index]) if isinstance(index, slice) else self.instructions[index]
 
     def __iter__(self) -> Iterator[AbstractInstruction]:
-        """
-        Allow built in iteration through a program's instructions, e.g. [a for a in Program(X(0))]
-
-        :return:
-        """
+        """Iterate through a program's instructions, e.g. [a for a in Program(X(0))]."""
         return self.instructions.__iter__()
 
     def __eq__(self, other: object) -> bool:
+        """Check if two programs are equal."""
         if isinstance(other, Program):
             return self._program == other._program
         return False
 
     def __len__(self) -> int:
+        """Get the number of instructions in the program."""
         return len(self.instructions)
 
     def __hash__(self) -> int:
+        """Hash the program."""
         return hash(self.out())
 
     def __repr__(self) -> str:
+        """Get a string representation of the Quil program for inspection.
+
+        This may not be suitable for submission to a QPU or QVM for example if
+        your program contains unaddressed QubitPlaceholders
+        """
         return repr(self._program)
 
     def __str__(self) -> str:
-        """
-        A string representation of the Quil program for inspection.
+        """Get a string representation of the Quil program for inspection.
 
         This may not be suitable for submission to a QPU or QVM for example if
         your program contains unaddressed QubitPlaceholders
         """
         return self._program.to_quil_or_debug()
 
-    def get_all_instructions(self) -> List[AbstractInstruction]:
-        """
-        Get _all_ instructions that makeup the program.
-        """
+    def get_all_instructions(self) -> list[AbstractInstruction]:
+        """Get _all_ instructions that makeup the program."""
         return _convert_to_py_instructions(self._program.to_instructions())
 
 
 def merge_with_pauli_noise(
     prog_list: Iterable[Program], probabilities: Sequence[float], qubits: Sequence[int]
 ) -> Program:
-    """
-    Insert pauli noise channels between each item in the list of programs.
+    """Insert pauli noise channels between each item in the list of programs.
+
     This noise channel is implemented as a single noisy identity gate acting on the provided qubits.
     This method does not rely on merge_programs and so avoids the inclusion of redundant Kraus
     Pragmas that would occur if merge_programs was called directly on programs with distinct noisy
@@ -1078,14 +1021,13 @@ def merge_with_pauli_noise(
     return p
 
 
-def get_classical_addresses_from_program(program: Program) -> Dict[str, List[int]]:
-    """
-    Returns a sorted list of classical addresses found in the MEASURE instructions in the program.
+def get_classical_addresses_from_program(program: Program) -> dict[str, list[int]]:
+    """Return a sorted list of classical addresses found in the MEASURE instructions in the program.
 
     :param program: The program from which to get the classical addresses.
     :return: A mapping from memory region names to lists of offsets appearing in the program.
     """
-    addresses: Dict[str, List[int]] = defaultdict(list)
+    addresses: dict[str, list[int]] = defaultdict(list)
     flattened_addresses = {}
 
     # Required to use the `classical_reg.address` int attribute.
@@ -1103,9 +1045,9 @@ def get_classical_addresses_from_program(program: Program) -> Dict[str, List[int
     return flattened_addresses
 
 
-def address_qubits(program: Program, qubit_mapping: Optional[Dict[QubitPlaceholder, int]] = None) -> Program:
-    """
-    Takes a program which contains placeholders and assigns them all defined values.
+def address_qubits(program: Program, qubit_mapping: Optional[dict[QubitPlaceholder, int]] = None) -> Program:
+    """Take a program which contains placeholders and assigns the all defined values.
+
     Either all qubits must be defined or all undefined. If qubits are
     undefined, you may provide a qubit mapping to specify how placeholders get mapped
     to actual qubits. If a mapping is not provided, integers 0 through N are used.
@@ -1125,31 +1067,30 @@ def address_qubits(program: Program, qubit_mapping: Optional[Dict[QubitPlacehold
 
 def _get_label(
     placeholder: LabelPlaceholder,
-    label_mapping: Dict[LabelPlaceholder, Label],
+    label_mapping: dict[LabelPlaceholder, Label],
     label_i: int,
-) -> Tuple[Label, Dict[LabelPlaceholder, Label], int]:
-    """Helper function to either get the appropriate label for a given placeholder or generate
-    a new label and update the mapping.
+) -> tuple[Label, dict[LabelPlaceholder, Label], int]:
+    """Get the appropriate label for a given placeholder or generate a new label and update the mapping.
+
     See :py:func:`instantiate_labels` for usage.
     """
     if placeholder in label_mapping:
         return label_mapping[placeholder], label_mapping, label_i
 
-    new_target = Label("{}{}".format(placeholder.prefix, label_i))
+    new_target = Label(f"{placeholder.prefix}{label_i}")
     label_i += 1
     label_mapping[placeholder] = new_target
     return new_target, label_mapping, label_i
 
 
-def instantiate_labels(instructions: Iterable[AbstractInstruction]) -> List[AbstractInstruction]:
-    """
-    Takes an iterable of instructions which may contain label placeholders and assigns
-    them all defined values.
+def instantiate_labels(instructions: Iterable[AbstractInstruction]) -> list[AbstractInstruction]:
+    """Take an iterable of instructions which may contain label placeholders and assigns them all defined values.
+
     :return: list of instructions with all label placeholders assigned to real labels.
     """
     label_i = 1
-    result: List[AbstractInstruction] = []
-    label_mapping: Dict[LabelPlaceholder, Label] = dict()
+    result: list[AbstractInstruction] = []
+    label_mapping: dict[LabelPlaceholder, Label] = dict()
     for instr in instructions:
         if isinstance(instr, Jump) and isinstance(instr.target, LabelPlaceholder):
             new_target, label_mapping, label_i = _get_label(instr.target, label_mapping, label_i)
@@ -1172,9 +1113,9 @@ def instantiate_labels(instructions: Iterable[AbstractInstruction]) -> List[Abst
     reason="The Program class now sorts instructions automatically. This function will be removed.",
 )
 def percolate_declares(program: Program) -> Program:
-    """
-    As of pyQuil v4.0, the Program class does this automatically. This function is deprecated
-    and just immediately returns the passed in program.
+    """As of pyQuil v4.0, the Program class does this automatically.
+
+    This function is deprecated and just immediately returns the passed in program.
 
     :param program: A program.
     """
@@ -1186,9 +1127,7 @@ def percolate_declares(program: Program) -> Program:
     reason="This function will be removed. Use Program addition instead.",
 )
 def merge_programs(prog_list: Sequence[Program]) -> Program:
-    """
-    Merges a list of pyQuil programs into a single one by appending them in sequence.
-    """
+    """Merge a list of pyQuil programs into a single one by appending them in sequence."""
     merged_program = Program()
     for prog in prog_list:
         merged_program += prog
@@ -1199,10 +1138,7 @@ def merge_programs(prog_list: Sequence[Program]) -> Program:
     version="4.0",
     reason="This is now a no-op and will be removed in future versions of pyQuil.",
 )
-def validate_protoquil(program: Program, quilt: bool = False) -> None:
-    """
-    This function has been deprecated. It is now a no-op.
-    """
+def validate_protoquil(program: Program, quilt: bool = False) -> None:  # noqa: D103 - deprecated no-op
     pass
 
 
@@ -1210,8 +1146,5 @@ def validate_protoquil(program: Program, quilt: bool = False) -> None:
     version="4.0",
     reason="This is now a no-op and will be removed in future versions of pyQuil.",
 )
-def validate_supported_quil(program: Program) -> None:
-    """
-    This function has been deprecated. It is now a no-op.
-    """
+def validate_supported_quil(program: Program) -> None:  # noqa: D103 - deprecated no-op
     pass
diff --git a/pyquil/quilatom.py b/pyquil/quilatom.py
index 4b93cff1b..65693cfa3 100644
--- a/pyquil/quilatom.py
+++ b/pyquil/quilatom.py
@@ -13,76 +13,70 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from fractions import Fraction
+"""Classes that represent the atomic building blocks of Quil expressions."""
+
 import inspect
+from collections.abc import Iterable, Mapping, Sequence
+from fractions import Fraction
 from numbers import Number
 from typing import (
     Any,
     Callable,
     ClassVar,
-    Dict,
-    List,
-    Iterable,
-    Mapping,
     NoReturn,
     Optional,
-    Set,
-    Sequence,
-    Tuple,
-    Type,
     Union,
     cast,
 )
-from typing_extensions import Self
-from deprecated.sphinx import deprecated
 
 import numpy as np
-
-import quil.instructions as quil_rs
 import quil.expression as quil_rs_expr
+import quil.instructions as quil_rs
+from deprecated.sphinx import deprecated
+from typing_extensions import Self
 
 
-class QuilAtom(object):
-    """
-    Abstract class for atomic elements of Quil.
-    """
+class QuilAtom:
+    """Abstract class for atomic elements of Quil."""
 
     def out(self) -> str:
+        """Return the element as a valid Quil string."""
         raise NotImplementedError()
 
     def __str__(self) -> str:
+        """Get a string representation of the element, possibly not valid Quil."""
         raise NotImplementedError()
 
     def __eq__(self, other: object) -> bool:
+        """Return True if the other object is equal to this one."""
         raise NotImplementedError()
 
-    def __ne__(self, other: object) -> bool:
-        return not self.__eq__(other)
-
     def __hash__(self) -> int:
+        """Return a hash of the object."""
         raise NotImplementedError()
 
 
 class Qubit(QuilAtom):
-    """
-    Representation of a qubit.
+    """Representation of a qubit.
 
     :param index: Index of the qubit.
     """
 
     def __init__(self, index: int):
+        """Initialize a qubit."""
         if not (isinstance(index, int) and index >= 0):
             raise TypeError("Addr index must be a non-negative int")
         self.index = index
 
     def out(self) -> str:
+        """Return the element as a valid Quil string."""
         return str(self.index)
 
     def __str__(self) -> str:
         return str(self.index)
 
     def __repr__(self) -> str:
-        return "<Qubit {0}>".format(self.index)
+        return f"<Qubit {self.index}>"
 
     def __hash__(self) -> int:
         return hash(self.index)
@@ -92,21 +86,25 @@ def __eq__(self, other: object) -> bool:
 
 
 class FormalArgument(QuilAtom):
-    """
-    Representation of a formal argument associated with a DEFCIRCUIT or DEFGATE ... AS PAULI-SUM
-    or DEFCAL form.
-    """
+    """Representation of a formal argument associated with a DEFCIRCUIT or DEFGATE ... AS PAULI-SUM or DEFCAL form."""
 
     def __init__(self, name: str):
+        """Initialize a formal argument."""
         if not isinstance(name, str):
             raise TypeError("Formal arguments must be named by a string.")
         self.name = name
 
     def out(self) -> str:
+        """Return the element as a valid Quil string."""
         return str(self)
 
     @property
+    @deprecated(
+        "Getting the index of a FormalArgument is invalid. This method will be removed in a future release.",
+        version="4.0",
+    )
     def index(self) -> NoReturn:
+        """Formal arguments do not have an index. Using this property raises a RuntimeError."""
         raise RuntimeError(f"Cannot derive an index from a FormalArgument {self}")
 
     def __str__(self) -> str:
@@ -123,24 +121,31 @@ def __eq__(self, other: object) -> bool:
 
 
 class QubitPlaceholder(QuilAtom):
+    """A placeholder for a qubit.
+
+    This is useful for constructing circuits without assigning qubits to specific indices.
+    Qubit placeholders must be resolved to actual qubits before they can be used in a program.
+    """
+
     def __init__(self, placeholder: Optional[quil_rs.QubitPlaceholder] = None):
+        """Initialize a qubit placeholder, or get a new handle for an existing placeholder."""
         if placeholder is not None:
             self._placeholder = placeholder
         else:
             self._placeholder = quil_rs.QubitPlaceholder()
 
     @staticmethod
-    def register(n: int) -> List["QubitPlaceholder"]:
-        """Return a 'register' of ``n`` QubitPlaceholders.
+    def register(n: int) -> list["QubitPlaceholder"]:
+        r"""Return a 'register' of ``n`` QubitPlaceholders.
 
         >>> from pyquil import Program
         >>> from pyquil.gates import H
         >>> from pyquil.quil import address_qubits
         >>> from pyquil.quilatom import QubitPlaceholder
-        >>> qs = QubitPlaceholder.register(8) # a qubyte
+        >>> qs = QubitPlaceholder.register(8)  # a qubyte
         >>> prog = Program(H(q) for q in qs)
         >>> address_qubits(prog).out()
-        'H 0\\nH 1\\nH 2\\nH 3\\nH 4\\nH 5\\nH 6\\nH 7\\n'
+        'H 0\nH 1\nH 2\nH 3\nH 4\nH 5\nH 6\nH 7\n'
         >>>
 
         The returned register is a Python list of QubitPlaceholder objects, so all
@@ -151,11 +156,13 @@ def register(n: int) -> List["QubitPlaceholder"]:
         return [QubitPlaceholder() for _ in range(n)]
 
     def out(self) -> str:
-        raise RuntimeError("Qubit {} has not been assigned an index".format(self))
+        """Raise a RuntimeError, as Qubit placeholders are not valid Quil."""
+        raise RuntimeError(f"Qubit {self} has not been assigned an index")
 
     @property
     def index(self) -> NoReturn:
-        raise RuntimeError("Qubit {} has not been assigned an index".format(self))
+        """Raise a RuntimeError, as Qubit placeholders do not have an index."""
+        raise RuntimeError(f"Qubit {self} has not been assigned an index")
 
     def __str__(self) -> str:
         return f"q{id(self)}"
@@ -198,7 +205,7 @@ def _convert_to_rs_qubit(qubit: Union[QubitDesignator, quil_rs.Qubit, QubitPlace
     raise ValueError(f"{type(qubit)} is not a valid QubitDesignator")
 
 
-def _convert_to_rs_qubits(qubits: Iterable[QubitDesignator]) -> List[quil_rs.Qubit]:
+def _convert_to_rs_qubits(qubits: Iterable[QubitDesignator]) -> list[quil_rs.Qubit]:
     return [_convert_to_rs_qubit(qubit) for qubit in qubits]
 
 
@@ -215,13 +222,12 @@ def _convert_to_py_qubit(qubit: Union[QubitDesignator, quil_rs.Qubit, quil_rs.Qu
     raise ValueError(f"{type(qubit)} is not a valid QubitDesignator")
 
 
-def _convert_to_py_qubits(qubits: Iterable[Union[QubitDesignator, quil_rs.Qubit]]) -> List[QubitDesignator]:
+def _convert_to_py_qubits(qubits: Iterable[Union[QubitDesignator, quil_rs.Qubit]]) -> list[QubitDesignator]:
     return [_convert_to_py_qubit(qubit) for qubit in qubits]
 
 
 def unpack_qubit(qubit: Union[QubitDesignator, FormalArgument]) -> Union[Qubit, QubitPlaceholder, FormalArgument]:
-    """
-    Get a qubit from an object.
+    """Get a qubit from an object.
 
     :param qubit: the qubit designator to unpack.
     :return: A Qubit or QubitPlaceholder instance
@@ -239,8 +245,7 @@ def unpack_qubit(qubit: Union[QubitDesignator, FormalArgument]) -> Union[Qubit,
 
 
 def qubit_index(qubit: QubitDesignator) -> int:
-    """
-    Get the index of a QubitDesignator.
+    """Get the index of a QubitDesignator.
 
     :param qubit: the qubit designator.
     :return: An int that is the qubit index.
@@ -256,13 +261,12 @@ def qubit_index(qubit: QubitDesignator) -> int:
 # Like the Tuple, the List must be length 2, where the first item is a string and the second an
 # int. However, specifying Union[str, int] as the generic type argument to List doesn't sufficiently
 # constrain the types, and mypy gets confused in unpack_classical_reg, below. Hence, just specify
-# List[Any] here.
-MemoryReferenceDesignator = Union["MemoryReference", quil_rs.MemoryReference, Tuple[str, int], List[Any], str]
+# list[Any] here.
+MemoryReferenceDesignator = Union["MemoryReference", quil_rs.MemoryReference, tuple[str, int], list[Any], str]
 
 
 def unpack_classical_reg(c: MemoryReferenceDesignator) -> "MemoryReference":
-    """
-    Get the address for a classical register.
+    """Get the address for a classical register.
 
     :param c: A list of length 2, a pair, a string (to be interpreted as name[0]), or a
         MemoryReference.
@@ -287,13 +291,13 @@ def unpack_classical_reg(c: MemoryReferenceDesignator) -> "MemoryReference":
 
 
 class Label(QuilAtom):
-    """
-    Representation of a label.
+    """Representation of a label.
 
     :param label_name: The label name.
     """
 
     def __init__(self, label_name: str):
+        """Initialize a new label."""
         self.target = quil_rs.Target.from_fixed(label_name)
 
     @staticmethod
@@ -301,10 +305,12 @@ def _from_rs_target(target: quil_rs.Target) -> "Label":
         return Label(target.to_fixed())
 
     def out(self) -> str:
+        """Return the label as a valid Quil string."""
         return self.target.to_quil()
 
     @property
     def name(self) -> str:
+        """Return the label name."""
         return self.target.to_fixed()
 
     @name.setter
@@ -327,7 +333,14 @@ def __hash__(self) -> int:
 
 
 class LabelPlaceholder(QuilAtom):
+    """A placeholder for a Quil label.
+
+    This is useful for constructing circuits without needing to name them.
+    All placeholders must be resolved to actual labels before they can be used in a program.
+    """
+
     def __init__(self, prefix: str = "L", *, placeholder: Optional[quil_rs.TargetPlaceholder] = None):
+        """Initialize a new label placeholder."""
         if placeholder:
             self.target = quil_rs.Target.from_placeholder(placeholder)
         else:
@@ -339,9 +352,11 @@ def _from_rs_target(target: quil_rs.Target) -> "LabelPlaceholder":
 
     @property
     def prefix(self) -> str:
+        """Get the prefix of the label placeholder."""
         return self.target.to_placeholder().base_label
 
     def out(self) -> str:
+        """Raise a RuntimeError, as label placeholders are not valid Quil."""
         raise RuntimeError("Label has not been assigned a name")
 
     def __str__(self) -> str:
@@ -363,7 +378,7 @@ def __hash__(self) -> int:
 
 
 def _convert_to_rs_expression(
-    parameter: Union[ParameterDesignator, quil_rs_expr.Expression]
+    parameter: Union[ParameterDesignator, quil_rs_expr.Expression],
 ) -> quil_rs_expr.Expression:
     if isinstance(parameter, quil_rs_expr.Expression):
         return parameter
@@ -375,16 +390,16 @@ def _convert_to_rs_expression(
 
 
 def _convert_to_rs_expressions(
-    parameters: Sequence[Union[ParameterDesignator, quil_rs_expr.Expression]]
-) -> List[quil_rs_expr.Expression]:
+    parameters: Sequence[Union[ParameterDesignator, quil_rs_expr.Expression]],
+) -> list[quil_rs_expr.Expression]:
     return [_convert_to_rs_expression(parameter) for parameter in parameters]
 
 
 @deprecated(version="4.0", reason="This function has been superseded by the `quil` package and will be removed soon.")
 def format_parameter(element: ParameterDesignator) -> str:
-    """
-    Formats a particular parameter. Essentially the same as built-in formatting except using 'i'
-    instead of 'j' for the imaginary number.
+    """Format a particular parameter.
+
+    Essentially the same as built-in formatting except using 'i' instead of 'j' for the imaginary number.
 
     :param element: The parameter to format for Quil output.
     """
@@ -403,10 +418,12 @@ def format_parameter(element: ParameterDesignator) -> str:
             out += repr(r)
 
         if i == 1:
-            assert np.isclose(r, 0, atol=1e-14)
+            if not np.isclose(r, 0, atol=1e-14):
+                raise ValueError(f"Invalid complex number: {element}")
             out = "i"
         elif i == -1:
-            assert np.isclose(r, 0, atol=1e-14)
+            if not np.isclose(r, 0, atol=1e-14):
+                raise ValueError(f"Invalid complex number: {element}")
             out = "-i"
         elif i < 0:
             out += repr(i) + "i"
@@ -420,7 +437,7 @@ def format_parameter(element: ParameterDesignator) -> str:
         return str(element)
     elif isinstance(element, Expression):
         return _expression_to_string(element)
-    raise AssertionError("Invalid parameter: %r" % element)
+    raise AssertionError(f"Invalid parameter: {element}")
 
 
 ExpressionValueDesignator = Union[int, float, complex]
@@ -434,7 +451,7 @@ def _convert_to_py_expression(
         ExpressionValueDesignator,
         quil_rs_expr.Expression,
         quil_rs.MemoryReference,
-    ]
+    ],
 ) -> ExpressionDesignator:
     if isinstance(expression, (Expression, Number)):
         return expression
@@ -475,15 +492,16 @@ def _convert_to_py_expression(
 def _convert_to_py_expressions(
     expressions: Sequence[
         Union[ParameterDesignator, ExpressionDesignator, quil_rs_expr.Expression, quil_rs.MemoryReference]
-    ]
+    ],
 ) -> Sequence[ExpressionDesignator]:
     return [_convert_to_py_expression(expression) for expression in expressions]
 
 
-class Expression(object):
-    """
-    Expression involving some unbound parameters. Parameters in Quil are represented as a label
-    like '%x' for the parameter named 'x'. An example expression therefore may be '%x*(%y/4)'.
+class Expression:
+    """Expression involving some unbound parameters.
+
+    Parameters in Quil are represented as a label like '%x' for the parameter named 'x'. An example expression therefore
+    may be '%x*(%y/4)'.
 
     Expressions may also have function calls, supported functions in Quil are sin, cos, sqrt, exp,
     and cis.
@@ -538,8 +556,7 @@ def _substitute(self, d: Any) -> ExpressionDesignator:
         return self
 
     def _evaluate(self) -> np.complex128:
-        """
-        Attempts to evaluate the expression to by simplifying it to a complex number.
+        """Attempt to evaluate the expression to by simplifying it to a complex number.
 
         Expression simplification can be slow, especially for large recursive expressions.
         This method will raise a ValueError if the expression cannot be simplified to a complex
@@ -552,8 +569,7 @@ def _evaluate(self) -> np.complex128:
         return np.complex128(expr.to_number())
 
     def __float__(self) -> float:
-        """
-        Returns a copy of the expression as a float by attempting to simplify the expression.
+        """Return a copy of the expression as a float by attempting to simplify the expression.
 
         Expression simplification can be slow, especially for large recursive expressions.
         This cast will raise a ValueError if simplification doesn't result in a real number.
@@ -564,8 +580,7 @@ def __float__(self) -> float:
         return float(value.real)
 
     def __array__(self, dtype: Optional[np.dtype] = None) -> np.ndarray:
-        """
-        Implements the numpy array protocol for this expression.
+        """Implement the numpy array protocol for this expression.
 
         If the dtype is not object, then there will be an attempt to simplify the expression to a
         complex number. If the expression cannot be simplified to one, then fallback to the
@@ -589,10 +604,10 @@ def __array__(self, dtype: Optional[np.dtype] = None) -> np.ndarray:
 
 
 def substitute(expr: ExpressionDesignator, d: ParameterSubstitutionsMapDesignator) -> ExpressionDesignator:
-    """
-    Using a dictionary of substitutions ``d``, try and explicitly evaluate as much of ``expr`` as
-    possible. This supports substitution of both parameters and memory references. Each memory
-    reference must be individually assigned a value at each memory offset to be substituted.
+    r"""Explicitly evaluate as much of ``expr`` as possible, using substitutions from `d`.
+
+    This supports substitution of both parameters and memory references. Each memory reference must be individually
+    assigned a value at each memory offset to be substituted.
 
     :param expr: The expression whose parameters or memory references are to be substituted.
     :param d: Numerical substitutions for parameters or memory references.
@@ -604,8 +619,7 @@ def substitute(expr: ExpressionDesignator, d: ParameterSubstitutionsMapDesignato
 
 
 def substitute_array(a: Union[Sequence[Expression], np.ndarray], d: ParameterSubstitutionsMapDesignator) -> np.ndarray:
-    """
-    Apply ``substitute`` to all elements of an array ``a`` and return the resulting array.
+    """Apply ``substitute`` to all elements of an array ``a`` and return the resulting array.
 
     :param a: The array of expressions whose parameters or memory references are to be substituted.
     :param d: Numerical substitutions for parameters or memory references, for all array elements.
@@ -616,14 +630,14 @@ def substitute_array(a: Union[Sequence[Expression], np.ndarray], d: ParameterSub
 
 
 class Parameter(QuilAtom, Expression):
-    """
-    Parameters in Quil are represented as a label like '%x' for the parameter named 'x'.
-    """
+    """Parameters in Quil are represented as a label like '%x' for the parameter named 'x'."""
 
     def __init__(self, name: str):
+        """Initialize a new parameter."""
         self.name = name
 
     def out(self) -> str:
+        """Return the parameter as a valid Quil string."""
         return "%" + self.name
 
     def _substitute(self, d: ParameterSubstitutionsMapDesignator) -> Union["Parameter", ExpressionValueDesignator]:
@@ -640,9 +654,7 @@ def __eq__(self, other: object) -> bool:
 
 
 class Function(Expression):
-    """
-    Supported functions in Quil are sin, cos, sqrt, exp, and cis
-    """
+    """Base class for standard Quil functions."""
 
     def __init__(
         self,
@@ -650,6 +662,7 @@ def __init__(
         expression: ExpressionDesignator,
         fn: Callable[[ExpressionValueDesignator], ExpressionValueDesignator],
     ):
+        """Initialize a new function."""
         self.name = name
         self.expression = expression
         self.fn = fn
@@ -681,22 +694,28 @@ def __neq__(self, other: "Function") -> bool:
 
 
 def quil_sin(expression: ExpressionDesignator) -> Function:
+    """Quil COS() function."""
     return Function("SIN", expression, np.sin)
 
 
 def quil_cos(expression: ExpressionDesignator) -> Function:
+    """Quil SIN() function."""
     return Function("COS", expression, np.cos)
 
 
 def quil_sqrt(expression: ExpressionDesignator) -> Function:
+    """Quil SQRT() function."""
     return Function("SQRT", expression, np.sqrt)
 
 
 def quil_exp(expression: ExpressionDesignator) -> Function:
+    """Quil EXP() function."""
     return Function("EXP", expression, np.exp)
 
 
 def quil_cis(expression: ExpressionDesignator) -> Function:
+    """Quil CIS() function."""
+
     def _cis(x: ExpressionValueDesignator) -> complex:
         # numpy doesn't ship with type stubs, so mypy assumes anything coming from numpy has type
         # Any, hence we need to cast the return type to complex here to satisfy the type checker.
@@ -706,15 +725,19 @@ def _cis(x: ExpressionValueDesignator) -> complex:
 
 
 class BinaryExp(Expression):
+    """A Quil binary expression."""
+
     operator: ClassVar[str]
     precedence: ClassVar[int]
     associates: ClassVar[str]
 
     @staticmethod
     def fn(a: ExpressionDesignator, b: ExpressionDesignator) -> Union["BinaryExp", ExpressionValueDesignator]:
+        """Perform the operation on the two expressions."""
         raise NotImplementedError
 
     def __init__(self, op1: ExpressionDesignator, op2: ExpressionDesignator):
+        """Initialize a new binary expression."""
         self.op1 = op1
         self.op2 = op2
 
@@ -748,74 +771,94 @@ def __neq__(self, other: "BinaryExp") -> bool:
 
 
 class Add(BinaryExp):
+    """The addition operation."""
+
     operator = " + "
     precedence = 1
     associates = "both"
 
     @staticmethod
     def fn(a: ExpressionDesignator, b: ExpressionDesignator) -> Union["Add", ExpressionValueDesignator]:
+        """Perform the addition operation."""
         return a + b
 
     def __init__(self, op1: ExpressionDesignator, op2: ExpressionDesignator):
-        super(Add, self).__init__(op1, op2)
+        """Initialize a new addition operation between two expressions."""
+        super().__init__(op1, op2)
 
 
 class Sub(BinaryExp):
+    """The subtraction operation."""
+
     operator = " - "
     precedence = 1
     associates = "left"
 
     @staticmethod
     def fn(a: ExpressionDesignator, b: ExpressionDesignator) -> Union["Sub", ExpressionValueDesignator]:
+        """Perform the subtraction operation."""
         return a - b
 
     def __init__(self, op1: ExpressionDesignator, op2: ExpressionDesignator):
-        super(Sub, self).__init__(op1, op2)
+        """Initialize a new addition operation between two expressions."""
+        super().__init__(op1, op2)
 
 
 class Mul(BinaryExp):
+    """The multiplication operation."""
+
     operator = "*"
     precedence = 2
     associates = "both"
 
     @staticmethod
     def fn(a: ExpressionDesignator, b: ExpressionDesignator) -> Union["Mul", ExpressionValueDesignator]:
+        """Perform the multiplication operation."""
         return a * b
 
     def __init__(self, op1: ExpressionDesignator, op2: ExpressionDesignator):
-        super(Mul, self).__init__(op1, op2)
+        """Initialize a new multiplication operation between two expressions."""
+        super().__init__(op1, op2)
 
 
 class Div(BinaryExp):
+    """The division operation."""
+
     operator = "/"
     precedence = 2
     associates = "left"
 
     @staticmethod
     def fn(a: ExpressionDesignator, b: ExpressionDesignator) -> Union["Div", ExpressionValueDesignator]:
+        """Perform the division operation."""
         return a / b
 
     def __init__(self, op1: ExpressionDesignator, op2: ExpressionDesignator):
-        super(Div, self).__init__(op1, op2)
+        """Initialize a new division operation between two expressions."""
+        super().__init__(op1, op2)
 
 
 class Pow(BinaryExp):
+    """The exponentiation operation."""
+
     operator = "^"
     precedence = 3
     associates = "right"
 
     @staticmethod
     def fn(a: ExpressionDesignator, b: ExpressionDesignator) -> Union["Pow", ExpressionValueDesignator]:
+        """Perform the exponentiation operation."""
         return a**b
 
     def __init__(self, op1: ExpressionDesignator, op2: ExpressionDesignator):
-        super(Pow, self).__init__(op1, op2)
+        """Initialize a new exponentiation operation between two expressions."""
+        super().__init__(op1, op2)
 
 
 def _expression_to_string(expression: ExpressionDesignator) -> str:
-    """
-    Recursively converts an expression to a string taking into account precedence and associativity
-    for placing parenthesis.
+    """Convert an expression to a string.
+
+    This operation is recursive, and takes into account precedence and associativity when placing parenthesis.
 
     :param expression: expression involving parameters
     :return: string such as '%x*(%y-4)'
@@ -841,7 +884,7 @@ def _expression_to_string(expression: ExpressionDesignator) -> str:
         # extra care to insert parens. Similarly, complex numbers need
         # to be wrapped in parens so the imaginary part is captured.
         # See gh-943,1734.
-        elif (isinstance(expression.op2, float) and (("pi" in right and right != "pi"))) or isinstance(
+        elif (isinstance(expression.op2, float) and ("pi" in right and right != "pi")) or isinstance(
             expression.op2, complex
         ):
             right = "(" + right + ")"
@@ -855,9 +898,8 @@ def _expression_to_string(expression: ExpressionDesignator) -> str:
         return format_parameter(expression)
 
 
-def _contained_parameters(expression: ExpressionDesignator) -> Set[Parameter]:
-    """
-    Determine which parameters are contained in this expression.
+def _contained_parameters(expression: ExpressionDesignator) -> set[Parameter]:
+    """Determine which parameters are contained in this expression.
 
     :param expression: expression involving parameters
     :return: set of parameters contained in this expression
@@ -873,10 +915,10 @@ def _contained_parameters(expression: ExpressionDesignator) -> Set[Parameter]:
 
 
 def _check_for_pi(element: float) -> str:
-    """
-    Check to see if there exists a rational number r = p/q
-    in reduced form for which the difference between element/np.pi
-    and r is small and q <= 8.
+    """Return the float as a string, expressing the float as a multiple of pi if possible.
+
+    More specifically, check to see if there exists a rational number r = p/q in reduced form for which the difference
+    between element/np.pi and r is small and q <= 8.
 
     :param element: the number to check
     :return element: pretty print string if true, else standard representation.
@@ -900,8 +942,7 @@ def _check_for_pi(element: float) -> str:
 
 
 class MemoryReference(QuilAtom, Expression):
-    """
-    Representation of a reference to a classical memory address.
+    """Representation of a reference to a classical memory address.
 
     :param name: The name of the variable
     :param offset: Everything in Quil is a C-style array, so every memory reference has an offset.
@@ -912,6 +953,7 @@ class MemoryReference(QuilAtom, Expression):
     """
 
     def __init__(self, name: str, offset: int = 0, declared_size: Optional[int] = None):
+        """Initialize a new memory reference."""
         if not isinstance(offset, int) or offset < 0:
             raise TypeError("MemoryReference offset must be a non-negative int")
         self.name = name
@@ -933,19 +975,20 @@ def _to_rs_memory_reference(self) -> quil_rs.MemoryReference:
         return quil_rs.MemoryReference(self.name, self.offset)
 
     def out(self) -> str:
+        """Return the memory reference as a valid Quil string."""
         if self.declared_size is not None and self.declared_size == 1 and self.offset == 0:
-            return "{}".format(self.name)
+            return f"{self.name}"
         else:
-            return "{}[{}]".format(self.name, self.offset)
+            return f"{self.name}[{self.offset}]"
 
     def __str__(self) -> str:
         if self.declared_size is not None and self.declared_size == 1 and self.offset == 0:
-            return "{}".format(self.name)
+            return f"{self.name}"
         else:
-            return "{}[{}]".format(self.name, self.offset)
+            return f"{self.name}[{self.offset}]"
 
     def __repr__(self) -> str:
-        return "<MRef {}[{}]>".format(self.name, self.offset)
+        return f"<MRef {self.name}[{self.offset}]>"
 
     def __eq__(self, other: object) -> bool:
         return isinstance(other, MemoryReference) and other.name == self.name and other.offset == self.offset
@@ -975,9 +1018,8 @@ def _substitute(
         return self
 
 
-def _contained_mrefs(expression: ExpressionDesignator) -> Set[MemoryReference]:
-    """
-    Determine which memory references are contained in this expression.
+def _contained_mrefs(expression: ExpressionDesignator) -> set[MemoryReference]:
+    """Determine which memory references are contained in this expression.
 
     :param expression: expression involving parameters
     :return: set of parameters contained in this expression
@@ -993,11 +1035,10 @@ def _contained_mrefs(expression: ExpressionDesignator) -> Set[MemoryReference]:
 
 
 class Frame(quil_rs.FrameIdentifier):
-    """
-    Representation of a frame descriptor.
-    """
+    """Representation of a frame descriptor."""
 
     def __new__(cls, qubits: Sequence[QubitDesignator], name: str) -> Self:
+        """Initialize a new Frame."""
         return super().__new__(cls, name, _convert_to_rs_qubits(qubits))
 
     @classmethod
@@ -1005,40 +1046,49 @@ def _from_rs_frame_identifier(cls, frame: quil_rs.FrameIdentifier) -> "Frame":
         return super().__new__(cls, frame.name, frame.qubits)
 
     @property  # type: ignore[override]
-    def qubits(self) -> Tuple[QubitDesignator, ...]:
+    def qubits(self) -> tuple[QubitDesignator, ...]:
+        """Get the qubits in the frame."""
         return tuple(_convert_to_py_qubits(super().qubits))
 
     @qubits.setter
-    def qubits(self, qubits: Tuple[Qubit, FormalArgument]) -> None:
+    def qubits(self, qubits: tuple[Qubit, FormalArgument]) -> None:
         quil_rs.FrameIdentifier.qubits.__set__(self, _convert_to_rs_qubits(qubits))  # type: ignore[attr-defined]
 
     def out(self) -> str:
+        """Return the frame as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
+        """Return the frame as a string."""
         return super().to_quil_or_debug()
 
 
 class WaveformInvocation(quil_rs.WaveformInvocation, QuilAtom):
-    def __new__(cls, name: str, parameters: Optional[Dict[str, ParameterDesignator]] = None) -> Self:
+    """A waveform invocation."""
+
+    def __new__(cls, name: str, parameters: Optional[dict[str, ParameterDesignator]] = None) -> Self:
+        """Initialize a new waveform invocation."""
         if parameters is None:
             parameters = {}
         rs_parameters = {key: _convert_to_rs_expression(value) for key, value in parameters.items()}
         return super().__new__(cls, name, rs_parameters)
 
     @property  # type: ignore[override]
-    def parameters(self) -> Dict[str, ParameterDesignator]:
+    def parameters(self) -> dict[str, ParameterDesignator]:
+        """The parameters in the waveform invocation."""
         return {key: _convert_to_py_expression(value) for key, value in super().parameters.items()}
 
     @parameters.setter
-    def parameters(self, parameters: Dict[str, ParameterDesignator]) -> None:
+    def parameters(self, parameters: dict[str, ParameterDesignator]) -> None:
         rs_parameters = {key: _convert_to_rs_expression(value) for key, value in parameters.items()}
         quil_rs.WaveformInvocation.parameters.__set__(self, rs_parameters)  # type: ignore[attr-defined]
 
     def out(self) -> str:
+        """Return the waveform invocation as a valid Quil string."""
         return self.to_quil()
 
     def __str__(self) -> str:
+        """Return the frame as a string."""
         return super().to_quil_or_debug()
 
 
@@ -1047,29 +1097,25 @@ def __str__(self) -> str:
     reason="The WaveformReference class will be removed, consider using WaveformInvocation instead.",
 )
 class WaveformReference(WaveformInvocation):
-    """
-    Representation of a Waveform reference.
-    """
+    """Representation of a Waveform reference."""
 
     def __new__(cls, name: str) -> Self:
+        """Initialize a new waveform reference."""
         return super().__new__(cls, name, {})
 
 
 def _template_waveform_property(
-    name: str, *, dtype: Optional[Union[Type[int], Type[float]]] = None, doc: Optional[str] = None
+    name: str, *, dtype: Optional[Union[type[int], type[float]]] = None, doc: Optional[str] = None
 ) -> property:
-    """
-    Helper method for initializing getters, setters, and deleters for
-    parameters on a ``TemplateWaveform``. Should only be used inside of
-    ``TemplateWaveform`` or one its base classes.
+    """Initialize a getters, setters, and deleter for a parameter on a ``TemplateWaveform``.
 
-    Parameters:
-        name - The name of the property
-        dtype - `dtype` is an optional parameter that takes the int or float type, and attempts
-            to convert the underlying complex value by casting the real part to `dtype`. If set, this
-            function will raise an error if the complex number has a non-zero imaginary part.
-        doc - Docstring for the property.
+    Should only be used inside of ``TemplateWaveform`` or one its base classes.
 
+    :param name: The name of the property
+    :param dtype: An optional parameter that takes the int or float type, and attempts
+            to convert the underlying complex value by casting the real part to `dtype`. If set, this function will
+            raise an error if the complex number has a non-zero imaginary part.
+    :param doc: Docstring for the property.
     """
 
     def fget(self: "TemplateWaveform") -> Optional[ParameterDesignator]:
@@ -1103,6 +1149,8 @@ def fdel(self: "TemplateWaveform") -> None:
 
 
 class TemplateWaveform(quil_rs.WaveformInvocation, QuilAtom):
+    """Base class for creating waveform templates."""
+
     NAME: ClassVar[str]
 
     def __new__(
@@ -1112,20 +1160,24 @@ def __new__(
         duration: float,
         **kwargs: Union[Optional[ParameterDesignator], Optional[ExpressionDesignator]],
     ) -> Self:
+        """Initialize a new TemplateWaveform."""
         rs_parameters = {key: _convert_to_rs_expression(value) for key, value in kwargs.items() if value is not None}
         rs_parameters["duration"] = _convert_to_rs_expression(duration)
         return super().__new__(cls, name, rs_parameters)
 
     def out(self) -> str:
+        """Return the waveform as a valid Quil string."""
         return str(self)
 
     def get_parameter(self, name: str) -> Optional[ParameterDesignator]:
+        """Get a parameter in the waveform by name."""
         parameter = super().parameters.get(name, None)
         if parameter is None:
             return None
         return _convert_to_py_expression(parameter)
 
     def set_parameter(self, name: str, value: Optional[ParameterDesignator]) -> None:
+        """Set a parameter with a value."""
         parameters = super().parameters
         if value is None:
             parameters.pop(name, None)
@@ -1136,14 +1188,12 @@ def set_parameter(self, name: str, value: Optional[ParameterDesignator]) -> None
     duration = _template_waveform_property("duration", dtype=float)
 
     def num_samples(self, rate: float) -> int:
-        """The number of samples in the reference implementation of the waveform.
+        """Return the number of samples in the reference implementation of the waveform.
 
-        Note: this does not include any hardware-enforced alignment (cf.
-        documentation for `samples`).
+        Note: this does not include any hardware-enforced alignment (cf. documentation for `samples`).
 
         :param rate: The sample rate, in Hz.
         :return: The number of samples.
-
         """
         duration = self.duration.real
         if self.duration.imag != 0.0:
@@ -1151,7 +1201,7 @@ def num_samples(self, rate: float) -> int:
         return int(np.ceil(duration * rate))
 
     def samples(self, rate: float) -> np.ndarray:
-        """A reference implementation of waveform sample generation.
+        """Generate samples of the waveform.
 
         Note: this is close but not always exactly equivalent to the actual IQ
         values produced by the waveform generators on Rigetti hardware. The
@@ -1166,22 +1216,23 @@ def samples(self, rate: float) -> np.ndarray:
 
     @classmethod
     def _from_rs_waveform_invocation(cls, waveform: quil_rs.WaveformInvocation) -> "TemplateWaveform":
-        """
-        The ``quil`` package has no equivalent to ``TemplateWaveform``s, this function checks the name and
-        properties of a ``quil`` ``WaveformInvocation`` to see if they potentially match a subclass of
-        ``TemplateWaveform``. If a match is found and construction succeeds, then that type is returned.
-        Otherwise, a generic ``WaveformInvocation`` is returned.
+        """Build a TemplateWaveform from a ``quil`` waveform invocation.
+
+        The ``quil`` package has no equivalent to ``TemplateWaveform``s, this function checks the name and properties of
+        a ``quil`` ``WaveformInvocation`` to see if they potentially match a subclass of ``TemplateWaveform``. If a
+        match is found and construction succeeds, then that type is returned. Otherwise, a generic
+        ``WaveformInvocation`` is returned.
         """
         from pyquil.quiltwaveforms import (
+            BoxcarAveragerKernel,
+            DragGaussianWaveform,
+            ErfSquareWaveform,
             FlatWaveform,
             GaussianWaveform,
-            DragGaussianWaveform,
             HrmGaussianWaveform,
-            ErfSquareWaveform,
-            BoxcarAveragerKernel,
         )
 
-        template: Type["TemplateWaveform"]  # mypy needs a type annotation here to understand this.
+        template: type[TemplateWaveform]  # mypy needs a type annotation here to understand this.
         for template in [
             FlatWaveform,
             GaussianWaveform,
diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py
index 281aff2af..c98d38351 100644
--- a/pyquil/quilbase.py
+++ b/pyquil/quilbase.py
@@ -13,66 +13,58 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-"""
-Contains the core pyQuil objects that correspond to Quil instructions.
-"""
-import abc
+"""Contains the core pyQuil objects that correspond to Quil instructions."""
 
+import abc
+from collections.abc import Container, Iterable, Sequence
 from typing import (
+    TYPE_CHECKING,
     Any,
     Callable,
     ClassVar,
-    Container,
-    Dict,
-    Iterable,
-    List,
     Optional,
-    Sequence,
-    Set,
-    Tuple,
     Union,
-    TYPE_CHECKING,
 )
-from typing_extensions import Self
 
 import numpy as np
 from deprecated.sphinx import deprecated
+from typing_extensions import Self
 
 from pyquil.quilatom import (
     Expression,
+    FormalArgument,
+    Frame,
     Label,
     LabelPlaceholder,
     MemoryReference,
     Parameter,
     ParameterDesignator,
-    Frame,
-    Waveform,
     Qubit,
     QubitDesignator,
     QubitPlaceholder,
-    FormalArgument,
+    Waveform,
+    _convert_to_py_expression,
+    _convert_to_py_expressions,
     _convert_to_py_qubit,
     _convert_to_py_qubits,
+    _convert_to_py_waveform,
     _convert_to_rs_expression,
     _convert_to_rs_expressions,
     _convert_to_rs_qubit,
     _convert_to_rs_qubits,
-    _convert_to_py_expression,
-    _convert_to_py_expressions,
-    _convert_to_py_waveform,
     unpack_qubit,
 )
 
 if TYPE_CHECKING:  # avoids circular import
     from pyquil.paulis import PauliSum
 
-import quil.instructions as quil_rs
 import quil.expression as quil_rs_expr
+import quil.instructions as quil_rs
 
 
 class _InstructionMeta(abc.ABCMeta):
-    """
-    A metaclass that allows us to group all instruction types from quil-rs and pyQuil as an `AbstractInstruction`.
+    """A metaclass that allows us to group all instruction types from quil-rs and pyQuil as an `AbstractInstruction`.
+
     As such, this should _only_ be used as a metaclass for `AbstractInstruction`.
     """
 
@@ -97,9 +89,7 @@ def __instancecheck__(self, __instance: Any) -> bool:
 
 
 class AbstractInstruction(metaclass=_InstructionMeta):
-    """
-    Abstract class for representing single instructions.
-    """
+    """Abstract class for representing single instructions."""
 
     def __str__(self) -> str:
         return self.__repr__()
@@ -198,7 +188,7 @@ def _convert_to_rs_instruction(instr: Union[AbstractInstruction, quil_rs.Instruc
     raise ValueError(f"{type(instr)} is not an Instruction")
 
 
-def _convert_to_rs_instructions(instrs: Iterable[AbstractInstruction]) -> List[quil_rs.Instruction]:
+def _convert_to_rs_instructions(instrs: Iterable[AbstractInstruction]) -> list[quil_rs.Instruction]:
     return [_convert_to_rs_instruction(instr) for instr in instrs]
 
 
@@ -299,7 +289,7 @@ def _convert_to_py_instruction(instr: Any) -> AbstractInstruction:
     raise ValueError(f"{type(instr)} is not a valid Instruction type")
 
 
-def _convert_to_py_instructions(instrs: Iterable[quil_rs.Instruction]) -> List[AbstractInstruction]:
+def _convert_to_py_instructions(instrs: Iterable[quil_rs.Instruction]) -> list[AbstractInstruction]:
     return [_convert_to_py_instruction(instr) for instr in instrs]
 
 
@@ -330,9 +320,7 @@ def _convert_to_py_instructions(instrs: Iterable[quil_rs.Instruction]) -> List[A
 
 
 class Gate(quil_rs.Gate, AbstractInstruction):
-    """
-    This is the pyQuil object for a quantum gate instruction.
-    """
+    """A quantum gate instruction."""
 
     def __new__(
         cls,
@@ -341,6 +329,7 @@ def __new__(
         qubits: Sequence[Union[Qubit, QubitPlaceholder, FormalArgument, int]],
         modifiers: Sequence[quil_rs.GateModifier] = [],
     ) -> Self:
+        """Initialize a new gate instruction."""
         return super().__new__(
             cls, name, _convert_to_rs_expressions(params), _convert_to_rs_qubits(qubits), list(modifiers)
         )
@@ -354,13 +343,15 @@ def _from_rs_gate(cls, gate: quil_rs.Gate) -> Self:
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
     def get_qubits(self, indices: bool = True) -> Sequence[QubitDesignator]:
+        """Get the qubits the gate operates on."""
         if indices:
             return self.get_qubit_indices()
         else:
             return _convert_to_py_qubits(super().qubits)
 
     @property  # type: ignore[override]
-    def qubits(self) -> List[QubitDesignator]:
+    def qubits(self) -> list[QubitDesignator]:
+        """Get the qubits the gate operates on."""
         return self.get_qubits(indices=False)  # type: ignore
 
     @qubits.setter
@@ -369,6 +360,7 @@ def qubits(self, qubits: Sequence[Union[Qubit, QubitPlaceholder, FormalArgument]
 
     @property
     def params(self) -> Sequence[ParameterDesignator]:
+        """Get the parameters the gate operates on."""
         return _convert_to_py_expressions(super().parameters)
 
     @params.setter
@@ -376,11 +368,12 @@ def params(self, params: Sequence[ParameterDesignator]) -> None:
         quil_rs.Gate.parameters.__set__(self, _convert_to_rs_expressions(params))  # type: ignore
 
     @property  # type: ignore[override]
-    def modifiers(self) -> List[str]:
+    def modifiers(self) -> list[str]:
+        """Get all modifiers applied to the gate."""
         return [str(modifier).upper() for modifier in super().modifiers]
 
     @modifiers.setter
-    def modifiers(self, modifiers: Union[List[str], List[quil_rs.GateModifier]]) -> None:
+    def modifiers(self, modifiers: Union[list[str], list[quil_rs.GateModifier]]) -> None:
         modifiers = [
             self._to_rs_gate_modifier(modifier) if isinstance(modifier, str) else modifier for modifier in modifiers
         ]
@@ -396,7 +389,8 @@ def _to_rs_gate_modifier(self, modifier: str) -> quil_rs.GateModifier:
             return quil_rs.GateModifier.Forked
         raise ValueError(f"{modifier} is not a valid Gate modifier.")
 
-    def get_qubit_indices(self) -> List[int]:
+    def get_qubit_indices(self) -> list[int]:
+        """Get the qubits the gate operates on, as integer indices."""
         return [qubit.to_fixed() for qubit in super().qubits]
 
     def controlled(
@@ -407,10 +401,7 @@ def controlled(
             Sequence[Union[QubitDesignator, quil_rs.Qubit]],
         ],
     ) -> "Gate":
-        """
-        Add the CONTROLLED modifier to the gate with the given control qubit or Sequence of control
-        qubits.
-        """
+        """Add the CONTROLLED modifier to the gate with the given control qubit or Sequence of control qubits."""
         if isinstance(control_qubit, Sequence):
             for qubit in control_qubit:
                 self._update_super(super().controlled(_convert_to_rs_qubit(qubit)))
@@ -424,29 +415,25 @@ def forked(
         fork_qubit: Union[quil_rs.Qubit, QubitDesignator],
         alt_params: Union[Sequence[ParameterDesignator], Sequence[quil_rs_expr.Expression]],
     ) -> "Gate":
-        """
-        Add the FORKED modifier to the gate with the given fork qubit and given additional
-        parameters.
-        """
+        """Add the FORKED modifier to the gate with the given fork qubit and given additional parameters."""
         forked = super().forked(_convert_to_rs_qubit(fork_qubit), _convert_to_rs_expressions(alt_params))
         self._update_super(forked)
         return self
 
     def dagger(self) -> "Gate":
-        """
-        Add the DAGGER modifier to the gate.
-        """
+        """Add the DAGGER modifier to the gate."""
         self._update_super(super().dagger())
         return self
 
     def out(self) -> str:
+        """Return the Gate as a valid Quil string."""
         return super().to_quil()
 
     def _update_super(self, gate: quil_rs.Gate) -> None:
-        """
-        Updates the state of the super class using a new gate.
-        The super class does not mutate the value of a gate when adding
-        modifiers with methods like `dagger()`, but pyQuil does.
+        """Update the state of the super class using a new gate.
+
+        The super class does not mutate the value of a gate when adding modifiers with methods like `dagger()`,
+        but pyQuil does.
         """
         quil_rs.Gate.name.__set__(self, gate.name)  # type: ignore[attr-defined]
         quil_rs.Gate.parameters.__set__(self, gate.parameters)  # type: ignore[attr-defined]
@@ -459,13 +446,12 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Gate":
+    def __deepcopy__(self, memo: dict) -> "Gate":
         return Gate._from_rs_gate(super().__deepcopy__(memo))
 
 
 def _strip_modifiers(gate: Gate, limit: Optional[int] = None) -> Gate:
-    """
-    Remove modifiers from :py:class:`Gate`.
+    """Remove modifiers from :py:class:`Gate`.
 
     This function removes up to ``limit`` gate modifiers from the given gate,
     starting from the leftmost gate modifier.
@@ -495,7 +481,7 @@ def _strip_modifiers(gate: Gate, limit: Optional[int] = None) -> Gate:
         elif m == "DAGGER":
             pass
         else:
-            raise TypeError("Unsupported gate modifier {}".format(m))
+            raise TypeError(f"Unsupported gate modifier {m}")
 
     stripped = Gate(gate.name, gate.params[:param_index], gate.qubits[qubit_index:])
     stripped.modifiers = gate.modifiers[limit:]
@@ -503,15 +489,14 @@ def _strip_modifiers(gate: Gate, limit: Optional[int] = None) -> Gate:
 
 
 class Measurement(quil_rs.Measurement, AbstractInstruction):
-    """
-    This is the pyQuil object for a Quil measurement instruction.
-    """
+    """A Quil measurement instruction."""
 
     def __new__(
         cls,
         qubit: QubitDesignator,
         classical_reg: Optional[MemoryReference],
     ) -> Self:
+        """Initialize a new measurement instruction."""
         target = cls._reg_to_target(classical_reg)
         return super().__new__(cls, _convert_to_rs_qubit(qubit), target)
 
@@ -531,6 +516,7 @@ def _from_rs_measurement(cls, measurement: quil_rs.Measurement) -> "Measurement"
 
     @property  # type: ignore[override]
     def qubit(self) -> QubitDesignator:
+        """Get the qubit the instruction measures."""
         return _convert_to_py_qubit(super().qubit)
 
     @qubit.setter
@@ -539,6 +525,7 @@ def qubit(self, qubit: QubitDesignator) -> None:
 
     @property
     def classical_reg(self) -> Optional[MemoryReference]:
+        """Get the MemoryReference that this instruction writes to, if any."""
         target = super().target
         if target is None:
             return None
@@ -553,16 +540,19 @@ def classical_reg(self, classical_reg: Optional[MemoryReference]) -> None:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubit this instruction measures."""
         if indices:
             return self.get_qubit_indices()
         else:
             return {_convert_to_py_qubit(super().qubit)}
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubit this instruction measures, as an integer index."""
         return {super().qubit.to_fixed()}
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -571,16 +561,15 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Measurement":
+    def __deepcopy__(self, memo: dict) -> "Measurement":
         return Measurement._from_rs_measurement(super().__deepcopy__(memo))
 
 
 class Reset(quil_rs.Reset, AbstractInstruction):
-    """
-    The RESET instruction.
-    """
+    """The RESET instruction."""
 
     def __new__(cls, qubit: Optional[Union[Qubit, QubitPlaceholder, FormalArgument, int]] = None) -> Self:
+        """Initialize a new reset instruction."""
         rs_qubit: Optional[quil_rs.Qubit] = None
         if qubit is not None:
             rs_qubit = _convert_to_rs_qubit(qubit)
@@ -591,26 +580,30 @@ def _from_rs_reset(cls, reset: quil_rs.Reset) -> "Reset":
         return super().__new__(cls, reset.qubit)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     @deprecated(
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Optional[Set[QubitDesignator]]:
+    def get_qubits(self, indices: bool = True) -> Optional[set[QubitDesignator]]:
+        """Get the qubit this instruction resets."""
         if super().qubit is None:
             return None
         if indices:
             return self.get_qubit_indices()  # type: ignore
         return {_convert_to_py_qubit(super().qubit)}  # type: ignore
 
-    def get_qubit_indices(self) -> Optional[Set[int]]:
+    def get_qubit_indices(self) -> Optional[set[int]]:
+        """Get the qubit this instruction resets, as an integer index."""
         if super().qubit is None:
             return None
         return {super().qubit.to_fixed()}  # type: ignore
 
     @property  # type: ignore[override]
     def qubit(self) -> Optional[QubitDesignator]:
+        """Get the qubit this instruction resets, if any."""
         if super().qubit:
             return _convert_to_py_qubit(super().qubit)  # type: ignore
         return None
@@ -628,18 +621,17 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Reset":
+    def __deepcopy__(self, memo: dict) -> "Reset":
         copy = Reset._from_rs_reset(super().__deepcopy__(memo))
         copy.__class__ = self.__class__
         return copy
 
 
 class ResetQubit(Reset):
-    """
-    This is the pyQuil object for a Quil targeted reset instruction.
-    """
+    """A targeted RESET instruction."""
 
     def __new__(cls, qubit: Union[Qubit, QubitPlaceholder, FormalArgument, int]) -> Self:
+        """Initialize a new reset instruction, with a target qubit."""
         if qubit is None:
             raise TypeError("qubit should not be None")
         return super().__new__(cls, qubit)
@@ -653,20 +645,20 @@ def _from_rs_reset(cls, reset: quil_rs.Reset) -> "ResetQubit":
 
 
 class DefGate(quil_rs.GateDefinition, AbstractInstruction):
-    """
-    A DEFGATE directive.
-
-    :param name: The name of the newly defined gate.
-    :param matrix: The matrix defining this gate.
-    :param parameters: list of parameters that are used in this gate
-    """
+    """A DEFGATE directive."""
 
     def __new__(
         cls,
         name: str,
-        matrix: Union[List[List[Expression]], np.ndarray, np.matrix],
-        parameters: Optional[List[Parameter]] = None,
+        matrix: Union[list[list[Expression]], np.ndarray, np.matrix],
+        parameters: Optional[list[Parameter]] = None,
     ) -> Self:
+        """Initialize a new gate definition.
+
+        :param name: The name of the newly defined gate.
+        :param matrix: The matrix defining this gate.
+        :param parameters: list of parameters that are used in this gate
+        """
         DefGate._validate_matrix(matrix, parameters is not None and len(parameters) > 0)
         specification = DefGate._convert_to_matrix_specification(matrix)
         rs_parameters = [param.name for param in parameters or []]
@@ -678,14 +670,14 @@ def _from_rs_gate_definition(cls, gate_definition: quil_rs.GateDefinition) -> Se
 
     @staticmethod
     def _convert_to_matrix_specification(
-        matrix: Union[List[List[Expression]], np.ndarray, np.matrix]
+        matrix: Union[list[list[Expression]], np.ndarray, np.matrix],
     ) -> quil_rs.GateSpecification:
         to_rs_matrix = np.vectorize(_convert_to_rs_expression, otypes=["O"])
         return quil_rs.GateSpecification.from_matrix(to_rs_matrix(np.asarray(matrix)))
 
     @staticmethod
     def _validate_matrix(
-        matrix: Union[List[List[Expression]], np.ndarray, np.matrix], contains_parameters: bool
+        matrix: Union[list[list[Expression]], np.ndarray, np.matrix], contains_parameters: bool
     ) -> None:
         if isinstance(matrix, list):
             rows = len(matrix)
@@ -699,7 +691,7 @@ def _validate_matrix(
             raise TypeError("Matrix argument must be a list or NumPy array/matrix")
 
         if 0 != rows & (rows - 1):
-            raise ValueError("Dimension of matrix must be a power of 2, got {0}".format(rows))
+            raise ValueError(f"Dimension of matrix must be a power of 2, got {rows}")
 
         if not contains_parameters:
             np_matrix = np.asarray(matrix)
@@ -708,12 +700,13 @@ def _validate_matrix(
                 raise ValueError("Matrix must be unitary.")
 
     def out(self) -> str:
+        """Return the Gate as a valid Quil string."""
         return super().to_quil()
 
     def get_constructor(self) -> Union[Callable[..., Gate], Callable[..., Callable[..., Gate]]]:
-        """
-        :returns: A function that constructs this gate on variable qubit indices. E.g.
-                  `mygate.get_constructor()(1) applies the gate to qubit 1.`
+        """Return a function that constructs this gate on variable qubit indices.
+
+        For example, `mygate.get_constructor()(1) applies the gate to qubit 1.`
         """
         if self.parameters:
             return lambda *params: lambda *qubits: Gate(
@@ -723,14 +716,13 @@ def get_constructor(self) -> Union[Callable[..., Gate], Callable[..., Callable[.
             return lambda *qubits: Gate(name=self.name, params=[], qubits=list(map(unpack_qubit, qubits)))
 
     def num_args(self) -> int:
-        """
-        :return: The number of qubit arguments the gate takes.
-        """
+        """Get the number of qubit arguments the gate takes."""
         rows = len(self.matrix)
         return int(np.log2(rows))
 
     @property
     def matrix(self) -> np.ndarray:
+        """Get the matrix that defines this GateDefinition."""
         to_py_matrix = np.vectorize(_convert_to_py_expression, otypes=["O"])
         return to_py_matrix(np.asarray(super().specification.to_matrix()))  # type: ignore[no-any-return]
 
@@ -739,11 +731,12 @@ def matrix(self, matrix: np.ndarray) -> None:
         quil_rs.GateDefinition.specification.__set__(self, DefGate._convert_to_matrix_specification(matrix))  # type: ignore[attr-defined] # noqa
 
     @property  # type: ignore[override]
-    def parameters(self) -> List[Parameter]:
+    def parameters(self) -> list[Parameter]:
+        """Get the parameters this gate definition takes."""
         return [Parameter(name) for name in super().parameters]
 
     @parameters.setter
-    def parameters(self, parameters: Optional[List[Parameter]]) -> None:
+    def parameters(self, parameters: Optional[list[Parameter]]) -> None:
         quil_rs.GateDefinition.parameters.__set__(self, [param.name for param in parameters or []])  # type: ignore[attr-defined] # noqa
 
     def __hash__(self) -> int:
@@ -755,33 +748,35 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "DefGate":
+    def __deepcopy__(self, memo: dict) -> "DefGate":
         return DefGate._from_rs_gate_definition(super().__deepcopy__(memo))
 
 
 class DefPermutationGate(DefGate):
-    def __new__(cls, name: str, permutation: Union[List[int], np.ndarray]) -> Self:
+    """A gate defined by a permutation of numbers."""
+
+    def __new__(cls, name: str, permutation: Union[list[int], np.ndarray]) -> Self:
+        """Initialize a new gate definition with a permutation."""
         specification = DefPermutationGate._convert_to_permutation_specification(permutation)
         gate_definition = quil_rs.GateDefinition(name, [], specification)
         return super()._from_rs_gate_definition(gate_definition)
 
     @staticmethod
-    def _convert_to_permutation_specification(permutation: Union[List[int], np.ndarray]) -> quil_rs.GateSpecification:
+    def _convert_to_permutation_specification(permutation: Union[list[int], np.ndarray]) -> quil_rs.GateSpecification:
         return quil_rs.GateSpecification.from_permutation([int(x) for x in permutation])
 
     @property
-    def permutation(self) -> List[int]:
+    def permutation(self) -> list[int]:
+        """Get the permutation that defines the gate."""
         return super().specification.to_permutation()
 
     @permutation.setter
-    def permutation(self, permutation: List[int]) -> None:
+    def permutation(self, permutation: list[int]) -> None:
         specification = DefPermutationGate._convert_to_permutation_specification(permutation)
         quil_rs.GateDefinition.specification.__set__(self, specification)  # type: ignore[attr-defined]
 
     def num_args(self) -> int:
-        """
-        :return: The number of qubit arguments the gate takes.
-        """
+        """Get the number of arguments the gate takes."""
         return int(np.log2(len(self.permutation)))
 
     def __str__(self) -> str:
@@ -789,17 +784,16 @@ def __str__(self) -> str:
 
 
 class DefGateByPaulis(DefGate):
-    """
-    Records a gate definition as the exponentiation of a PauliSum.
-    """
+    """A gate definition defined by the exponentiation of a PauliSum."""
 
     def __new__(
         cls,
         gate_name: str,
-        parameters: List[Parameter],
-        arguments: List[QubitDesignator],
+        parameters: list[Parameter],
+        arguments: list[QubitDesignator],
         body: "PauliSum",
     ) -> Self:
+        """Initialize a new gate, defined by a PauliSum."""
         specification = DefGateByPaulis._convert_to_pauli_specification(body, arguments)
         rs_parameters = [param.name for param in parameters]
         gate_definition = quil_rs.GateDefinition(gate_name, rs_parameters, specification)
@@ -807,7 +801,7 @@ def __new__(
 
     @staticmethod
     def _convert_to_pauli_specification(
-        body: "PauliSum", arguments: List[QubitDesignator]
+        body: "PauliSum", arguments: list[QubitDesignator]
     ) -> quil_rs.GateSpecification:
         if isinstance(body, Sequence):
             from pyquil.paulis import PauliSum
@@ -816,17 +810,19 @@ def _convert_to_pauli_specification(
         return quil_rs.GateSpecification.from_pauli_sum(body._to_rs_pauli_sum(arguments))
 
     @property
-    def arguments(self) -> List[FormalArgument]:
+    def arguments(self) -> list[FormalArgument]:
+        """Get the arguments the gate takes."""
         return [FormalArgument(arg) for arg in super().specification.to_pauli_sum().arguments]
 
     @arguments.setter
-    def arguments(self, arguments: List[QubitDesignator]) -> None:
+    def arguments(self, arguments: list[QubitDesignator]) -> None:
         pauli_sum = super().specification.to_pauli_sum()
         pauli_sum.arguments = [str(arg) for arg in arguments]
         quil_rs.GateDefinition.specification.__set__(self, quil_rs.GateSpecification.from_pauli_sum(pauli_sum))  # type: ignore[attr-defined] # noqa
 
     @property
     def body(self) -> "PauliSum":
+        """Get the PauliSum that defines the gate."""
         from pyquil.paulis import PauliSum  # avoids circular import
 
         return PauliSum._from_rs_pauli_sum(super().specification.to_pauli_sum())
@@ -837,6 +833,7 @@ def body(self, body: "PauliSum") -> None:
         quil_rs.GateDefinition.specification.__set__(self, specification)  # type: ignore[attr-defined]
 
     def num_args(self) -> int:
+        """Get the number of arguments the gate takes."""
         return len(self.arguments)
 
     def __str__(self) -> str:
@@ -844,11 +841,10 @@ def __str__(self) -> str:
 
 
 class JumpTarget(quil_rs.Label, AbstractInstruction):
-    """
-    Representation of a target that can be jumped to.
-    """
+    """Representation of a target that can be jumped to."""
 
     def __new__(cls, label: Union[Label, LabelPlaceholder]) -> Self:
+        """Initialize a new target."""
         return super().__new__(cls, label.target)
 
     @classmethod
@@ -857,29 +853,36 @@ def _from_rs_label(cls, label: quil_rs.Label) -> "JumpTarget":
 
     @property
     def label(self) -> Union[Label, LabelPlaceholder]:
+        """Get the target as a label."""
         if super().target.is_placeholder():
             return LabelPlaceholder._from_rs_target(super().target)
         return Label._from_rs_target(super().target)
 
     def __repr__(self) -> str:
-        return "<JumpTarget {0}>".format(str(self.label))
+        return f"<JumpTarget {str(self.label)}>"
 
     def out(self) -> str:
+        """Return the target as valid Quil. Raises an error if the target is an unresolved placeholder."""
         return super().to_quil()
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "JumpTarget":
+    def __deepcopy__(self, memo: dict) -> "JumpTarget":
         return JumpTarget._from_rs_label(super().__deepcopy__(memo))
 
 
 class JumpWhen(quil_rs.JumpWhen, AbstractInstruction):
-    """
-    The JUMP-WHEN instruction.
-    """
+    """The JUMP-WHEN instruction."""
 
     def __new__(cls, target: Union[Label, LabelPlaceholder], condition: MemoryReference) -> Self:
+        """Initialize a new JumpWhen instruction.
+
+        :param target: The target to jump to if the condition is true.
+        :param condition: A memory reference that determines if the jump should be performed. The memory reference must
+            refer to an INTEGER or BIT. The jump will be performed if the value in the reference is not 0 when the
+            instruction is evaluated.
+        """
         return super().__new__(cls, target.target, condition._to_rs_memory_reference())
 
     @classmethod
@@ -887,10 +890,12 @@ def _from_rs_jump_when(cls, jump_when: quil_rs.JumpWhen) -> Self:
         return super().__new__(cls, jump_when.target, jump_when.condition)
 
     def out(self) -> str:
+        """Return the instruction as valid Quil. Raises an error if the target is an unresolved placeholder."""
         return super().to_quil()
 
     @property  # type: ignore[override]
     def condition(self) -> MemoryReference:
+        """Get the MemoryReference the instruction uses to determine if the jump should be performed or not."""
         return MemoryReference._from_rs_memory_reference(super().condition)
 
     @condition.setter
@@ -899,6 +904,7 @@ def condition(self, condition: MemoryReference) -> None:
 
     @property  # type: ignore[override]
     def target(self) -> Union[Label, LabelPlaceholder]:
+        """Get the target the instruction will jump to if the condition bit is not 1."""
         if super().target.is_placeholder():
             return LabelPlaceholder._from_rs_target(super().target)
         return Label._from_rs_target(super().target)
@@ -913,16 +919,21 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "JumpWhen":
+    def __deepcopy__(self, memo: dict) -> "JumpWhen":
         return JumpWhen._from_rs_jump_when(super().__deepcopy__(memo))
 
 
 class JumpUnless(quil_rs.JumpUnless, AbstractInstruction):
-    """
-    The JUMP-UNLESS instruction.
-    """
+    """The JUMP-UNLESS instruction."""
 
     def __new__(cls, target: Union[Label, LabelPlaceholder], condition: MemoryReference) -> Self:
+        """Initialize a new JumpUnless instruction.
+
+        :param target: The target to jump to if the condition is true.
+        :param condition: A memory reference that determines if the jump should be performed. The memory reference must
+            refer to an INTEGER or BIT. The jump will be performed if the value in the reference is 0 when the
+            instruction is evaluated.
+        """
         return super().__new__(cls, target.target, condition._to_rs_memory_reference())
 
     @classmethod
@@ -930,10 +941,12 @@ def _from_rs_jump_unless(cls, jump_unless: quil_rs.JumpUnless) -> Self:
         return super().__new__(cls, jump_unless.target, jump_unless.condition)
 
     def out(self) -> str:
+        """Return the instruction as valid Quil. Raises an error if the target is an unresolved placeholder."""
         return super().to_quil()
 
     @property  # type: ignore[override]
     def condition(self) -> MemoryReference:
+        """Get the MemoryReference the instruction uses to determine if the jump should be performed or not."""
         return MemoryReference._from_rs_memory_reference(super().condition)
 
     @condition.setter
@@ -942,6 +955,7 @@ def condition(self, condition: MemoryReference) -> None:
 
     @property  # type: ignore[override]
     def target(self) -> Union[Label, LabelPlaceholder]:
+        """Get the target the instruction will jump to if the condition bit is not 1."""
         if super().target.is_placeholder():
             return LabelPlaceholder._from_rs_target(super().target)
         return Label._from_rs_target(super().target)
@@ -956,18 +970,17 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "JumpUnless":
+    def __deepcopy__(self, memo: dict) -> "JumpUnless":
         return JumpUnless._from_rs_jump_unless(super().__deepcopy__(memo))
 
 
 class SimpleInstruction(AbstractInstruction):
-    """
-    Abstract class for simple instructions with no arguments.
-    """
+    """Base class for simple instructions with no arguments."""
 
     instruction: ClassVar[quil_rs.Instruction]
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return self.instruction.to_quil()
 
     def __str__(self) -> str:
@@ -976,42 +989,35 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "SimpleInstruction":
+    def __deepcopy__(self, memo: dict) -> "SimpleInstruction":
         return self
 
 
 class Halt(SimpleInstruction):
-    """
-    The HALT instruction.
-    """
+    """The HALT instruction."""
 
     instruction = quil_rs.Instruction.new_halt()
 
 
 class Wait(SimpleInstruction):
-    """
-    The WAIT instruction.
-    """
+    """The WAIT instruction."""
 
     instruction = quil_rs.Instruction.new_wait()
 
 
 class Nop(SimpleInstruction):
-    """
-    The NOP instruction.
-    """
+    """The NOP instruction."""
 
     instruction = quil_rs.Instruction.new_nop()
 
 
 class UnaryClassicalInstruction(quil_rs.UnaryLogic, AbstractInstruction):
-    """
-    The abstract class for unary classical instructions.
-    """
+    """Base class for unary classical instructions."""
 
     op: ClassVar[quil_rs.UnaryOperator]
 
     def __new__(cls, target: MemoryReference) -> "UnaryClassicalInstruction":
+        """Initialize a new unary classical instruction."""
         return super().__new__(cls, cls.op, target._to_rs_memory_reference())
 
     @classmethod
@@ -1020,6 +1026,7 @@ def _from_rs_unary_logic(cls, unary_logic: quil_rs.UnaryLogic) -> Self:
 
     @property
     def target(self) -> MemoryReference:
+        """The MemoryReference that the instruction operates on."""
         return MemoryReference._from_rs_memory_reference(super().operand)
 
     @target.setter
@@ -1027,6 +1034,7 @@ def target(self, target: MemoryReference) -> None:
         quil_rs.UnaryLogic.operand.__set__(self, target._to_rs_memory_reference())  # type: ignore
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string. Raises an error if the instruction contains placeholders."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1035,36 +1043,31 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "UnaryClassicalInstruction":
+    def __deepcopy__(self, memo: dict) -> "UnaryClassicalInstruction":
         copy = UnaryClassicalInstruction._from_rs_unary_logic(super().__deepcopy__(memo))
         copy.__class__ = self.__class__
         return copy
 
 
 class ClassicalNeg(UnaryClassicalInstruction):
-    """
-    The NEG instruction.
-    """
+    """The NEG instruction."""
 
     op = quil_rs.UnaryOperator.Neg
 
 
 class ClassicalNot(UnaryClassicalInstruction):
-    """
-    The NOT instruction.
-    """
+    """The NOT instruction."""
 
     op = quil_rs.UnaryOperator.Not
 
 
 class LogicalBinaryOp(quil_rs.BinaryLogic, AbstractInstruction):
-    """
-    The abstract class for binary logical classical instructions.
-    """
+    """Base class for binary logical classical instructions."""
 
     op: ClassVar[quil_rs.BinaryOperator]
 
     def __new__(cls, left: MemoryReference, right: Union[MemoryReference, int]) -> Self:
+        """Initialize the operands of the binary logical instruction."""
         operands = cls._to_rs_binary_operands(left, right)
         return super().__new__(cls, cls.op, operands)
 
@@ -1092,6 +1095,7 @@ def _to_py_binary_operand(operand: quil_rs.BinaryOperand) -> Union[MemoryReferen
 
     @property
     def left(self) -> MemoryReference:
+        """The left hand side of the binary expression."""
         return MemoryReference._from_rs_memory_reference(super().operands.memory_reference)
 
     @left.setter
@@ -1102,6 +1106,7 @@ def left(self, left: MemoryReference) -> None:
 
     @property
     def right(self) -> Union[MemoryReference, int]:
+        """The right hand side of the binary expression."""
         return self._to_py_binary_operand(super().operands.operand)
 
     @right.setter
@@ -1111,6 +1116,7 @@ def right(self, right: Union[MemoryReference, int]) -> None:
         quil_rs.BinaryLogic.operands.__set__(self, operands)  # type: ignore[attr-defined]
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string. Raises an error if the instruction contains placeholders."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1119,44 +1125,37 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "LogicalBinaryOp":
+    def __deepcopy__(self, memo: dict) -> "LogicalBinaryOp":
         copy = LogicalBinaryOp._from_rs_binary_logic(super().__deepcopy__(memo))
         copy.__class__ = self.__class__
         return copy
 
 
 class ClassicalAnd(LogicalBinaryOp):
-    """
-    The AND instruction.
-    """
+    """The AND instruction."""
 
     op = quil_rs.BinaryOperator.And
 
 
 class ClassicalInclusiveOr(LogicalBinaryOp):
-    """
-    The IOR instruction.
-    """
+    """The IOR instruction."""
 
     op = quil_rs.BinaryOperator.Ior
 
 
 class ClassicalExclusiveOr(LogicalBinaryOp):
-    """
-    The XOR instruction.
-    """
+    """The XOR instruction."""
 
     op = quil_rs.BinaryOperator.Xor
 
 
 class ArithmeticBinaryOp(quil_rs.Arithmetic, AbstractInstruction):
-    """
-    The abstract class for binary arithmetic classical instructions.
-    """
+    """Base class for binary arithmetic classical instructions."""
 
     op: ClassVar[quil_rs.ArithmeticOperator]
 
     def __new__(cls, left: MemoryReference, right: Union[MemoryReference, int, float]) -> Self:
+        """Initialize the operands of the binary arithmetic instruction."""
         left_operand = quil_rs.ArithmeticOperand.from_memory_reference(left._to_rs_memory_reference())
         right_operand = _to_rs_arithmetic_operand(right)
         return super().__new__(cls, cls.op, left_operand, right_operand)
@@ -1167,6 +1166,7 @@ def _from_rs_arithmetic(cls, arithmetic: quil_rs.Arithmetic) -> "ArithmeticBinar
 
     @property
     def left(self) -> MemoryReference:
+        """The left hand side of the binary expression."""
         return MemoryReference._from_rs_memory_reference(super().destination.to_memory_reference())
 
     @left.setter
@@ -1177,6 +1177,7 @@ def left(self, left: MemoryReference) -> None:
 
     @property
     def right(self) -> Union[MemoryReference, int, float]:
+        """The left hand side of the binary expression."""
         return _to_py_arithmetic_operand(super().source)
 
     @right.setter
@@ -1184,6 +1185,7 @@ def right(self, right: Union[MemoryReference, int, float]) -> None:
         quil_rs.Arithmetic.source.__set__(self, _to_rs_arithmetic_operand(right))  # type: ignore[attr-defined]
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string. Raises an error if the instruction contains placeholders."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1192,50 +1194,41 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ArithmeticBinaryOp":
+    def __deepcopy__(self, memo: dict) -> "ArithmeticBinaryOp":
         copy = ArithmeticBinaryOp._from_rs_arithmetic(super().__deepcopy__(memo))
         copy.__class__ = self.__class__
         return copy
 
 
 class ClassicalAdd(ArithmeticBinaryOp):
-    """
-    The ADD instruction.
-    """
+    """The ADD instruction."""
 
     op = quil_rs.ArithmeticOperator.Add
 
 
 class ClassicalSub(ArithmeticBinaryOp):
-    """
-    The SUB instruction.
-    """
+    """The SUB instruction."""
 
     op = quil_rs.ArithmeticOperator.Subtract
 
 
 class ClassicalMul(ArithmeticBinaryOp):
-    """
-    The MUL instruction.
-    """
+    """The MUL instruction."""
 
     op = quil_rs.ArithmeticOperator.Multiply
 
 
 class ClassicalDiv(ArithmeticBinaryOp):
-    """
-    The DIV instruction.
-    """
+    """The DIV instruction."""
 
     op = quil_rs.ArithmeticOperator.Divide
 
 
 class ClassicalMove(quil_rs.Move, AbstractInstruction):
-    """
-    The MOVE instruction.
-    """
+    """The MOVE instruction."""
 
     def __new__(cls, left: MemoryReference, right: Union[MemoryReference, int, float]) -> "ClassicalMove":
+        """Initialize a new MOVE instruction."""
         return super().__new__(cls, left._to_rs_memory_reference(), _to_rs_arithmetic_operand(right))
 
     @classmethod
@@ -1244,6 +1237,7 @@ def _from_rs_move(cls, move: quil_rs.Move) -> Self:
 
     @property
     def left(self) -> MemoryReference:
+        """The left hand side (or "destination") of the move instruction."""
         return MemoryReference._from_rs_memory_reference(super().destination)
 
     @left.setter
@@ -1252,6 +1246,7 @@ def left(self, left: MemoryReference) -> None:
 
     @property
     def right(self) -> Union[MemoryReference, int, float]:
+        """The right hand side (or "source") of the move instruction."""
         return _to_py_arithmetic_operand(super().source)
 
     @right.setter
@@ -1259,6 +1254,7 @@ def right(self, right: Union[MemoryReference, int, float]) -> None:
         quil_rs.Move.source.__set__(self, _to_rs_arithmetic_operand(right))  # type: ignore
 
     def out(self) -> str:
+        """Return the move instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1267,20 +1263,19 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ClassicalMove":
+    def __deepcopy__(self, memo: dict) -> "ClassicalMove":
         return ClassicalMove._from_rs_move(super().__deepcopy__(memo))
 
 
 class ClassicalExchange(quil_rs.Exchange, AbstractInstruction):
-    """
-    The EXCHANGE instruction.
-    """
+    """The EXCHANGE instruction."""
 
     def __new__(
         cls,
         left: MemoryReference,
         right: MemoryReference,
     ) -> "ClassicalExchange":
+        """Initialize a new EXCHANGE instruction."""
         return super().__new__(cls, left._to_rs_memory_reference(), right._to_rs_memory_reference())
 
     @classmethod
@@ -1289,6 +1284,7 @@ def _from_rs_exchange(cls, exchange: quil_rs.Exchange) -> Self:
 
     @property  # type: ignore[override]
     def left(self) -> MemoryReference:
+        """The left hand side of the exchange instruction."""
         return MemoryReference._from_rs_memory_reference(super().left)
 
     @left.setter
@@ -1297,6 +1293,7 @@ def left(self, left: MemoryReference) -> None:
 
     @property  # type: ignore[override]
     def right(self) -> MemoryReference:
+        """The left hand side of the exchange instruction."""
         return MemoryReference._from_rs_memory_reference(super().right)
 
     @right.setter
@@ -1304,6 +1301,7 @@ def right(self, right: MemoryReference) -> None:
         quil_rs.Exchange.right.__set__(self, right._to_rs_memory_reference())  # type: ignore
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1312,16 +1310,15 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ClassicalExchange":
+    def __deepcopy__(self, memo: dict) -> "ClassicalExchange":
         return ClassicalExchange._from_rs_exchange(super().__deepcopy__(memo))
 
 
 class ClassicalConvert(quil_rs.Convert, AbstractInstruction):
-    """
-    The CONVERT instruction.
-    """
+    """The CONVERT instruction."""
 
     def __new__(cls, left: MemoryReference, right: MemoryReference) -> "ClassicalConvert":
+        """Initialize a new CONVERT instruction."""
         return super().__new__(cls, left._to_rs_memory_reference(), right._to_rs_memory_reference())
 
     @classmethod
@@ -1330,6 +1327,7 @@ def _from_rs_convert(cls, convert: quil_rs.Convert) -> Self:
 
     @property
     def left(self) -> MemoryReference:
+        """Return the left hand side (or "destination") of the conversion instruction."""
         return MemoryReference._from_rs_memory_reference(super().destination)
 
     @left.setter
@@ -1338,6 +1336,7 @@ def left(self, memory_reference: MemoryReference) -> None:
 
     @property
     def right(self) -> MemoryReference:
+        """Return the right hand side (or "source") of the conversion instruction."""
         return MemoryReference._from_rs_memory_reference(super().source)
 
     @right.setter
@@ -1345,6 +1344,7 @@ def right(self, memory_reference: MemoryReference) -> None:
         quil_rs.Convert.source.__set__(self, memory_reference._to_rs_memory_reference())  # type: ignore
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1353,16 +1353,15 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ClassicalConvert":
+    def __deepcopy__(self, memo: dict) -> "ClassicalConvert":
         return ClassicalConvert._from_rs_convert(super().__deepcopy__(memo))
 
 
 class ClassicalLoad(quil_rs.Load, AbstractInstruction):
-    """
-    The LOAD instruction.
-    """
+    """The LOAD instruction."""
 
     def __new__(cls, target: MemoryReference, left: str, right: MemoryReference) -> "ClassicalLoad":
+        """Initialize a new LOAD instruction."""
         return super().__new__(cls, target._to_rs_memory_reference(), left, right._to_rs_memory_reference())
 
     @classmethod
@@ -1371,6 +1370,7 @@ def _from_rs_load(cls, load: quil_rs.Load) -> Self:
 
     @property
     def target(self) -> MemoryReference:
+        """The MemoryReference that the instruction loads into."""
         return MemoryReference._from_rs_memory_reference(super().destination)
 
     @target.setter
@@ -1379,6 +1379,7 @@ def target(self, target: MemoryReference) -> None:
 
     @property
     def left(self) -> str:
+        """The left hand side of the LOAD instruction."""
         return super().source
 
     @left.setter
@@ -1387,6 +1388,7 @@ def left(self, left: str) -> None:
 
     @property
     def right(self) -> MemoryReference:
+        """The right hand side of the LOAD instruction."""
         return MemoryReference._from_rs_memory_reference(super().offset)
 
     @right.setter
@@ -1394,6 +1396,7 @@ def right(self, right: MemoryReference) -> None:
         quil_rs.Load.offset.__set__(self, right._to_rs_memory_reference())  # type: ignore
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1402,7 +1405,7 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ClassicalLoad":
+    def __deepcopy__(self, memo: dict) -> "ClassicalLoad":
         return ClassicalLoad._from_rs_load(super().__deepcopy__(memo))
 
 
@@ -1426,11 +1429,10 @@ def _to_py_arithmetic_operand(operand: quil_rs.ArithmeticOperand) -> Union[Memor
 
 
 class ClassicalStore(quil_rs.Store, AbstractInstruction):
-    """
-    The STORE instruction.
-    """
+    """The STORE instruction."""
 
     def __new__(cls, target: str, left: MemoryReference, right: Union[MemoryReference, int, float]) -> "ClassicalStore":
+        """Initialize a new STORE instruction."""
         rs_right = _to_rs_arithmetic_operand(right)
         return super().__new__(cls, target, left._to_rs_memory_reference(), rs_right)
 
@@ -1440,6 +1442,7 @@ def _from_rs_store(cls, store: quil_rs.Store) -> Self:
 
     @property
     def target(self) -> str:
+        """The target of the STORE instruction."""
         return super().destination
 
     @target.setter
@@ -1448,6 +1451,7 @@ def target(self, target: str) -> None:
 
     @property
     def left(self) -> MemoryReference:
+        """The left hand side of the STORE instruction."""
         return MemoryReference._from_rs_memory_reference(super().offset)
 
     @left.setter
@@ -1456,6 +1460,7 @@ def left(self, left: MemoryReference) -> None:
 
     @property
     def right(self) -> Union[MemoryReference, int, float]:
+        """The left hand side of the STORE instruction."""
         return _to_py_arithmetic_operand(super().source)
 
     @right.setter
@@ -1463,6 +1468,7 @@ def right(self, right: Union[MemoryReference, int, float]) -> None:
         quil_rs.Store.source.__set__(self, _to_rs_arithmetic_operand(right))  # type: ignore
 
     def out(self) -> str:
+        """Return a valid Quil string representation of the instruction."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1471,14 +1477,12 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ClassicalStore":
+    def __deepcopy__(self, memo: dict) -> "ClassicalStore":
         return ClassicalStore._from_rs_store(super().__deepcopy__(memo))
 
 
 class ClassicalComparison(quil_rs.Comparison, AbstractInstruction):
-    """
-    Abstract class for ternary comparison instructions.
-    """
+    """Base class for ternary comparison instructions."""
 
     op: ClassVar[quil_rs.ComparisonOperator]
 
@@ -1488,6 +1492,7 @@ def __new__(
         left: MemoryReference,
         right: Union[MemoryReference, int, float],
     ) -> "ClassicalComparison":
+        """Initialize a new comparison instruction."""
         operands = (target._to_rs_memory_reference(), left._to_rs_memory_reference(), cls._to_comparison_operand(right))
         return super().__new__(cls, cls.op, operands)
 
@@ -1516,6 +1521,7 @@ def _to_py_operand(operand: quil_rs.ComparisonOperand) -> Union[MemoryReference,
 
     @property
     def target(self) -> MemoryReference:
+        """The target of the comparison."""
         return MemoryReference._from_rs_memory_reference(super().operands[0])
 
     @target.setter
@@ -1526,6 +1532,7 @@ def target(self, target: MemoryReference) -> None:
 
     @property
     def left(self) -> MemoryReference:
+        """The left hand side of the comparison."""
         return MemoryReference._from_rs_memory_reference(super().operands[1])
 
     @left.setter
@@ -1536,6 +1543,7 @@ def left(self, left: MemoryReference) -> None:
 
     @property
     def right(self) -> Union[MemoryReference, int, float]:
+        """The right hand side of the comparison."""
         return self._to_py_operand(super().operands[2])
 
     @right.setter
@@ -1545,6 +1553,7 @@ def right(self, right: MemoryReference) -> None:
         quil_rs.Comparison.operands.__set__(self, tuple(operands))  # type: ignore
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1553,58 +1562,47 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ClassicalComparison":
+    def __deepcopy__(self, memo: dict) -> "ClassicalComparison":
         copy = ClassicalComparison._from_rs_comparison(super().__deepcopy__(memo))
         copy.__class__ = self.__class__
         return copy
 
 
 class ClassicalEqual(ClassicalComparison):
-    """
-    The EQ comparison instruction.
-    """
+    """The EQ comparison instruction."""
 
     op = quil_rs.ComparisonOperator.Equal
 
 
 class ClassicalLessThan(ClassicalComparison):
-    """
-    The LT comparison instruction.
-    """
+    """The LT comparison instruction."""
 
     op = quil_rs.ComparisonOperator.LessThan
 
 
 class ClassicalLessEqual(ClassicalComparison):
-    """
-    The LE comparison instruction.
-    """
+    """The LE comparison instruction."""
 
     op = quil_rs.ComparisonOperator.LessThanOrEqual
 
 
 class ClassicalGreaterThan(ClassicalComparison):
-    """
-    The GT comparison instruction.
-    """
+    """The GT comparison instruction."""
 
     op = quil_rs.ComparisonOperator.GreaterThan
 
 
 class ClassicalGreaterEqual(ClassicalComparison):
-    """
-    The GE comparison instruction.
-    """
+    """The GE comparison instruction."""
 
     op = quil_rs.ComparisonOperator.GreaterThanOrEqual
 
 
 class Jump(quil_rs.Jump, AbstractInstruction):
-    """
-    Representation of an unconditional jump instruction (JUMP).
-    """
+    """Representation of an unconditional jump instruction (JUMP)."""
 
     def __new__(cls, target: Union[Label, LabelPlaceholder]) -> Self:
+        """Initialize a new jump instruction."""
         return super().__new__(cls, target.target)
 
     @classmethod
@@ -1613,6 +1611,7 @@ def _from_rs_jump(cls, jump: quil_rs.Jump) -> Self:
 
     @property  # type: ignore[override]
     def target(self) -> Union[Label, LabelPlaceholder]:
+        """Get the target of the jump."""
         if super().target.is_placeholder():
             return LabelPlaceholder._from_rs_target(super().target)
         return Label._from_rs_target(super().target)
@@ -1622,6 +1621,7 @@ def target(self, target: Union[Label, LabelPlaceholder]) -> None:
         quil_rs.Jump.target.__set__(self, target.target)  # type: ignore[attr-defined]
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string. Raises an error if the target is an unresolved placeholder."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1630,13 +1630,12 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Jump":
+    def __deepcopy__(self, memo: dict) -> "Jump":
         return Jump._from_rs_jump(super().__deepcopy__(memo))
 
 
 class Pragma(quil_rs.Pragma, AbstractInstruction):
-    """
-    A PRAGMA instruction.
+    """A PRAGMA instruction.
 
     This is printed in QUIL as:
 
@@ -1650,6 +1649,7 @@ def __new__(
         args: Sequence[Union[Qubit, FormalArgument, int, str]] = (),
         freeform_string: str = "",
     ) -> Self:
+        """Initialize a new PRAGMA instruction."""
         data = freeform_string or None
         return super().__new__(cls, command, Pragma._to_pragma_arguments(args), data)
 
@@ -1658,7 +1658,7 @@ def _from_rs_pragma(cls, pragma: quil_rs.Pragma) -> "Pragma":
         return super().__new__(cls, pragma.name, pragma.arguments, pragma.data)
 
     @staticmethod
-    def _to_pragma_arguments(args: Sequence[Union[QubitDesignator, str]]) -> List[quil_rs.PragmaArgument]:
+    def _to_pragma_arguments(args: Sequence[Union[QubitDesignator, str]]) -> list[quil_rs.PragmaArgument]:
         pragma_arguments = []
         for arg in args:
             if isinstance(arg, Qubit):
@@ -1672,8 +1672,8 @@ def _to_pragma_arguments(args: Sequence[Union[QubitDesignator, str]]) -> List[qu
         return pragma_arguments
 
     @staticmethod
-    def _to_py_arguments(args: List[quil_rs.PragmaArgument]) -> List[QubitDesignator]:
-        arguments: List[QubitDesignator] = []
+    def _to_py_arguments(args: list[quil_rs.PragmaArgument]) -> list[QubitDesignator]:
+        arguments: list[QubitDesignator] = []
         for arg in args:
             if arg.is_integer():
                 arguments.append(Qubit(arg.to_integer()))
@@ -1682,6 +1682,7 @@ def _to_py_arguments(args: List[quil_rs.PragmaArgument]) -> List[QubitDesignator
         return arguments
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1689,6 +1690,7 @@ def __str__(self) -> str:
 
     @property
     def command(self) -> str:
+        """Get the pragma command identifier."""
         return super().name
 
     @command.setter
@@ -1696,7 +1698,8 @@ def command(self, command: str) -> None:
         quil_rs.Pragma.name.__set__(self, command)  # type: ignore[attr-defined]
 
     @property
-    def args(self) -> Tuple[QubitDesignator]:
+    def args(self) -> tuple[QubitDesignator]:
+        """Get the arguments of the PRAGMA command."""
         return tuple(Pragma._to_py_arguments(super().arguments))  # type: ignore[return-value]
 
     @args.setter
@@ -1705,6 +1708,7 @@ def args(self, args: Sequence[Union[QubitDesignator, str]]) -> None:
 
     @property
     def freeform_string(self) -> str:
+        """Get the PRAGMA's freeform string."""
         return super().data or ""
 
     @freeform_string.setter
@@ -1714,18 +1718,16 @@ def freeform_string(self, freeform_string: str) -> None:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Pragma":
+    def __deepcopy__(self, memo: dict) -> "Pragma":
         return Pragma._from_rs_pragma(super().__deepcopy__(memo))
 
 
 class Declare(quil_rs.Declaration, AbstractInstruction):
-    """
-    A DECLARE directive.
+    """A DECLARE directive.
 
     This is printed in Quil as::
 
         DECLARE <name> <memory-type> (SHARING <other-name> (OFFSET <amount> <type>)* )?
-
     """
 
     def __new__(
@@ -1734,8 +1736,9 @@ def __new__(
         memory_type: str,
         memory_size: int = 1,
         shared_region: Optional[str] = None,
-        offsets: Optional[Sequence[Tuple[int, str]]] = None,
+        offsets: Optional[Sequence[tuple[int, str]]] = None,
     ) -> Self:
+        """Initialize a new DECLARE directive."""
         vector = quil_rs.Vector(Declare._memory_type_to_scalar_type(memory_type), memory_size)
         sharing = None
         if shared_region is not None:
@@ -1760,7 +1763,7 @@ def _memory_type_to_scalar_type(memory_type: str) -> quil_rs.ScalarType:
         raise ValueError(f"{memory_type} is not a valid scalar type.")
 
     @staticmethod
-    def _to_rs_offsets(offsets: Optional[Sequence[Tuple[int, str]]]) -> List[quil_rs.Offset]:
+    def _to_rs_offsets(offsets: Optional[Sequence[tuple[int, str]]]) -> list[quil_rs.Offset]:
         if offsets is None:
             return []
         return [
@@ -1769,6 +1772,7 @@ def _to_rs_offsets(offsets: Optional[Sequence[Tuple[int, str]]]) -> List[quil_rs
 
     @property
     def memory_type(self) -> str:
+        """Get the type of memory being declared."""
         return str(super().size.data_type).upper()
 
     @memory_type.setter
@@ -1779,6 +1783,7 @@ def memory_type(self, memory_type: str) -> None:
 
     @property
     def memory_size(self) -> int:
+        """Get the number of elements being declared."""
         return super().size.length
 
     @memory_size.setter
@@ -1789,6 +1794,7 @@ def memory_size(self, memory_size: int) -> None:
 
     @property
     def shared_region(self) -> Optional[str]:
+        """Get the memory region this declaration is sharing with, if any."""
         sharing = super().sharing
         if sharing is None:
             return None
@@ -1803,21 +1809,23 @@ def shared_region(self, shared_region: Optional[str]) -> None:
         quil_rs.Declaration.sharing.__set__(self, sharing)  # type: ignore[attr-defined]
 
     @property
-    def offsets(self) -> List[Tuple[int, str]]:
+    def offsets(self) -> list[tuple[int, str]]:
+        """Get the offsets for this declaration."""
         sharing = super().sharing
         if sharing is None:
             return []
         return [(offset.offset, str(offset.data_type).upper()) for offset in sharing.offsets]
 
     @offsets.setter
-    def offsets(self, offsets: Optional[List[Tuple[int, str]]]) -> None:
+    def offsets(self, offsets: Optional[list[tuple[int, str]]]) -> None:
         sharing = super().sharing
         if sharing is None:
             raise ValueError("DECLARE without a shared region cannot use offsets")
         sharing.offsets = Declare._to_rs_offsets(offsets)
         quil_rs.Declaration.sharing.__set__(self, sharing)  # type: ignore[attr-defined]
 
-    def asdict(self) -> Dict[str, Union[Sequence[Tuple[int, str]], Optional[str], int]]:
+    def asdict(self) -> dict[str, Union[Sequence[tuple[int, str]], Optional[str], int]]:
+        """Get the DECLARE directive as a dictionary."""
         return {
             "name": self.name,
             "memory_type": self.memory_type,
@@ -1827,6 +1835,7 @@ def asdict(self) -> Dict[str, Union[Sequence[Tuple[int, str]], Optional[str], in
         }
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1835,12 +1844,15 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Declare":
+    def __deepcopy__(self, memo: dict) -> "Declare":
         return Declare._from_rs_declaration(super().__deepcopy__(memo))
 
 
 class Include(quil_rs.Include, AbstractInstruction):
+    """An INCLUDE directive."""
+
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     @classmethod
@@ -1853,12 +1865,15 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Include":
+    def __deepcopy__(self, memo: dict) -> "Include":
         return Include._from_rs_include(super().__deepcopy__(memo))
 
 
 class Pulse(quil_rs.Pulse, AbstractInstruction):
+    """A PULSE instruction."""
+
     def __new__(cls, frame: Frame, waveform: Waveform, nonblocking: bool = False) -> Self:
+        """Initialize a new Pulse instruction."""
         return super().__new__(cls, not nonblocking, frame, waveform)
 
     @classmethod
@@ -1866,6 +1881,7 @@ def _from_rs_pulse(cls, pulse: quil_rs.Pulse) -> "Pulse":
         return super().__new__(cls, pulse.blocking, pulse.frame, pulse.waveform)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1875,17 +1891,20 @@ def __str__(self) -> str:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubits the pulse operates on."""
         if indices:
             return self.get_qubit_indices()
         else:
             return set(_convert_to_py_qubits(super().frame.qubits))
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubits the pulse operates on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame.qubits}
 
     @property  # type: ignore[override]
     def frame(self) -> Frame:
+        """Get the frame the pulse operates on."""
         return Frame._from_rs_frame_identifier(super().frame)
 
     @frame.setter
@@ -1894,6 +1913,7 @@ def frame(self, frame: Frame) -> None:
 
     @property  # type: ignore[override]
     def waveform(self) -> Waveform:
+        """Get the waveform of the pulse."""
         return _convert_to_py_waveform(super().waveform)
 
     @waveform.setter
@@ -1902,6 +1922,7 @@ def waveform(self, waveform: Waveform) -> None:
 
     @property
     def nonblocking(self) -> bool:
+        """Return whether the pulse is non-blocking."""
         return not super().blocking
 
     @nonblocking.setter
@@ -1911,12 +1932,15 @@ def nonblocking(self, nonblocking: bool) -> None:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Pulse":
+    def __deepcopy__(self, memo: dict) -> "Pulse":
         return Pulse._from_rs_pulse(super().__deepcopy__(memo))
 
 
 class SetFrequency(quil_rs.SetFrequency, AbstractInstruction):
+    """A SET-FREQUENCY instruction."""
+
     def __new__(cls, frame: Frame, freq: ParameterDesignator) -> Self:
+        """Initialize a new SET-FREQUENCY instruction."""
         return super().__new__(cls, frame, _convert_to_rs_expression(freq))
 
     @classmethod
@@ -1924,6 +1948,7 @@ def _from_rs_set_frequency(cls, set_frequency: quil_rs.SetFrequency) -> "SetFreq
         return super().__new__(cls, set_frequency.frame, set_frequency.frequency)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1931,6 +1956,7 @@ def __str__(self) -> str:
 
     @property  # type: ignore[override]
     def frame(self) -> Frame:
+        """Get the frame the frequency is set on."""
         return Frame._from_rs_frame_identifier(super().frame)
 
     @frame.setter
@@ -1939,6 +1965,7 @@ def frame(self, frame: Frame) -> None:
 
     @property
     def freq(self) -> ParameterDesignator:
+        """Get the frequency that is set."""
         return _convert_to_py_expression(super().frequency)
 
     @freq.setter
@@ -1949,23 +1976,28 @@ def freq(self, freq: ParameterDesignator) -> None:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubits the frequency is set on."""
         if indices:
             return self.get_qubit_indices()
         return set(self.frame.qubits)
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubits the frequency is set on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame.qubits}
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "SetFrequency":
+    def __deepcopy__(self, memo: dict) -> "SetFrequency":
         return SetFrequency._from_rs_set_frequency(super().__deepcopy__(memo))
 
 
 class ShiftFrequency(quil_rs.ShiftFrequency, AbstractInstruction):
+    """The SHIFT-FREQUENCY instruction."""
+
     def __new__(cls, frame: Frame, freq: ParameterDesignator) -> Self:
+        """Initialize a new SHIFT-FREQUENCY instruction."""
         return super().__new__(cls, frame, _convert_to_rs_expression(freq))
 
     @classmethod
@@ -1973,6 +2005,7 @@ def _from_rs_shift_frequency(cls, shift_frequency: quil_rs.ShiftFrequency) -> "S
         return super().__new__(cls, shift_frequency.frame, shift_frequency.frequency)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -1980,6 +2013,7 @@ def __str__(self) -> str:
 
     @property  # type: ignore[override]
     def frame(self) -> Frame:
+        """Get the frame the frequency is shifted on."""
         return Frame._from_rs_frame_identifier(super().frame)
 
     @frame.setter
@@ -1988,6 +2022,7 @@ def frame(self, frame: Frame) -> None:
 
     @property
     def freq(self) -> ParameterDesignator:
+        """Get the parameter that defines how the frequency is shifted."""
         return _convert_to_py_expression(super().frequency)
 
     @freq.setter
@@ -1998,23 +2033,28 @@ def freq(self, freq: ParameterDesignator) -> None:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubits the frequency is shifted on."""
         if indices:
             return self.get_qubit_indices()
         return set(self.frame.qubits)
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubits the frequency is shifted on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame.qubits}
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ShiftFrequency":
+    def __deepcopy__(self, memo: dict) -> "ShiftFrequency":
         return ShiftFrequency._from_rs_shift_frequency(super().__deepcopy__(memo))
 
 
 class SetPhase(quil_rs.SetPhase, AbstractInstruction):
+    """The SET-PHASE instruction."""
+
     def __new__(cls, frame: Frame, phase: ParameterDesignator) -> Self:
+        """Initialize a new SET-PHASE instruction."""
         return super().__new__(cls, frame, _convert_to_rs_expression(phase))
 
     @classmethod
@@ -2022,6 +2062,7 @@ def _from_rs_set_phase(cls, set_phase: quil_rs.SetPhase) -> "SetPhase":
         return super().__new__(cls, set_phase.frame, set_phase.phase)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2029,6 +2070,7 @@ def __str__(self) -> str:
 
     @property  # type: ignore[override]
     def frame(self) -> Frame:
+        """Get the frame the phase is set on."""
         return Frame._from_rs_frame_identifier(super().frame)
 
     @frame.setter
@@ -2037,6 +2079,7 @@ def frame(self, frame: Frame) -> None:
 
     @property  # type: ignore[override]
     def phase(self) -> ParameterDesignator:
+        """Get the phase this instruction sets."""
         return _convert_to_py_expression(super().phase)
 
     @phase.setter
@@ -2047,23 +2090,28 @@ def phase(self, phase: ParameterDesignator) -> None:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the quibts the phase is set on."""
         if indices:
             return self.get_qubit_indices()
         return set(self.frame.qubits)
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the quibts the phase is set on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame.qubits}
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "SetPhase":
+    def __deepcopy__(self, memo: dict) -> "SetPhase":
         return SetPhase._from_rs_set_phase(super().__deepcopy__(memo))
 
 
 class ShiftPhase(quil_rs.ShiftPhase, AbstractInstruction):
+    """The SHIFT-PHASE instruction."""
+
     def __new__(cls, frame: Frame, phase: ParameterDesignator) -> Self:
+        """Initialize a new SHIFT-PHASE instruction."""
         return super().__new__(cls, frame, _convert_to_rs_expression(phase))
 
     @classmethod
@@ -2071,6 +2119,7 @@ def _from_rs_shift_phase(cls, shift_phase: quil_rs.ShiftPhase) -> "ShiftPhase":
         return super().__new__(cls, shift_phase.frame, shift_phase.phase)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2078,6 +2127,7 @@ def __str__(self) -> str:
 
     @property  # type: ignore[override]
     def frame(self) -> Frame:
+        """Get the frame the phase is shifted on."""
         return Frame._from_rs_frame_identifier(super().frame)
 
     @frame.setter
@@ -2086,6 +2136,7 @@ def frame(self, frame: Frame) -> None:
 
     @property  # type: ignore[override]
     def phase(self) -> ParameterDesignator:
+        """Get the parameter that defines how the phase is shifted."""
         return _convert_to_py_expression(super().phase)
 
     @phase.setter
@@ -2096,23 +2147,28 @@ def phase(self, phase: ParameterDesignator) -> None:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubits the phase is shifted on."""
         if indices:
             return self.get_qubit_indices()
         return set(self.frame.qubits)
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubits the phase is shifted on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame.qubits}
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "ShiftPhase":
+    def __deepcopy__(self, memo: dict) -> "ShiftPhase":
         return ShiftPhase._from_rs_shift_phase(super().__deepcopy__(memo))
 
 
 class SwapPhases(quil_rs.SwapPhases, AbstractInstruction):
+    """The SWAP-PHASES instruction."""
+
     def __new__(cls, frameA: Frame, frameB: Frame) -> Self:
+        """Initialize a new SWAP-PHASES instruction."""
         return super().__new__(cls, frameA, frameB)
 
     @classmethod
@@ -2121,6 +2177,7 @@ def _from_rs_swap_phases(cls, swap_phases: quil_rs.SwapPhases) -> "SwapPhases":
 
     @property
     def frameA(self) -> Frame:
+        """The first frame of the SWAP-PHASES instruction."""
         return Frame._from_rs_frame_identifier(super().frame_1)
 
     @frameA.setter
@@ -2129,6 +2186,7 @@ def frameA(self, frame: Frame) -> None:
 
     @property
     def frameB(self) -> Frame:
+        """The second frame of the SWAP-PHASES instruction."""
         return Frame._from_rs_frame_identifier(super().frame_2)
 
     @frameB.setter
@@ -2136,6 +2194,7 @@ def frameB(self, frame: Frame) -> None:
         quil_rs.SwapPhases.frame_2.__set__(self, frame)  # type: ignore[attr-defined]
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2145,23 +2204,28 @@ def __str__(self) -> str:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubits the swap-phases instruction operates on."""
         if indices:
             return self.get_qubit_indices()
         return set(self.frameA.qubits) | set(self.frameB.qubits)
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubits the swap-phases instruction operates on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame_1.qubits + super().frame_2.qubits}
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "SwapPhases":
+    def __deepcopy__(self, memo: dict) -> "SwapPhases":
         return SwapPhases._from_rs_swap_phases(super().__deepcopy__(memo))
 
 
 class SetScale(quil_rs.SetScale, AbstractInstruction):
+    """The SET-SCALE instruction."""
+
     def __new__(cls, frame: Frame, scale: ParameterDesignator) -> Self:
+        """Initialize a new SET-SCALE instruction."""
         return super().__new__(cls, frame, _convert_to_rs_expression(scale))
 
     @classmethod
@@ -2169,6 +2233,7 @@ def _from_rs_set_scale(cls, set_scale: quil_rs.SetScale) -> "SetScale":
         return super().__new__(cls, set_scale.frame, set_scale.scale)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2176,6 +2241,7 @@ def __str__(self) -> str:
 
     @property  # type: ignore[override]
     def frame(self) -> Frame:
+        """Get the frame the scale is set on."""
         return Frame._from_rs_frame_identifier(super().frame)
 
     @frame.setter
@@ -2184,6 +2250,7 @@ def frame(self, frame: Frame) -> None:
 
     @property  # type: ignore[override]
     def scale(self) -> ParameterDesignator:
+        """Get the scale that is set."""
         return _convert_to_py_expression(super().scale)
 
     @scale.setter
@@ -2194,22 +2261,26 @@ def scale(self, scale: ParameterDesignator) -> None:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubits the scale is set on."""
         if indices:
             return self.get_qubit_indices()
         return set(self.frame.qubits)
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubits the scale is set on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame.qubits}
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "SetScale":
+    def __deepcopy__(self, memo: dict) -> "SetScale":
         return SetScale._from_rs_set_scale(super().__deepcopy__(memo))
 
 
 class Capture(quil_rs.Capture, AbstractInstruction):
+    """The CAPTURE instruction."""
+
     def __new__(
         cls,
         frame: Frame,
@@ -2217,6 +2288,7 @@ def __new__(
         memory_region: MemoryReference,
         nonblocking: bool = False,
     ) -> Self:
+        """Initialize a new CAPTURE instruction."""
         rs_memory_reference = _convert_to_rs_expression(memory_region).to_address()
         return super().__new__(cls, not nonblocking, frame, rs_memory_reference, kernel)
 
@@ -2226,6 +2298,7 @@ def _from_rs_capture(cls, capture: quil_rs.Capture) -> "Capture":
 
     @property  # type: ignore[override]
     def frame(self) -> Frame:
+        """Get the frame this instruction captures."""
         return Frame._from_rs_frame_identifier(super().frame)
 
     @frame.setter
@@ -2234,6 +2307,7 @@ def frame(self, frame: Frame) -> None:
 
     @property
     def kernel(self) -> Waveform:
+        """Get the kernel waveform."""
         return _convert_to_py_waveform(super().waveform)
 
     @kernel.setter
@@ -2242,6 +2316,7 @@ def kernel(self, kernel: Waveform) -> None:
 
     @property
     def memory_region(self) -> MemoryReference:
+        """Get the memory reference that the capture is written to."""
         return MemoryReference._from_rs_memory_reference(super().memory_reference)
 
     @memory_region.setter
@@ -2251,6 +2326,7 @@ def memory_region(self, memory_region: MemoryReference) -> None:
 
     @property
     def nonblocking(self) -> bool:
+        """Whether the capture is non-blocking."""
         return not super().blocking
 
     @nonblocking.setter
@@ -2258,6 +2334,7 @@ def nonblocking(self, nonblocking: bool) -> None:
         quil_rs.Capture.blocking.__set__(self, not nonblocking)  # type: ignore[attr-defined]
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2267,23 +2344,27 @@ def __str__(self) -> str:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubits the capture instruction operates on."""
         if indices:
             return self.get_qubit_indices()
         else:
             return set(_convert_to_py_qubits(super().frame.qubits))
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubits the capture instruction operates on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame.qubits}
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Capture":
+    def __deepcopy__(self, memo: dict) -> "Capture":
         return Capture._from_rs_capture(super().__deepcopy__(memo))
 
 
 class RawCapture(quil_rs.RawCapture, AbstractInstruction):
+    """The RAW-CAPTURE instruction."""
+
     def __new__(
         cls,
         frame: Frame,
@@ -2291,6 +2372,7 @@ def __new__(
         memory_region: MemoryReference,
         nonblocking: bool = False,
     ) -> Self:
+        """Initialize a new RAW-CAPTURE instruction."""
         rs_duration = _convert_to_rs_expression(duration)
         rs_memory_reference = _convert_to_rs_expression(memory_region).to_address()
         return super().__new__(cls, not nonblocking, frame, rs_duration, rs_memory_reference)
@@ -2303,6 +2385,7 @@ def _from_rs_raw_capture(cls, raw_capture: quil_rs.RawCapture) -> "RawCapture":
 
     @property  # type: ignore[override]
     def frame(self) -> Frame:
+        """Get the frame this instruction operates on."""
         return Frame._from_rs_frame_identifier(super().frame)
 
     @frame.setter
@@ -2311,6 +2394,7 @@ def frame(self, frame: Frame) -> None:
 
     @property  # type: ignore[override]
     def duration(self) -> complex:
+        """Get the duration of the capture."""
         return super().duration.to_number()
 
     @duration.setter
@@ -2320,6 +2404,7 @@ def duration(self, duration: float) -> None:
 
     @property
     def memory_region(self) -> MemoryReference:
+        """Get the memory region that the capture is written to."""
         return MemoryReference._from_rs_memory_reference(super().memory_reference)
 
     @memory_region.setter
@@ -2329,6 +2414,7 @@ def memory_region(self, memory_region: MemoryReference) -> None:
 
     @property
     def nonblocking(self) -> bool:
+        """Whether the capture is non-blocking."""
         return not super().blocking
 
     @nonblocking.setter
@@ -2336,6 +2422,7 @@ def nonblocking(self, nonblocking: bool) -> None:
         quil_rs.RawCapture.blocking.__set__(self, not nonblocking)  # type: ignore[attr-defined]
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2345,24 +2432,29 @@ def __str__(self) -> str:
         version="4.0",
         reason="The indices flag will be removed, use get_qubit_indices() instead.",
     )
-    def get_qubits(self, indices: bool = True) -> Union[Set[QubitDesignator], Set[int]]:
+    def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[int]]:
+        """Get the qubits the raw-capture instruction operates on."""
         if indices:
             return self.get_qubit_indices()
         else:
             return set(_convert_to_py_qubits(super().frame.qubits))
 
-    def get_qubit_indices(self) -> Set[int]:
+    def get_qubit_indices(self) -> set[int]:
+        """Get the qubits the raw-capture instruction operates on, as integer indices."""
         return {qubit.to_fixed() for qubit in super().frame.qubits}
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "RawCapture":
+    def __deepcopy__(self, memo: dict) -> "RawCapture":
         return RawCapture._from_rs_raw_capture(super().__deepcopy__(memo))
 
 
 class Delay(quil_rs.Delay, AbstractInstruction):
-    def __new__(cls, frames: List[Frame], qubits: Sequence[Union[int, Qubit, FormalArgument]], duration: float) -> Self:
+    """The DELAY instruction."""
+
+    def __new__(cls, frames: list[Frame], qubits: Sequence[Union[int, Qubit, FormalArgument]], duration: float) -> Self:
+        """Initialize a new DELAY instruction."""
         frame_names = [frame.name for frame in frames]
         rs_qubits = _convert_to_rs_qubits(Delay._join_frame_qubits(frames, list(qubits)))
         expression = quil_rs_expr.Expression.from_number(complex(duration))
@@ -2375,20 +2467,22 @@ def _from_rs_delay(cls, delay: quil_rs.Delay) -> "Delay":
     @staticmethod
     def _join_frame_qubits(
         frames: Sequence[Frame], qubits: Sequence[Union[int, Qubit, FormalArgument]]
-    ) -> List[Union[int, Qubit, FormalArgument]]:
+    ) -> list[Union[int, Qubit, FormalArgument]]:
         merged_qubits = set(qubits)
         for frame in frames:
             merged_qubits.update(frame.qubits)  # type: ignore
         return list(merged_qubits)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
         return super().to_quil_or_debug()
 
     @property  # type: ignore[override]
-    def qubits(self) -> List[QubitDesignator]:
+    def qubits(self) -> list[QubitDesignator]:
+        """Get the qubits the delay operates on."""
         return _convert_to_py_qubits(super().qubits)
 
     @qubits.setter
@@ -2396,11 +2490,12 @@ def qubits(self, qubits: Sequence[Union[int, Qubit, FormalArgument]]) -> None:
         quil_rs.Delay.qubits.__set__(self, _convert_to_rs_qubits(qubits))  # type: ignore
 
     @property
-    def frames(self) -> List[Frame]:
+    def frames(self) -> list[Frame]:
+        """Get the frames the delay operates on."""
         return [Frame(self.qubits, name) for name in super().frame_names]
 
     @frames.setter
-    def frames(self, frames: List[Frame]) -> None:
+    def frames(self, frames: list[Frame]) -> None:
         new_qubits = Delay._join_frame_qubits(frames, [])
         frame_names = [frame.name for frame in frames]
         quil_rs.Delay.qubits.__set__(self, _convert_to_rs_qubits(new_qubits))  # type: ignore[attr-defined]
@@ -2408,6 +2503,7 @@ def frames(self, frames: List[Frame]) -> None:
 
     @property  # type: ignore[override]
     def duration(self) -> float:
+        """Get the duration of the delay."""
         return super().duration.to_real()
 
     @duration.setter
@@ -2418,14 +2514,17 @@ def duration(self, duration: float) -> None:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Delay":
+    def __deepcopy__(self, memo: dict) -> "Delay":
         copy = Delay._from_rs_delay(super().__deepcopy__(memo))
         copy.__class__ = self.__class__
         return copy
 
 
 class DelayFrames(Delay):
-    def __new__(cls, frames: List[Frame], duration: float) -> Self:
+    """A DELAY instruction that operates on frames."""
+
+    def __new__(cls, frames: list[Frame], duration: float) -> Self:
+        """Initialize a new DELAY instruction that operates on frames."""
         return super().__new__(cls, frames, [], duration)
 
     @classmethod
@@ -2434,7 +2533,10 @@ def _from_rs_delay(cls, delay: quil_rs.Delay) -> "DelayFrames":
 
 
 class DelayQubits(Delay):
+    """Initialize a new DELAY instruction that operates on qubits."""
+
     def __new__(cls, qubits: Sequence[Union[Qubit, FormalArgument]], duration: float) -> Self:
+        """Initialize a new DELAY instruction that operates on qubits."""
         return super().__new__(cls, [], qubits, duration)
 
     @classmethod
@@ -2443,7 +2545,10 @@ def _from_rs_delay(cls, delay: quil_rs.Delay) -> "DelayQubits":
 
 
 class Fence(quil_rs.Fence, AbstractInstruction):
-    def __new__(cls, qubits: List[Union[Qubit, FormalArgument]]) -> Self:
+    """The FENCE instruction."""
+
+    def __new__(cls, qubits: list[Union[Qubit, FormalArgument]]) -> Self:
+        """Initialize a new FENCE instruction."""
         return super().__new__(cls, _convert_to_rs_qubits(qubits))
 
     @classmethod
@@ -2451,42 +2556,49 @@ def _from_rs_fence(cls, fence: quil_rs.Fence) -> "Fence":
         return super().__new__(cls, fence.qubits)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
         return super().to_quil_or_debug()
 
     @property  # type: ignore[override]
-    def qubits(self) -> List[QubitDesignator]:
+    def qubits(self) -> list[QubitDesignator]:
+        """Get the qubits the fence operates on.
+
+        Note: If qubits is empty, then the instruction fences all qubits.
+        """
         return _convert_to_py_qubits(super().qubits)
 
     @qubits.setter
-    def qubits(self, qubits: List[Union[Qubit, FormalArgument]]) -> None:
+    def qubits(self, qubits: list[Union[Qubit, FormalArgument]]) -> None:
         quil_rs.Fence.qubits.__set__(self, _convert_to_rs_qubits(qubits))  # type: ignore[attr-defined]
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "Fence":
+    def __deepcopy__(self, memo: dict) -> "Fence":
         return Fence._from_rs_fence(super().__deepcopy__(memo))
 
 
 class FenceAll(Fence):
-    """
-    The FENCE instruction.
-    """
+    """A FENCE instruction that operates on all qubits."""
 
     def __new__(cls) -> Self:
+        """Initialize a new FenceAll instruction."""
         return super().__new__(cls, [])
 
 
 class DefWaveform(quil_rs.WaveformDefinition, AbstractInstruction):
+    """A waveform definition."""
+
     def __new__(
         cls,
         name: str,
-        parameters: List[Parameter],
-        entries: List[Union[complex, Expression]],
+        parameters: list[Parameter],
+        entries: list[Union[complex, Expression]],
     ) -> Self:
+        """Initialize a new waveform definition."""
         rs_waveform = DefWaveform._build_rs_waveform(parameters, entries)
         return super().__new__(cls, name, rs_waveform)
 
@@ -2495,32 +2607,35 @@ def _from_rs_waveform_definition(cls, waveform_definition: quil_rs.WaveformDefin
         return super().__new__(cls, waveform_definition.name, waveform_definition.definition)
 
     @staticmethod
-    def _build_rs_waveform(parameters: List[Parameter], entries: List[Union[complex, Expression]]) -> quil_rs.Waveform:
+    def _build_rs_waveform(parameters: list[Parameter], entries: list[Union[complex, Expression]]) -> quil_rs.Waveform:
         rs_parameters = [parameter.name for parameter in parameters]
         return quil_rs.Waveform(_convert_to_rs_expressions(entries), rs_parameters)
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
         return super().to_quil_or_debug()
 
     @property
-    def parameters(self) -> List[Parameter]:
+    def parameters(self) -> list[Parameter]:
+        """Get the parameters of the waveform."""
         return [Parameter(parameter) for parameter in super().definition.parameters]
 
     @parameters.setter
-    def parameters(self, parameters: List[Parameter]) -> None:
+    def parameters(self, parameters: list[Parameter]) -> None:
         waveform = super().definition
         waveform.parameters = [parameter.name for parameter in parameters]
         quil_rs.WaveformDefinition.definition.__set__(self, waveform)  # type: ignore[attr-defined]
 
     @property
     def entries(self) -> Sequence[ParameterDesignator]:
+        """Get the entries in the waveform definition."""
         return _convert_to_py_expressions(super().definition.matrix)
 
     @entries.setter
-    def entries(self, entries: List[Union[complex, Expression]]) -> None:
+    def entries(self, entries: list[Union[complex, Expression]]) -> None:
         waveform = super().definition
         waveform.matrix = _convert_to_rs_expressions(entries)
         quil_rs.WaveformDefinition.definition.__set__(self, waveform)  # type: ignore[attr-defined]
@@ -2528,18 +2643,21 @@ def entries(self, entries: List[Union[complex, Expression]]) -> None:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "DefWaveform":
+    def __deepcopy__(self, memo: dict) -> "DefWaveform":
         return DefWaveform._from_rs_waveform_definition(super().__deepcopy__(memo))
 
 
 class DefCircuit(quil_rs.CircuitDefinition, AbstractInstruction):
+    """A circuit definition."""
+
     def __new__(
         cls,
         name: str,
-        parameters: List[Parameter],
-        qubits: List[FormalArgument],
-        instructions: List[AbstractInstruction],
+        parameters: list[Parameter],
+        qubits: list[FormalArgument],
+        instructions: list[AbstractInstruction],
     ) -> Self:
+        """Initialize a new circuit definition."""
         rs_parameters = [parameter.name for parameter in parameters]
         rs_qubits = [qubit.name for qubit in qubits]
         rs_instructions = _convert_to_rs_instructions(instructions)
@@ -2556,54 +2674,61 @@ def _from_rs_circuit_definition(cls, circuit_definition: quil_rs.CircuitDefiniti
         )
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
         return super().to_quil_or_debug()
 
     @property  # type: ignore[override]
-    def parameters(self) -> List[Parameter]:
+    def parameters(self) -> list[Parameter]:
+        """Get the parameters of the circuit."""
         return [Parameter(parameter) for parameter in super().parameters]
 
     @parameters.setter
-    def parameters(self, parameters: List[Parameter]) -> None:
+    def parameters(self, parameters: list[Parameter]) -> None:
         rs_parameters = [parameter.name for parameter in parameters]
         quil_rs.CircuitDefinition.parameters.__set__(self, rs_parameters)  # type: ignore[attr-defined]
 
     @property  # type: ignore[override]
-    def qubit_variables(self) -> List[FormalArgument]:
+    def qubit_variables(self) -> list[FormalArgument]:
+        """Get the qubit parameters of the circuit."""
         return [FormalArgument(qubit) for qubit in super().qubit_variables]
 
     @qubit_variables.setter
-    def qubit_variables(self, qubits: List[FormalArgument]) -> None:
+    def qubit_variables(self, qubits: list[FormalArgument]) -> None:
         rs_qubits = [qubit.name for qubit in qubits]
         quil_rs.CircuitDefinition.qubit_variables.__set__(self, rs_qubits)  # type: ignore[attr-defined]
 
     @property  # type: ignore[override]
-    def instructions(self) -> List[AbstractInstruction]:
+    def instructions(self) -> list[AbstractInstruction]:
+        """Get the instructions in the circuit."""
         return _convert_to_py_instructions(super().instructions)
 
     @instructions.setter
-    def instructions(self, instructions: List[AbstractInstruction]) -> None:
+    def instructions(self, instructions: list[AbstractInstruction]) -> None:
         rs_instructions = _convert_to_rs_instructions(instructions)
         quil_rs.CircuitDefinition.instructions.__set__(self, rs_instructions)  # type: ignore[attr-defined]
 
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "DefCircuit":
+    def __deepcopy__(self, memo: dict) -> "DefCircuit":
         return DefCircuit._from_rs_circuit_definition(super().__deepcopy__(memo))
 
 
 class DefCalibration(quil_rs.Calibration, AbstractInstruction):
+    """A calibration definition."""
+
     def __new__(
         cls,
         name: str,
         parameters: Sequence[ParameterDesignator],
         qubits: Sequence[Union[Qubit, FormalArgument]],
         instrs: Sequence[AbstractInstruction],
-        modifiers: Optional[List[quil_rs.GateModifier]] = None,
+        modifiers: Optional[list[quil_rs.GateModifier]] = None,
     ) -> Self:
+        """Initialize a new calibration definition."""
         return super().__new__(
             cls,
             name,
@@ -2626,6 +2751,7 @@ def _from_rs_calibration(cls, calibration: quil_rs.Calibration) -> "DefCalibrati
 
     @property  # type: ignore[override]
     def parameters(self) -> Sequence[ParameterDesignator]:
+        """The parameters of the calibration."""
         return _convert_to_py_expressions(super().parameters)
 
     @parameters.setter
@@ -2633,7 +2759,8 @@ def parameters(self, parameters: Sequence[ParameterDesignator]) -> None:
         quil_rs.Calibration.parameters.__set__(self, _convert_to_rs_expressions(parameters))  # type: ignore[attr-defined] # noqa
 
     @property  # type: ignore[override]
-    def qubits(self) -> List[QubitDesignator]:
+    def qubits(self) -> list[QubitDesignator]:
+        """The qubits the calibration operates on."""
         return _convert_to_py_qubits(super().qubits)
 
     @qubits.setter
@@ -2641,14 +2768,25 @@ def qubits(self, qubits: Sequence[QubitDesignator]) -> None:
         quil_rs.Calibration.qubits.__set__(self, _convert_to_rs_qubits(qubits))  # type: ignore[attr-defined]
 
     @property
-    def instrs(self) -> List[AbstractInstruction]:
+    def instrs(self) -> list[AbstractInstruction]:
+        """The instructions in the calibration."""
         return _convert_to_py_instructions(super().instructions)
 
     @instrs.setter
     def instrs(self, instrs: Sequence[AbstractInstruction]) -> None:
         quil_rs.Calibration.instructions.__set__(self, _convert_to_rs_instructions(instrs))  # type: ignore[attr-defined] # noqa
 
+    @property  # type: ignore[override]
+    def instructions(self) -> list[AbstractInstruction]:
+        """The instructions in the calibration."""
+        return self.instrs
+
+    @instructions.setter
+    def instructions(self, instructions: list[AbstractInstruction]) -> None:
+        self.instrs = instructions
+
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2657,17 +2795,20 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "DefCalibration":
+    def __deepcopy__(self, memo: dict) -> "DefCalibration":
         return DefCalibration._from_rs_calibration(super().__deepcopy__(memo))
 
 
 class DefMeasureCalibration(quil_rs.MeasureCalibrationDefinition, AbstractInstruction):
+    """A measure calibration definition."""
+
     def __new__(
         cls,
         qubit: Optional[Union[Qubit, FormalArgument]],
         memory_reference: MemoryReference,
-        instrs: List[AbstractInstruction],
+        instrs: list[AbstractInstruction],
     ) -> Self:
+        """Initialize a new measure calibration definition."""
         rs_qubit = None if not qubit else _convert_to_rs_qubit(qubit)
         return super().__new__(
             cls,
@@ -2684,6 +2825,7 @@ def _from_rs_measure_calibration_definition(
 
     @property  # type: ignore[override]
     def qubit(self) -> Optional[QubitDesignator]:
+        """Get the qubit this calibration matches."""
         qubit = super().qubit
         if not qubit:
             return None
@@ -2695,6 +2837,7 @@ def qubit(self, qubit: QubitDesignator) -> None:
 
     @property
     def memory_reference(self) -> Optional[MemoryReference]:
+        """Get the memory reference this calibration writes to."""
         return MemoryReference._from_parameter_str(super().parameter)
 
     @memory_reference.setter
@@ -2702,14 +2845,16 @@ def memory_reference(self, memory_reference: MemoryReference) -> None:
         quil_rs.MeasureCalibrationDefinition.parameter.__set__(self, memory_reference.name)  # type: ignore[attr-defined] # noqa
 
     @property
-    def instrs(self) -> List[AbstractInstruction]:
+    def instrs(self) -> list[AbstractInstruction]:
+        """Get the instructions in the calibration."""
         return _convert_to_py_instructions(super().instructions)
 
     @instrs.setter
-    def instrs(self, instrs: List[AbstractInstruction]) -> None:
+    def instrs(self, instrs: list[AbstractInstruction]) -> None:
         quil_rs.MeasureCalibrationDefinition.instructions.__set__(self, _convert_to_rs_instructions(instrs))  # type: ignore[attr-defined] # noqa
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2718,11 +2863,13 @@ def __str__(self) -> str:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "DefMeasureCalibration":
+    def __deepcopy__(self, memo: dict) -> "DefMeasureCalibration":
         return DefMeasureCalibration._from_rs_measure_calibration_definition(super().__deepcopy__(memo))
 
 
 class DefFrame(quil_rs.FrameDefinition, AbstractInstruction):
+    """A frame definition."""
+
     def __new__(
         cls,
         frame: Frame,
@@ -2734,6 +2881,7 @@ def __new__(
         enable_raw_capture: Optional[str] = None,
         channel_delay: Optional[float] = None,
     ) -> Self:
+        """Get the frame definition."""
         attributes = {
             key: DefFrame._to_attribute_value(value)
             for key, value in zip(
@@ -2766,7 +2914,7 @@ def _from_rs_frame_definition(cls, def_frame: quil_rs.FrameDefinition) -> "DefFr
 
     @classmethod
     def _from_rs_attribute_values(
-        cls, frame: quil_rs.FrameIdentifier, attributes: Dict[str, quil_rs.AttributeValue]
+        cls, frame: quil_rs.FrameIdentifier, attributes: dict[str, quil_rs.AttributeValue]
     ) -> "DefFrame":
         return super().__new__(cls, frame, attributes)
 
@@ -2779,6 +2927,7 @@ def _to_attribute_value(value: Union[str, float]) -> quil_rs.AttributeValue:
         raise ValueError(f"{type(value)} is not a valid AttributeValue")
 
     def out(self) -> str:
+        """Return the instruction as a valid Quil string."""
         return super().to_quil()
 
     def __str__(self) -> str:
@@ -2786,6 +2935,7 @@ def __str__(self) -> str:
 
     @property
     def frame(self) -> Frame:
+        """Get the frame identifier."""
         return Frame._from_rs_frame_identifier(super().identifier)
 
     @frame.setter
@@ -2793,11 +2943,13 @@ def frame(self, frame: Frame) -> None:
         quil_rs.FrameDefinition.identifier.__set__(self, frame)  # type: ignore[attr-defined]
 
     def set_attribute(self, name: str, value: Union[str, float]) -> None:
+        """Set an attribute on the frame definition."""
         updated = super().attributes
         updated.update({name: DefFrame._to_attribute_value(value)})
         quil_rs.FrameDefinition.attributes.__set__(self, updated)  # type: ignore[attr-defined]
 
     def get_attribute(self, name: str) -> Optional[Union[str, float]]:
+        """Get an attribute's value on the frame definition."""
         value = super().attributes.get(name, None)
         if value is None:
             return None
@@ -2824,6 +2976,7 @@ def __setitem__(self, name: str, value: Union[str, float]) -> None:
         reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use get_attribute('DIRECTION') instead.",
     )
     def direction(self) -> Optional[str]:
+        """Get the DIRECTION attribute of the frame."""
         return self.get_attribute("DIRECTION")  # type: ignore
 
     @direction.setter
@@ -2840,6 +2993,7 @@ def direction(self, direction: str) -> None:
         reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use set_attribute('INITIAL-FREQUENCY') instead.",  # noqa: E501
     )
     def initial_frequency(self) -> Optional[float]:
+        """Get the INITIAL-FREQUENCY attribute of the frame."""
         return self.get_attribute("INITIAL-FREQUENCY")  # type: ignore
 
     @initial_frequency.setter
@@ -2856,6 +3010,7 @@ def initial_frequency(self, initial_frequency: float) -> None:
         reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use get_attribute('HARDWARE-OBJECT') instead.",
     )
     def hardware_object(self) -> Optional[str]:
+        """Get the HARDWARE-OBJECT attribute of the frame."""
         return self.get_attribute("HARDWARE-OBJECT")  # type: ignore
 
     @hardware_object.setter
@@ -2872,6 +3027,7 @@ def hardware_object(self, hardware_object: str) -> None:
         reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use get_attribute('SAMPLE-RATE') instead.",
     )
     def sample_rate(self) -> Frame:
+        """Get the SAMPLE-RATE attribute of the frame."""
         return self.get_attribute("SAMPLE-RATE")  # type: ignore
 
     @sample_rate.setter
@@ -2888,6 +3044,7 @@ def sample_rate(self, sample_rate: float) -> None:
         reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use get_attribute('CENTER-FREQUENCY') instead.",
     )
     def center_frequency(self) -> Frame:
+        """Get the CENTER-FREQUENCY attribute of the frame."""
         return self.get_attribute("CENTER-FREQUENCY")  # type: ignore
 
     @center_frequency.setter
@@ -2904,6 +3061,7 @@ def center_frequency(self, center_frequency: float) -> None:
         reason="Quil now supports generic key/value pairs in DEFFRAMEs. Use get_attribute('CHANNEL-DELAY') instead.",
     )
     def channel_delay(self) -> Frame:
+        """Get the CHANNEL-DELAY attribute of the frame."""
         return self.get_attribute("CHANNEL-DELAY")  # type: ignore
 
     @channel_delay.setter
@@ -2917,5 +3075,5 @@ def channel_delay(self, channel_delay: float) -> None:
     def __copy__(self) -> Self:
         return self
 
-    def __deepcopy__(self, memo: Dict) -> "DefFrame":
+    def __deepcopy__(self, memo: dict) -> "DefFrame":
         return DefFrame._from_rs_frame_definition(super().__deepcopy__(memo))
diff --git a/pyquil/quiltcalibrations.py b/pyquil/quiltcalibrations.py
index 70f50f74b..5a19b95bf 100644
--- a/pyquil/quiltcalibrations.py
+++ b/pyquil/quiltcalibrations.py
@@ -1,37 +1,46 @@
+"""A module containing utilities for working with Quil-T calibrations."""
+
+from collections.abc import Sequence
 from dataclasses import dataclass
-from typing import Union, Dict, Any, Optional, Sequence
+from typing import Any, Optional, Union
+
+import quil.expression as quil_expr
+import quil.instructions as quil_rs
+from quil.program import CalibrationSet
 
 from pyquil.quilatom import (
     ExpressionDesignator,
-    QubitDesignator,
     MemoryReference,
+    QubitDesignator,
     _convert_to_py_expression,
 )
 from pyquil.quilbase import (
     AbstractInstruction,
     DefCalibration,
     DefMeasureCalibration,
-    _convert_to_rs_instruction,
     _convert_to_py_qubit,
+    _convert_to_rs_instruction,
 )
 
-from quil.program import CalibrationSet
-import quil.instructions as quil_rs
-import quil.expression as quil_expr
-
 
 class CalibrationError(Exception):
+    """Base class for calibration errors."""
+
     pass
 
 
 class CalibrationDoesntMatch(CalibrationError):
+    """Raised when a calibration doesn't match an instruction."""
+
     pass
 
 
 @dataclass
 class CalibrationMatch:
+    """A match between a calibration definition and an instruction."""
+
     cal: Union[DefCalibration, DefMeasureCalibration]
-    settings: Dict[Union[QubitDesignator, ExpressionDesignator], Any]
+    settings: dict[Union[QubitDesignator, ExpressionDesignator], Any]
 
 
 def _convert_to_calibration_match(
@@ -55,7 +64,7 @@ def _convert_to_calibration_match(
     else:
         return None
 
-    settings: Dict[Union[QubitDesignator, ExpressionDesignator], Union[QubitDesignator, ExpressionDesignator]] = {
+    settings: dict[Union[QubitDesignator, ExpressionDesignator], Union[QubitDesignator, ExpressionDesignator]] = {
         _convert_to_py_qubit(param): _convert_to_py_qubit(qubit)
         for param, qubit in zip(parameter_qubits, target_qubits)
         if isinstance(param, MemoryReference) or param.is_variable()
@@ -87,9 +96,9 @@ def match_calibration(
         instruction = _convert_to_rs_instruction(instr)
         gate = instruction.to_gate()
         calibration_set = CalibrationSet([calibration.to_calibration_definition()], [])
-        matched_calibration: Optional[
-            Union[quil_rs.Calibration, quil_rs.MeasureCalibrationDefinition]
-        ] = calibration_set.get_match_for_gate(gate)
+        matched_calibration: Optional[Union[quil_rs.Calibration, quil_rs.MeasureCalibrationDefinition]] = (
+            calibration_set.get_match_for_gate(gate)
+        )
         return _convert_to_calibration_match(gate, matched_calibration)
 
     if calibration.is_measure_calibration_definition() and instruction.is_measurement():
diff --git a/pyquil/quiltwaveforms.py b/pyquil/quiltwaveforms.py
index dc388d362..834265032 100644
--- a/pyquil/quiltwaveforms.py
+++ b/pyquil/quiltwaveforms.py
@@ -1,20 +1,20 @@
+"""Waveform templates that are commonly useful when working with pulse programs."""
+
 from typing import Optional
-from typing_extensions import Self
 
 import numpy as np
 from scipy.special import erf
+from typing_extensions import Self
 
 from pyquil.quilatom import (
     TemplateWaveform,
-    _update_envelope,
     _template_waveform_property,
+    _update_envelope,
 )
 
 
 class FlatWaveform(TemplateWaveform):
-    """
-    A flat (constant) waveform.
-    """
+    """A flat (constant) waveform."""
 
     NAME = "flat"
 
@@ -26,6 +26,7 @@ def __new__(
         phase: Optional[float] = None,
         detuning: Optional[float] = None,
     ) -> Self:
+        """Initialize a new FlatWaveform."""
         return super().__new__(cls, cls.NAME, duration=duration, iq=iq, scale=scale, phase=phase, detuning=detuning)
 
     iq = _template_waveform_property("iq", doc="A raw IQ value.")
@@ -34,6 +35,7 @@ def __new__(
     detuning = _template_waveform_property("detuning", doc="An optional frequency detuning factor.", dtype=float)
 
     def samples(self, rate: float) -> np.ndarray:
+        """Get the samples of the waveform at a given sample rate."""
         iqs = np.full(self.num_samples(rate), self.iq, dtype=np.complex128)
         return _update_envelope(iqs, rate, scale=self.scale, phase=self.phase, detuning=self.detuning)
 
@@ -52,6 +54,7 @@ def __new__(
         phase: Optional[float] = None,
         detuning: Optional[float] = None,
     ) -> Self:
+        """Initialize a new GaussianWaveform."""
         return super().__new__(
             cls, cls.NAME, duration=duration, fwhm=fwhm, t0=t0, scale=scale, phase=phase, detuning=detuning
         )
@@ -67,6 +70,7 @@ def __new__(
     detuning = _template_waveform_property("detuning", doc="An optional frequency detuning factor.", dtype=float)
 
     def samples(self, rate: float) -> np.ndarray:
+        """Get the samples of the waveform at a given sample rate."""
         ts = np.arange(self.num_samples(rate), dtype=np.complex128) / rate
         sigma = 0.5 * self.fwhm / np.sqrt(2.0 * np.log(2.0))
         iqs = np.exp(-0.5 * (ts - self.t0) ** 2 / sigma**2)
@@ -89,6 +93,7 @@ def __new__(
         phase: Optional[float] = None,
         detuning: Optional[float] = None,
     ) -> Self:
+        """Initialize a new DragGaussianWaveform."""
         return super().__new__(
             cls,
             cls.NAME,
@@ -117,6 +122,7 @@ def __new__(
     detuning = _template_waveform_property("detuning", doc="An optional frequency detuning factor.", dtype=float)
 
     def samples(self, rate: float) -> np.ndarray:
+        """Get the samples of the waveform at a given sample rate."""
         ts = np.arange(self.num_samples(rate), dtype=np.complex128) / rate
         sigma = 0.5 * self.fwhm / np.sqrt(2.0 * np.log(2.0))
         env = np.exp(-0.5 * (ts - self.t0) ** 2 / sigma**2)
@@ -147,6 +153,7 @@ def __new__(
         phase: Optional[float] = None,
         detuning: Optional[float] = None,
     ) -> Self:
+        """Initialize a new HrmGaussianWaveform."""
         return super().__new__(
             cls,
             cls.NAME,
@@ -180,6 +187,7 @@ def __new__(
     detuning = _template_waveform_property("detuning", doc="An optional frequency detuning factor.", dtype=float)
 
     def samples(self, rate: float) -> np.ndarray:
+        """Get the samples of the waveform at a given sample rate."""
         ts = np.arange(self.num_samples(rate), dtype=np.complex128) / rate
         sigma = 0.5 * self.fwhm / np.sqrt(2.0 * np.log(2.0))
         exponent_of_t = 0.5 * (ts - self.t0) ** 2 / sigma**2
@@ -212,6 +220,7 @@ def __new__(
         phase: Optional[float] = None,
         detuning: Optional[float] = None,
     ) -> Self:
+        """Initialize a new ErfSquareWaveform."""
         return super().__new__(
             cls,
             cls.NAME,
@@ -243,6 +252,7 @@ def __new__(
     detuning = _template_waveform_property("detuning", doc="An optional frequency detuning factor.", dtype=float)
 
     def samples(self, rate: float) -> np.ndarray:
+        """Get the samples of the waveform at a given sample rate."""
         ts = np.arange(self.num_samples(rate), dtype=np.complex128) / rate
         fwhm = 0.5 * self.risetime
         t1 = fwhm
@@ -256,6 +266,7 @@ def samples(self, rate: float) -> np.ndarray:
 
 
 class BoxcarAveragerKernel(TemplateWaveform):
+    """A boxcar averaging kernel."""
 
     NAME = "boxcar_kernel"
 
@@ -266,6 +277,7 @@ def __new__(
         phase: Optional[float] = None,
         detuning: Optional[float] = None,
     ) -> Self:
+        """Initialize a new BoxcarAveragerKernel."""
         return super().__new__(cls, cls.NAME, duration=duration, scale=scale, phase=phase, detuning=detuning)
 
     scale = _template_waveform_property("scale", doc="An optional global scaling factor.", dtype=float)
@@ -275,6 +287,7 @@ def __new__(
     detuning = _template_waveform_property("detuning", doc="An optional frequency detuning factor.", dtype=float)
 
     def samples(self, rate: float) -> np.ndarray:
+        """Get the samples of the waveform at a given sample rate."""
         n = self.num_samples(rate)
         iqs = np.full(n, 1.0 / n, dtype=np.complex128)
         return _update_envelope(iqs, rate, scale=self.scale, phase=self.phase, detuning=self.detuning)
diff --git a/pyquil/simulation/__init__.py b/pyquil/simulation/__init__.py
index 088a03965..e3cb023e6 100644
--- a/pyquil/simulation/__init__.py
+++ b/pyquil/simulation/__init__.py
@@ -1,3 +1,5 @@
+"""Functions and classes for running simulations."""
+
 __all__ = [
     "get_measure_probabilities",
     "NumpyWavefunctionSimulator",
diff --git a/pyquil/simulation/_numpy.py b/pyquil/simulation/_numpy.py
index 6f28d922c..0bba502eb 100644
--- a/pyquil/simulation/_numpy.py
+++ b/pyquil/simulation/_numpy.py
@@ -13,12 +13,13 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from typing import Any, List, Optional, Sequence, Tuple, Union, cast
+from collections.abc import Sequence
+from typing import Any, Optional, Union, cast
 
 import numpy as np
 from numpy.random.mtrand import RandomState
 
-from pyquil.paulis import PauliTerm, PauliSum
+from pyquil.paulis import PauliSum, PauliTerm
 from pyquil.pyqvm import AbstractQuantumSimulator
 from pyquil.quilbase import Gate
 from pyquil.simulation.matrices import QUANTUM_GATES
@@ -43,7 +44,7 @@
 from pyquil.simulation.tools import all_bitstrings
 
 
-def targeted_einsum(gate: np.ndarray, wf: np.ndarray, wf_target_inds: List[int]) -> np.ndarray:
+def targeted_einsum(gate: np.ndarray, wf: np.ndarray, wf_target_inds: list[int]) -> np.ndarray:
     """Left-multiplies the given axes of the wf tensor by the given gate matrix.
 
     Note that the matrix must have a compatible tensor structure.
@@ -78,7 +79,7 @@ def targeted_einsum(gate: np.ndarray, wf: np.ndarray, wf_target_inds: List[int])
         output_indices[t] = w
 
     # TODO: `out` does not work if input matrices share memory with outputs, as is usually
-    # TODO: the case when propogating a wavefunction. This might be fixed in numpy 1.15
+    # TODO: the case when propagating a wavefunction. This might be fixed in numpy 1.15
     # https://github.com/numpy/numpy/pull/11286
     # It might be worth re-investigating memory savings with `out` when numpy 1.15 becomes
     # commonplace.
@@ -102,7 +103,8 @@ def targeted_tensordot(gate: np.ndarray, wf: np.ndarray, wf_target_inds: Sequenc
 
     # the indices we want to sum over are the final half
     gate_inds = np.arange(gate_n_qubits, 2 * gate_n_qubits)
-    assert len(wf_target_inds) == len(gate_inds), (wf_target_inds, gate_inds)
+    if len(wf_target_inds) != len(gate_inds):
+        raise ValueError(f"Length mismatch: wf_target_inds={wf_target_inds}, gate_inds={gate_inds}")
     wf = np.tensordot(gate, wf, (gate_inds, wf_target_inds))
 
     # tensordot dumps "output" indices into 0, 1, .. gate_n_qubits
@@ -128,8 +130,7 @@ def targeted_tensordot(gate: np.ndarray, wf: np.ndarray, wf_target_inds: Sequenc
 
 
 def get_measure_probabilities(wf: np.ndarray, qubit: int) -> np.ndarray:
-    """
-    Get the probabilities of measuring a qubit.
+    """Get the probabilities of measuring a qubit.
 
     :param wf: The statevector with a dimension for each qubit
     :param qubit: The qubit to measure. We will sum over every axis except this one.
@@ -141,7 +142,7 @@ def get_measure_probabilities(wf: np.ndarray, qubit: int) -> np.ndarray:
     return np.einsum(np.conj(wf), all_inds, wf, all_inds, [int(qubit)])  # type: ignore
 
 
-def _get_gate_tensor_and_qubits(gate: Gate) -> Tuple[np.ndarray, List[int]]:
+def _get_gate_tensor_and_qubits(gate: Gate) -> tuple[np.ndarray, list[int]]:
     """Given a gate ``Instruction``, turn it into a matrix and extract qubit indices.
 
     :param gate: the instruction
@@ -164,7 +165,8 @@ def _term_expectation(wf: np.ndarray, term: PauliTerm) -> Any:
     # Computes <psi|XYZ..XXZ|psi>
     wf2 = wf
     for qubit_i, op_str in term._ops.items():
-        assert isinstance(qubit_i, int)
+        if not isinstance(qubit_i, int):
+            raise ValueError("Only PauliTerms with integer qubits are supported.")
         # Re-use QUANTUM_GATES since it has X, Y, Z
         op_mat = QUANTUM_GATES[op_str]
         wf2 = targeted_tensordot(gate=op_mat, wf=wf2, wf_target_inds=[qubit_i])
@@ -176,8 +178,7 @@ def _term_expectation(wf: np.ndarray, term: PauliTerm) -> Any:
 
 class NumpyWavefunctionSimulator(AbstractQuantumSimulator):
     def __init__(self, n_qubits: int, rs: Optional[RandomState] = None):
-        """
-        A wavefunction simulator that uses numpy's tensordot or einsum to update a state vector
+        """Initialize a wavefunction simulator that uses numpy's tensordot or einsum to update a state vector.
 
         Please consider using
         :py:class:`PyQVM(..., quantum_simulator_type=NumpyWavefunctionSimulator)` rather
@@ -191,8 +192,6 @@ def __init__(self, n_qubits: int, rs: Optional[RandomState] = None):
         :param rs: a RandomState (should be shared with the owning :py:class:`PyQVM`) for
             doing anything stochastic. A value of ``None`` disallows doing anything stochastic.
         """
-        super().__init__(n_qubits=n_qubits, rs=rs)
-
         self.n_qubits = n_qubits
         self.rs = rs
 
@@ -200,8 +199,7 @@ def __init__(self, n_qubits: int, rs: Optional[RandomState] = None):
         self.wf[(0,) * n_qubits] = complex(1.0, 0)
 
     def sample_bitstrings(self, n_samples: int) -> np.ndarray:
-        """
-        Sample bitstrings from the distribution defined by the wavefunction.
+        """Sample bitstrings from the distribution defined by the wavefunction.
 
         Qubit 0 is at ``out[:, 0]``.
 
@@ -223,8 +221,7 @@ def sample_bitstrings(self, n_samples: int) -> np.ndarray:
         return possible_bitstrings[inds, :]
 
     def do_measurement(self, qubit: int) -> int:
-        """
-        Measure a qubit, collapse the wavefunction, and return the measurement result.
+        """Measure a qubit, collapse the wavefunction, and return the measurement result.
 
         :param qubit: Index of the qubit to measure.
         :return: measured bit
@@ -252,8 +249,7 @@ def do_measurement(self, qubit: int) -> int:
         return measured_bit
 
     def do_gate(self, gate: Gate) -> "NumpyWavefunctionSimulator":
-        """
-        Perform a gate.
+        """Perform a gate.
 
         :return: ``self`` to support method chaining.
         """
@@ -265,8 +261,7 @@ def do_gate(self, gate: Gate) -> "NumpyWavefunctionSimulator":
         return self
 
     def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "NumpyWavefunctionSimulator":
-        """
-        Apply an arbitrary unitary; not necessarily a named gate.
+        """Apply an arbitrary unitary; not necessarily a named gate.
 
         :param matrix: The unitary matrix to apply. No checks are done
         :param qubits: A list of qubits to apply the unitary to.
@@ -282,8 +277,7 @@ def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "NumpyWav
         return self
 
     def expectation(self, operator: Union[PauliTerm, PauliSum]) -> float:
-        """
-        Compute the expectation of an operator.
+        """Compute the expectation of an operator.
 
         :param operator: The operator
         :return: The operator's expectation value
@@ -294,8 +288,7 @@ def expectation(self, operator: Union[PauliTerm, PauliSum]) -> float:
         return sum(_term_expectation(self.wf, term) for term in operator)  # type: ignore
 
     def reset(self) -> "NumpyWavefunctionSimulator":
-        """
-        Reset the wavefunction to the ``|000...00>`` state.
+        """Reset the wavefunction to the ``|000...00>`` state.
 
         :return: ``self`` to support method chaining.
         """
@@ -303,5 +296,5 @@ def reset(self) -> "NumpyWavefunctionSimulator":
         self.wf[(0,) * self.n_qubits] = complex(1.0, 0)
         return self
 
-    def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: List[int]) -> "AbstractQuantumSimulator":
+    def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: list[int]) -> "AbstractQuantumSimulator":
         raise NotImplementedError("The numpy simulator cannot handle noise")
diff --git a/pyquil/simulation/_reference.py b/pyquil/simulation/_reference.py
index ea6f77bcc..fe5a72936 100644
--- a/pyquil/simulation/_reference.py
+++ b/pyquil/simulation/_reference.py
@@ -14,23 +14,25 @@
 #    limitations under the License.
 ##############################################################################
 import warnings
-from typing import Any, List, Optional, Sequence, Union
+from collections.abc import Sequence
+from typing import Any, Optional, Union
 
 import numpy as np
 from numpy.random.mtrand import RandomState
 
-from pyquil.paulis import PauliTerm, PauliSum
+from pyquil.paulis import PauliSum, PauliTerm
 from pyquil.pyqvm import AbstractQuantumSimulator
 from pyquil.quilbase import Gate
-from pyquil.simulation.matrices import P0, P1, KRAUS_OPS, QUANTUM_GATES
-from pyquil.simulation.tools import lifted_gate_matrix, lifted_gate, all_bitstrings
+from pyquil.simulation.matrices import KRAUS_OPS, P0, P1, QUANTUM_GATES
+from pyquil.simulation.tools import all_bitstrings, lifted_gate, lifted_gate_matrix
 
 
 def _term_expectation(wf: np.ndarray, term: PauliTerm, n_qubits: int) -> Any:
     # Computes <psi|XYZ..XXZ|psi>
     wf2 = wf
     for qubit_i, op_str in term._ops.items():
-        assert isinstance(qubit_i, int)
+        if not isinstance(qubit_i, int):
+            raise TypeError("Operation only supported for on PauliTerms with integer qubits.")
         # Re-use QUANTUM_GATES since it has X, Y, Z
         op_mat = QUANTUM_GATES[op_str]
         op_mat = lifted_gate_matrix(matrix=op_mat, qubit_inds=[qubit_i], n_qubits=n_qubits)
@@ -45,9 +47,9 @@ def _term_expectation(wf: np.ndarray, term: PauliTerm, n_qubits: int) -> Any:
 
 
 def _is_valid_quantum_state(state_matrix: np.ndarray, rtol: float = 1e-05, atol: float = 1e-08) -> bool:
-    """
-    Checks if a quantum state is valid, i.e. the matrix is Hermitian; trace one, and that the
-    eigenvalues are non-negative.
+    """Check if a quantum state is valid.
+
+    That is, the matrix is Hermitian; trace one, and that the eigenvalues are non-negative.
 
     :param state_matrix: a D by D np.ndarray representing a quantum state
     :param rtol: The relative tolerance parameter in np.allclose and np.isclose
@@ -69,8 +71,7 @@ def _is_valid_quantum_state(state_matrix: np.ndarray, rtol: float = 1e-05, atol:
 
 class ReferenceWavefunctionSimulator(AbstractQuantumSimulator):
     def __init__(self, n_qubits: int, rs: Optional[RandomState] = None):
-        """
-        A wavefunction simulator that prioritizes readability over performance.
+        """Initialize a wavefunction simulator that prioritizes readability over performance.
 
         Please consider using
         :py:class:`PyQVM(..., wf_simulator_type=ReferenceWavefunctionSimulator)` rather
@@ -84,8 +85,6 @@ def __init__(self, n_qubits: int, rs: Optional[RandomState] = None):
         :param rs: a RandomState (should be shared with the owning :py:class:`PyQVM`) for
             doing anything stochastic. A value of ``None`` disallows doing anything stochastic.
         """
-        super().__init__(n_qubits=n_qubits, rs=rs)
-
         self.n_qubits = n_qubits
         self.rs = rs
 
@@ -93,8 +92,7 @@ def __init__(self, n_qubits: int, rs: Optional[RandomState] = None):
         self.wf[0] = complex(1.0, 0)
 
     def sample_bitstrings(self, n_samples: int) -> np.ndarray:
-        """
-        Sample bitstrings from the distribution defined by the wavefunction.
+        """Sample bitstrings from the distribution defined by the wavefunction.
 
         Qubit 0 is at ``out[:, 0]``.
 
@@ -114,8 +112,7 @@ def sample_bitstrings(self, n_samples: int) -> np.ndarray:
         return bitstrings
 
     def do_gate(self, gate: Gate) -> "ReferenceWavefunctionSimulator":
-        """
-        Perform a gate.
+        """Perform a gate.
 
         :return: ``self`` to support method chaining.
         """
@@ -124,8 +121,7 @@ def do_gate(self, gate: Gate) -> "ReferenceWavefunctionSimulator":
         return self
 
     def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "ReferenceWavefunctionSimulator":
-        """
-        Apply an arbitrary unitary; not necessarily a named gate.
+        """Apply an arbitrary unitary; not necessarily a named gate.
 
         :param matrix: The unitary matrix to apply. No checks are done.
         :param qubits: The qubits to apply the unitary to.
@@ -136,8 +132,7 @@ def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "Referenc
         return self
 
     def do_measurement(self, qubit: int) -> int:
-        """
-        Measure a qubit, collapse the wavefunction, and return the measurement result.
+        """Measure a qubit, collapse the wavefunction, and return the measurement result.
 
         :param qubit: Index of the qubit to measure.
         :return: measured bit
@@ -166,8 +161,7 @@ def do_measurement(self, qubit: int) -> int:
             return 1
 
     def expectation(self, operator: Union[PauliTerm, PauliSum]) -> float:
-        """
-        Compute the expectation of an operator.
+        """Compute the expectation of an operator.
 
         :param operator: The operator
         :return: The operator's expectation value
@@ -178,8 +172,7 @@ def expectation(self, operator: Union[PauliTerm, PauliSum]) -> float:
         return sum(_term_expectation(self.wf, term, n_qubits=self.n_qubits) for term in operator)  # type: ignore
 
     def reset(self) -> "ReferenceWavefunctionSimulator":
-        """
-        Reset the wavefunction to the ``|000...00>`` state.
+        """Reset the wavefunction to the ``|000...00>`` state.
 
         :return: ``self`` to support method chaining.
         """
@@ -187,13 +180,12 @@ def reset(self) -> "ReferenceWavefunctionSimulator":
         self.wf[0] = complex(1.0, 0)
         return self
 
-    def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: List[int]) -> "AbstractQuantumSimulator":
+    def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: list[int]) -> "AbstractQuantumSimulator":
         raise NotImplementedError("The reference wavefunction simulator cannot handle noise")
 
 
 def zero_state_matrix(n_qubits: int) -> np.ndarray:
-    """
-    Construct a matrix corresponding to the tensor product of `n` ground states ``|0><0|``.
+    """Construct a matrix corresponding to the tensor product of `n` ground states ``|0><0|``.
 
     :param n_qubits: The number of qubits.
     :return: The state matrix  ``|000...0><000...0|`` for `n_qubits`.
@@ -204,8 +196,7 @@ def zero_state_matrix(n_qubits: int) -> np.ndarray:
 
 
 class ReferenceDensitySimulator(AbstractQuantumSimulator):
-    """
-    A density matrix simulator that prioritizes readability over performance.
+    """A density matrix simulator that prioritizes readability over performance.
 
     Please consider using
     :py:class:`PyQVM(..., wf_simulator_type=ReferenceDensitySimulator)` rather
@@ -220,18 +211,15 @@ class ReferenceDensitySimulator(AbstractQuantumSimulator):
     """
 
     def __init__(self, n_qubits: int, rs: Optional[RandomState] = None):
-        super().__init__(n_qubits=n_qubits, rs=rs)
-
         self.n_qubits = n_qubits
         self.rs = rs
         self.density: np.ndarray
         self.set_initial_state(zero_state_matrix(n_qubits)).reset()
 
     def set_initial_state(self, state_matrix: np.ndarray) -> "ReferenceDensitySimulator":
-        """
-        This method is the correct way (TM) to update the initial state matrix that is
-        initialized every time reset() is called. The default initial state of
-        ReferenceDensitySimulator is ``|000...00>``.
+        """Update the initial state matrix that is initialized every time reset() is called.
+
+        The default initial state of ReferenceDensitySimulator is ``|000...00>``.
 
         Note that the current state matrix, i.e. ``self.density`` is not affected by this
         method; you must change it directly or else call reset() after calling this method.
@@ -258,8 +246,7 @@ def set_initial_state(self, state_matrix: np.ndarray) -> "ReferenceDensitySimula
         return self
 
     def sample_bitstrings(self, n_samples: int, tol_factor: float = 1e8) -> np.ndarray:
-        """
-        Sample bitstrings from the distribution defined by the wavefunction.
+        """Sample bitstrings from the distribution defined by the wavefunction.
 
         Qubit 0 is at ``out[:, 0]``.
 
@@ -289,8 +276,7 @@ def sample_bitstrings(self, n_samples: int, tol_factor: float = 1e8) -> np.ndarr
         return bitstrings
 
     def do_gate(self, gate: Gate) -> "AbstractQuantumSimulator":
-        """
-        Perform a gate.
+        """Perform a gate.
 
         :return: ``self`` to support method chaining.
         """
@@ -299,8 +285,7 @@ def do_gate(self, gate: Gate) -> "AbstractQuantumSimulator":
         return self
 
     def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "AbstractQuantumSimulator":
-        """
-        Apply an arbitrary unitary; not necessarily a named gate.
+        """Apply an arbitrary unitary; not necessarily a named gate.
 
         :param matrix: The unitary matrix to apply. No checks are done
         :param qubits: A list of qubits to apply the unitary to.
@@ -311,8 +296,7 @@ def do_gate_matrix(self, matrix: np.ndarray, qubits: Sequence[int]) -> "Abstract
         return self
 
     def do_measurement(self, qubit: int) -> int:
-        """
-        Measure a qubit and collapse the wavefunction
+        """Measure a qubit and collapse the wavefunction.
 
         :return: The measurement result. A 1 or a 0.
         """
@@ -340,16 +324,14 @@ def expectation(self, operator: Union[PauliTerm, PauliSum]) -> complex:
         raise NotImplementedError("To implement")
 
     def reset(self) -> "AbstractQuantumSimulator":
-        """
-        Resets the current state of ReferenceDensitySimulator ``self.density`` to
-        ``self.initial_density``.
+        """Reset the current state of ReferenceDensitySimulator ``self.density`` to ``self.initial_density``.
 
         :return: ``self`` to support method chaining.
         """
         self.density = self.initial_density
         return self
 
-    def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: List[int]) -> "ReferenceDensitySimulator":
+    def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: list[int]) -> "ReferenceDensitySimulator":
         kraus_ops = KRAUS_OPS[noise_type](p=noise_prob)
         if np.isclose(noise_prob, 0.0):
             warnings.warn(f"Skipping {noise_type} post-gate noise because noise_prob is close to 0", stacklevel=2)
diff --git a/pyquil/simulation/matrices.py b/pyquil/simulation/matrices.py
index 460af7192..6dc476724 100644
--- a/pyquil/simulation/matrices.py
+++ b/pyquil/simulation/matrices.py
@@ -13,8 +13,7 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-r"""
-Standard gate set, as detailed in Quil whitepaper (arXiV:1608:03355v2)
+r"""Standard gate set, as detailed in Quil whitepaper (arXiV:1608:03355v2).
 
 Currently includes:
     I - identity :math:`\begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}`
@@ -88,10 +87,10 @@
     SQISW - XY(:math: `\pi/2`)-interaction
     :math:`\begin{pmatrix} 1&0&0&0 \\ 0&\frac{1}{\sqrt{2}}&\frac{i}{\sqrt{2}}&0 \\ \frac{i}{\sqrt{2}}&\frac{1}{\sqrt{2}} \\  0&0&0&1 \end{pmatrix}`
 
-    FSIM(:math:`\theta, \phi`) - XX+YY interaction with conditonal phase on \|11\>
+    FSIM(:math:`\theta, \phi`) - XX+YY interaction with conditional phase on \|11\>
     :math:`\begin{pmatrix} 1&0&0&0 \\ 0&\cos(\frac{\theta}{2})&i\sin(\frac{\theta}{2})&0 \\ 0&i\sin(\frac{\theta}{2})&\cos(\frac{\theta}{2})&0 \\  0&0&0&e^{i \phi} \end{pmatrix}`
 
-    PHASEDFSIM(:math:`\theta, \zeta, \chi, \gamma, \phi`) - XX+YY interaction with conditonal phase on \|11\>
+    PHASEDFSIM(:math:`\theta, \zeta, \chi, \gamma, \phi`) - XX+YY interaction with conditional phase on \|11\>
     :math:`\begin{pmatrix} 1&0&0&0 \\ 0&\ e^{-i(\gamma+\zeta)}\cos(\frac{\theta}{2})&ie^{-i(\gamma-\chi)}\sin(\frac{\theta}{2})&0 \\ 0&ie^{-i(\gamma+\chi)}\sin(\frac{\theta}{2})&e^{-i(\gamma-\zeta)}\cos(\frac{\theta}{2})&0 \\  0&0&0&e^{ i\phi - 2i\gamma} \end{pmatrix}`
 
     RXX(:math:`\phi`) - XX-interaction
@@ -114,12 +113,12 @@
     P1 - project-onto-one
     :math:`\begin{pmatrix} 0 & 0 \\ 0 & 1 \end{pmatrix}`
 """  # noqa: E501
+
 import cmath
-from typing import Tuple
 
 import numpy as np
 
-I = np.array([[1.0, 0.0], [0.0, 1.0]])
+I = np.array([[1.0, 0.0], [0.0, 1.0]])  # noqa: E741
 
 X = np.array([[0.0, 1.0], [1.0, 0.0]])
 
@@ -356,28 +355,22 @@ def BARENCO(alpha: float, phi: float, theta: float) -> np.ndarray:
 }
 
 
-def relaxation_operators(p: float) -> Tuple[np.ndarray, np.ndarray]:
-    """
-    Return the amplitude damping Kraus operators
-    """
+def relaxation_operators(p: float) -> tuple[np.ndarray, np.ndarray]:
+    """Return the amplitude damping Kraus operators."""
     k0 = np.array([[1.0, 0.0], [0.0, np.sqrt(1 - p)]])
     k1 = np.array([[0.0, np.sqrt(p)], [0.0, 0.0]])
     return k0, k1
 
 
-def dephasing_operators(p: float) -> Tuple[np.ndarray, np.ndarray]:
-    """
-    Return the phase damping Kraus operators
-    """
+def dephasing_operators(p: float) -> tuple[np.ndarray, np.ndarray]:
+    """Return the phase damping Kraus operators."""
     k0 = np.eye(2) * np.sqrt(1 - p / 2)
     k1 = np.sqrt(p / 2) * Z
     return k0, k1
 
 
-def depolarizing_operators(p: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
-    """
-    Return the phase damping Kraus operators
-    """
+def depolarizing_operators(p: float) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
+    """Return the phase damping Kraus operators."""
     k0 = np.sqrt(1.0 - p) * I
     k1 = np.sqrt(p / 3.0) * X
     k2 = np.sqrt(p / 3.0) * Y
@@ -385,28 +378,22 @@ def depolarizing_operators(p: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray
     return k0, k1, k2, k3
 
 
-def phase_flip_operators(p: float) -> Tuple[np.ndarray, np.ndarray]:
-    """
-    Return the phase flip kraus operators
-    """
+def phase_flip_operators(p: float) -> tuple[np.ndarray, np.ndarray]:
+    """Return the phase flip kraus operators."""
     k0 = np.sqrt(1 - p) * I
     k1 = np.sqrt(p) * Z
     return k0, k1
 
 
-def bit_flip_operators(p: float) -> Tuple[np.ndarray, np.ndarray]:
-    """
-    Return the phase flip kraus operators
-    """
+def bit_flip_operators(p: float) -> tuple[np.ndarray, np.ndarray]:
+    """Return the phase flip kraus operators."""
     k0 = np.sqrt(1 - p) * I
     k1 = np.sqrt(p) * X
     return k0, k1
 
 
-def bitphase_flip_operators(p: float) -> Tuple[np.ndarray, np.ndarray]:
-    """
-    Return the bitphase flip kraus operators
-    """
+def bitphase_flip_operators(p: float) -> tuple[np.ndarray, np.ndarray]:
+    """Return the bitphase flip kraus operators."""
     k0 = np.sqrt(1 - p) * I
     k1 = np.sqrt(p) * Y
     return k0, k1
diff --git a/pyquil/simulation/tools.py b/pyquil/simulation/tools.py
index f3afd6a78..433bf84ca 100644
--- a/pyquil/simulation/tools.py
+++ b/pyquil/simulation/tools.py
@@ -13,7 +13,10 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from typing import List, Sequence, Tuple, Union, cast
+"""Miscellaneous tools that are helpful for simulation."""
+
+from collections.abc import Sequence
+from typing import Union, cast
 
 import numpy as np
 
@@ -22,7 +25,7 @@
 from pyquil.quil import Program
 from pyquil.quilatom import Parameter
 from pyquil.quilbase import Gate, Halt, _strip_modifiers
-from pyquil.simulation.matrices import SWAP, STATES, QUANTUM_GATES
+from pyquil.simulation.matrices import QUANTUM_GATES, STATES, SWAP
 
 
 def all_bitstrings(n_bits: int) -> np.ndarray:
@@ -48,9 +51,7 @@ def all_bitstrings(n_bits: int) -> np.ndarray:
 
 
 def qubit_adjacent_lifted_gate(i: int, matrix: np.ndarray, n_qubits: int) -> np.ndarray:
-    """
-    Lifts input k-qubit gate on adjacent qubits starting from qubit i
-    to complete Hilbert space of dimension 2 ** num_qubits.
+    """Lift k-qubit gate on adjacent qubits from qubit i to complete Hilbert space of dimension 2 ** num_qubits.
 
     Ex: 1-qubit gate, lifts from qubit i
     Ex: 2-qubit gate, lifts from qubits (i+1, i)
@@ -77,9 +78,11 @@ def qubit_adjacent_lifted_gate(i: int, matrix: np.ndarray, n_qubits: int) -> np.
         complete Hilbert space of all num_qubits.
     """
     n_rows, n_cols = matrix.shape
-    assert n_rows == n_cols, "Matrix must be square"
+    if n_rows != n_cols:
+        raise ValueError("Matrix must be square")
     gate_size = np.log2(n_rows)
-    assert gate_size == int(gate_size), "Matrix must be 2^n by 2^n"
+    if gate_size != int(gate_size):
+        raise ValueError("Matrix must be 2^n by 2^n")
     gate_size = int(gate_size)
 
     # Outer-product to lift gate to complete Hilbert space
@@ -93,10 +96,8 @@ def qubit_adjacent_lifted_gate(i: int, matrix: np.ndarray, n_qubits: int) -> np.
     return np.kron(top_matrix, np.kron(matrix, bottom_matrix))
 
 
-def two_swap_helper(j: int, k: int, num_qubits: int, qubit_map: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
-    """
-    Generate the permutation matrix that permutes two single-particle Hilbert
-    spaces into adjacent positions.
+def two_swap_helper(j: int, k: int, num_qubits: int, qubit_map: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
+    """Generate the permutation matrix that permutes two single-particle Hilbert spaces into adjacent positions.
 
     ALWAYS swaps j TO k. Recall that Hilbert spaces are ordered in decreasing
     qubit index order. Hence, j > k implies that j is to the left of k.
@@ -139,10 +140,8 @@ def two_swap_helper(j: int, k: int, num_qubits: int, qubit_map: np.ndarray) -> T
     return perm, new_qubit_map
 
 
-def permutation_arbitrary(qubit_inds: Sequence[int], n_qubits: int) -> Tuple[np.ndarray, np.ndarray, int]:
-    """
-    Generate the permutation matrix that permutes an arbitrary number of
-    single-particle Hilbert spaces into adjacent positions.
+def permutation_arbitrary(qubit_inds: Sequence[int], n_qubits: int) -> tuple[np.ndarray, np.ndarray, int]:
+    """Generate the permutation matrix that permutes an arbitrary number of single-particle Hilbert spaces into adjacent positions.
 
     Transposes the qubit indices in the order they are passed to a
     contiguous region in the complete Hilbert space, in increasing
@@ -217,14 +216,19 @@ def permutation_arbitrary(qubit_inds: Sequence[int], n_qubits: int) -> Tuple[np.
         # for next iteration, go in opposite direction
         right = not right
 
-    assert np.allclose(qubit_arr[final_map[-1] : final_map[0] + 1][::-1], qubit_inds)
+    if not np.allclose(qubit_arr[final_map[-1] : final_map[0] + 1][::-1], qubit_inds):
+        segment_values = qubit_arr[final_map[-1] : final_map[0] + 1][::-1]
+        raise ValueError(
+            f"Permutation matrix not constructed correctly. "
+            f"Expected array segment {segment_values} to match qubit indices {qubit_inds}. "
+            f"Segment values: {segment_values}, Expected values: {qubit_inds}"
+        )
+
     return perm, qubit_arr[::-1], start_i
 
 
 def lifted_gate_matrix(matrix: np.ndarray, qubit_inds: Sequence[int], n_qubits: int) -> np.ndarray:
-    """
-    Lift a unitary matrix to act on the specified qubits in a full ``n_qubits``-qubit
-    Hilbert space.
+    """Lift a unitary matrix to act on the specified qubits in a full ``n_qubits``-qubit Hilbert space.
 
     For 1-qubit gates, this is easy and can be achieved with appropriate kronning of identity
     matrices. For 2-qubit gates acting on adjacent qubit indices, it is also easy. However,
@@ -237,9 +241,11 @@ def lifted_gate_matrix(matrix: np.ndarray, qubit_inds: Sequence[int], n_qubits:
     :return: A 2^n by 2^n lifted version of the unitary matrix acting on the specified qubits.
     """
     n_rows, n_cols = matrix.shape
-    assert n_rows == n_cols, "Matrix must be square"
+    if n_rows != n_cols:
+        raise ValueError("Matrix must be square")
     gate_size = np.log2(n_rows)
-    assert gate_size == int(gate_size), "Matrix must be 2^n by 2^n"
+    if gate_size != int(gate_size):
+        raise ValueError("Matrix must be 2^n by 2^n")
     gate_size = int(gate_size)
 
     pi_permutation_matrix, final_map, start_i = permutation_arbitrary(qubit_inds, n_qubits)
@@ -255,8 +261,7 @@ def lifted_gate_matrix(matrix: np.ndarray, qubit_inds: Sequence[int], n_qubits:
 
 
 def lifted_gate(gate: Gate, n_qubits: int) -> np.ndarray:
-    """
-    Lift a pyquil :py:class:`Gate` in a full ``n_qubits``-qubit Hilbert space.
+    """Lift a pyquil :py:class:`Gate` in a full ``n_qubits``-qubit Hilbert space.
 
     This function looks up the matrix form of the gate and then dispatches to
     :py:func:`lifted_gate_matrix` with the target qubits.
@@ -265,7 +270,6 @@ def lifted_gate(gate: Gate, n_qubits: int) -> np.ndarray:
     :param n_qubits: The total number of qubits.
     :return: A 2^n by 2^n lifted version of the gate acting on its specified qubits.
     """
-
     zero = np.eye(2)
     zero[1, 1] = 0
     one = np.eye(2)
@@ -304,7 +308,8 @@ def _gate_matrix(gate: Gate) -> np.ndarray:
                 matrix = _gate_matrix(child)
                 return np.kron(zero, np.eye(*matrix.shape)) + np.kron(one, matrix)  # type: ignore
             elif mod == "FORKED":
-                assert len(gate.params) % 2 == 0
+                if len(gate.params) % 2 != 0:
+                    raise ValueError("FORKED gates must have an even number of parameters.")
                 p0, p1 = gate.params[: len(gate.params) // 2], gate.params[len(gate.params) // 2 :]
                 child = _strip_modifiers(gate, limit=1)
                 # handle the first half of the FORKED params
@@ -315,7 +320,7 @@ def _gate_matrix(gate: Gate) -> np.ndarray:
                 mat1 = _gate_matrix(child)
                 return np.kron(zero, mat0) + np.kron(one, mat1)
             else:
-                raise TypeError("Unsupported gate modifier {}".format(mod))
+                raise TypeError(f"Unsupported gate modifier {mod}")
 
     matrix = _gate_matrix(gate)
 
@@ -323,8 +328,7 @@ def _gate_matrix(gate: Gate) -> np.ndarray:
 
 
 def program_unitary(program: Program, n_qubits: int) -> np.ndarray:
-    """
-    Return the unitary of a pyQuil program.
+    """Return the unitary of a pyQuil program.
 
     :param program: A program consisting only of :py:class:`Gate`.:
     :return: a unitary corresponding to the composition of the program's gates.
@@ -344,11 +348,8 @@ def program_unitary(program: Program, n_qubits: int) -> np.ndarray:
     return umat
 
 
-def lifted_pauli(pauli_sum: Union[PauliSum, PauliTerm], qubits: List[int]) -> np.ndarray:
-    """
-    Takes a PauliSum object along with a list of
-    qubits and returns a matrix corresponding the tensor representation of the
-    object.
+def lifted_pauli(pauli_sum: Union[PauliSum, PauliTerm], qubits: list[int]) -> np.ndarray:
+    """Return a matrix corresponding to the tensor representation of the given PauliSum and qubits.
 
     Useful for generating the full Hamiltonian after a particular fermion to
     pauli transformation. For example:
@@ -357,10 +358,12 @@ def lifted_pauli(pauli_sum: Union[PauliSum, PauliTerm], qubits: List[int]) -> np
 
     .. code-block:: python
 
-       [[ 0.+0.j,  0.+0.j,  0.+0.j,  0.-2.j],
-        [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j],
-        [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j],
-        [ 0.+2.j,  0.+0.j,  0.+0.j,  0.+0.j]]
+       [
+           [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 - 2.0j],
+           [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+           [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+           [0.0 + 2.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+       ]
 
     Developer note: Quil and the QVM like qubits to be ordered such that qubit 0 is on the right.
     Therefore, in ``qubit_adjacent_lifted_gate``, ``lifted_pauli``, and ``lifted_state_operator``,
@@ -386,11 +389,8 @@ def lifted_pauli(pauli_sum: Union[PauliSum, PauliTerm], qubits: List[int]) -> np
     return result_hilbert
 
 
-def tensor_up(pauli_sum: Union[PauliSum, PauliTerm], qubits: List[int]) -> np.ndarray:
-    """
-    Takes a PauliSum object along with a list of
-    qubits and returns a matrix corresponding the tensor representation of the
-    object.
+def tensor_up(pauli_sum: Union[PauliSum, PauliTerm], qubits: list[int]) -> np.ndarray:
+    """Return a matrix corresponding to the tensor representation of the given PauliSum and qubits.
 
     This is the same as :py:func:`lifted_pauli`. Nick R originally wrote this functionality
     and really likes the name ``tensor_up``. Who can blame him?
@@ -402,9 +402,8 @@ def tensor_up(pauli_sum: Union[PauliSum, PauliTerm], qubits: List[int]) -> np.nd
     return lifted_pauli(pauli_sum=pauli_sum, qubits=qubits)
 
 
-def lifted_state_operator(state: TensorProductState, qubits: List[int]) -> np.ndarray:
-    """Take a TensorProductState along with a list of qubits and return a matrix
-    corresponding to the tensored-up representation of the states' density operator form.
+def lifted_state_operator(state: TensorProductState, qubits: list[int]) -> np.ndarray:
+    """Return a matrix corresponding to the tensored-up representation of the given state and qubits.
 
     Developer note: Quil and the QVM like qubits to be ordered such that qubit 0 is on the right.
     Therefore, in ``qubit_adjacent_lifted_gate``, ``lifted_pauli``, and ``lifted_state_operator``,
@@ -416,7 +415,8 @@ def lifted_state_operator(state: TensorProductState, qubits: List[int]) -> np.nd
     mat: np.ndarray = np.eye(1)
     for qubit in qubits:
         oneq_state = state[qubit]
-        assert oneq_state.qubit == qubit
+        if oneq_state.qubit != qubit:
+            raise ValueError(f"State qubit mismatch: {oneq_state.qubit} != {qubit}")
         state_vector = STATES[oneq_state.label][oneq_state.index][:, np.newaxis]
         state_matrix = state_vector @ state_vector.conj().T
         mat = np.kron(state_matrix, mat)
@@ -424,9 +424,7 @@ def lifted_state_operator(state: TensorProductState, qubits: List[int]) -> np.nd
 
 
 def scale_out_phase(unitary1: np.ndarray, unitary2: np.ndarray) -> np.ndarray:
-    """
-    Returns a matrix m equal to unitary1/θ where ɑ satisfies unitary2
-    = e^(iθ)·unitary1.
+    """Return a matrix m equal to unitary1/θ where ɑ satisfies unitary2 = e^(iθ)·unitary1.
 
     :param unitary1: The unitary matrix from which the constant of
         proportionality should be scaled-out.
@@ -449,6 +447,7 @@ def scale_out_phase(unitary1: np.ndarray, unitary2: np.ndarray) -> np.ndarray:
 
 def unitary_equal(A: np.ndarray, B: np.ndarray) -> bool:
     """Check if two matrices are unitarily equal."""
-    assert A.shape == B.shape
+    if A.shape != B.shape:
+        raise ValueError("Matrices must have the same shape")
     dim = A.shape[0]
     return np.allclose(np.abs(np.trace(A.T.conjugate() @ B) / dim), 1.0)
diff --git a/pyquil/wavefunction.py b/pyquil/wavefunction.py
index 3681cab27..c6161a68d 100644
--- a/pyquil/wavefunction.py
+++ b/pyquil/wavefunction.py
@@ -13,11 +13,11 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-"""
-Module containing the Wavefunction object and methods for working with wavefunctions.
-"""
+"""Module containing the Wavefunction object and methods for working with wavefunctions."""
+
 import itertools
-from typing import Dict, Iterator, List, Optional, Sequence, cast
+from collections.abc import Iterator, Sequence
+from typing import Optional, cast
 
 import numpy as np
 
@@ -25,10 +25,8 @@
 OCTETS_PER_COMPLEX_DOUBLE = 2 * OCTETS_PER_DOUBLE_FLOAT
 
 
-class Wavefunction(object):
-    """
-    Encapsulate a wavefunction representing a quantum state
-    as returned by :py:class:`~pyquil.api.WavefunctionSimulator`.
+class Wavefunction:
+    """Encapsulate a wavefunction representing a quantum state as returned by :py:class:`~pyquil.api.WavefunctionSimulator`.
 
     .. note::
 
@@ -40,8 +38,7 @@ class Wavefunction(object):
     """
 
     def __init__(self, amplitude_vector: np.ndarray):
-        """
-        Initializes a wavefunction
+        """Initialize a Wavefunction.
 
         :param amplitude_vector: A numpy array of complex amplitudes
         """
@@ -51,14 +48,11 @@ def __init__(self, amplitude_vector: np.ndarray):
         self.amplitudes: np.ndarray = np.asarray(amplitude_vector)
         sumprob = np.sum(self.probabilities())
         if not np.isclose(sumprob, 1.0):
-            raise ValueError(
-                "The wavefunction is not normalized. " "The probabilities sum to {} instead of 1".format(sumprob)
-            )
+            raise ValueError("The wavefunction is not normalized. " f"The probabilities sum to {sumprob} instead of 1")
 
     @staticmethod
     def zeros(qubit_num: int) -> "Wavefunction":
-        """
-        Constructs the groundstate wavefunction for a given number of qubits.
+        """Construct the groundstate wavefunction for a given number of qubits.
 
         :param qubit_num:
         :return: A Wavefunction in the ground state
@@ -69,8 +63,8 @@ def zeros(qubit_num: int) -> "Wavefunction":
 
     @staticmethod
     def from_bit_packed_string(coef_string: bytes) -> "Wavefunction":
-        """
-        From a bit packed string, unpacks to get the wavefunction
+        """Unpack the bit string to get a Wavefunction.
+
         :param coef_string:
         """
         num_cfloat = len(coef_string) // OCTETS_PER_COMPLEX_DOUBLE
@@ -93,13 +87,11 @@ def __str__(self) -> str:
         return self.pretty_print(decimal_digits=10)
 
     def probabilities(self) -> np.ndarray:
-        """Returns an array of probabilities in lexicographical order"""
+        """Return an array of probabilities in lexicographical order."""
         return np.abs(self.amplitudes) ** 2  # type: ignore
 
-    def get_outcome_probs(self) -> Dict[str, float]:
-        """
-        Parses a wavefunction (array of complex amplitudes) and returns a dictionary of
-        outcomes and associated probabilities.
+    def get_outcome_probs(self) -> dict[str, float]:
+        """Parse a wavefunction (array of complex amplitudes) and return a dictionary of outcomes and associated probabilities.
 
         :return: A dict with outcomes as keys and probabilities as values.
         :rtype: dict
@@ -111,9 +103,8 @@ def get_outcome_probs(self) -> Dict[str, float]:
             outcome_dict[outcome] = abs(amplitude) ** 2
         return outcome_dict
 
-    def pretty_print_probabilities(self, decimal_digits: int = 2) -> Dict[str, float]:
-        """
-        TODO: This doesn't seem like it is named correctly...
+    def pretty_print_probabilities(self, decimal_digits: int = 2) -> dict[str, float]:
+        """TODO: This doesn't seem like it is named correctly...
 
         Prints outcome probabilities, ignoring all outcomes with approximately zero probabilities
         (up to a certain number of decimal digits) and rounding the probabilities to decimal_digits.
@@ -131,10 +122,10 @@ def pretty_print_probabilities(self, decimal_digits: int = 2) -> Dict[str, float
         return outcome_dict
 
     def pretty_print(self, decimal_digits: int = 2) -> str:
-        """
-        Returns a string repr of the wavefunction, ignoring all outcomes with approximately zero
-        amplitude (up to a certain number of decimal digits) and rounding the amplitudes to
-        decimal_digits.
+        """Return a human-friendly string representation of the wavefunction.
+
+        Ignores all outcomes with approximately zero amplitude (up to a certain number of decimal digits) and rounding
+        the amplitudes to decimal_digits.
 
         :param int decimal_digits: The number of digits to truncate to.
         :return: A string representation of the wavefunction.
@@ -147,16 +138,13 @@ def pretty_print(self, decimal_digits: int = 2) -> str:
             amplitude = round(amplitude.real, decimal_digits) + round(amplitude.imag, decimal_digits) * 1.0j
             if amplitude != 0.0:
                 outcome_dict[outcome] = amplitude
-                pp_string += str(amplitude) + "|{}> + ".format(outcome)
+                pp_string += str(amplitude) + f"|{outcome}> + "
         if len(pp_string) >= 3:
             pp_string = pp_string[:-3]  # remove the dangling + if it is there
         return pp_string
 
     def plot(self, qubit_subset: Optional[Sequence[int]] = None) -> None:
-        """
-        TODO: calling this will error because of matplotlib
-
-        Plots a bar chart with bitstring on the x axis and probability on the y axis.
+        """Plot a bar chart with bitstring on the x-axis and probability on the y-axis.
 
         :param qubit_subset: Optional parameter used for plotting a subset of the Hilbert space.
         """
@@ -168,17 +156,16 @@ def plot(self, qubit_subset: Optional[Sequence[int]] = None) -> None:
             qubit_num = len(self)
             for i in qubit_subset:
                 if i > (2**qubit_num - 1):
-                    raise IndexError("Index {} too large for {} qubits.".format(i, qubit_num))
+                    raise IndexError(f"Index {i} too large for {qubit_num} qubits.")
                 else:
                     sub_dict[get_bitstring_from_index(i, qubit_num)] = prob_dict[get_bitstring_from_index(i, qubit_num)]
             prob_dict = sub_dict
-        plt.bar(range(len(prob_dict)), prob_dict.values(), align="center", color="#6CAFB7")
-        plt.xticks(range(len(prob_dict)), prob_dict.keys())
+        plt.bar(range(len(prob_dict)), list(prob_dict.values()), align="center", color="#6CAFB7")
+        plt.xticks(range(len(prob_dict)), list(prob_dict.keys()))
         plt.show()
 
     def sample_bitstrings(self, n_samples: int) -> np.ndarray:
-        """
-        Sample bitstrings from the distribution defined by the wavefunction.
+        """Sample bitstrings from the distribution defined by the wavefunction.
 
         :param n_samples: The number of bitstrings to sample
         :return: An array of shape (n_samples, n_qubits)
@@ -190,21 +177,20 @@ def sample_bitstrings(self, n_samples: int) -> np.ndarray:
 
 
 def get_bitstring_from_index(index: int, qubit_num: int) -> str:
-    """
-    Returns the bitstring in lexical order that corresponds to the given index in 0 to 2^(qubit_num)
+    """Get the bitstring in lexical order that corresponds to the given index in 0 to 2^(qubit_num).
+
     :param int index:
     :param int qubit_num:
     :return: the bitstring
     :rtype: str
     """
     if index > (2**qubit_num - 1):
-        raise IndexError("Index {} too large for {} qubits.".format(index, qubit_num))
+        raise IndexError(f"Index {index} too large for {qubit_num} qubits.")
     return bin(index)[2:].rjust(qubit_num, "0")
 
 
-def _octet_bits(o: int) -> List[int]:
-    """
-    Get the bits of an octet.
+def _octet_bits(o: int) -> list[int]:
+    """Get the bits of an octet.
 
     :param o: The octets.
     :return: The bits as a list in LSB-to-MSB order.
diff --git a/scripts/ci_publish_grpc_web.py b/scripts/ci_publish_grpc_web.py
index 5657d3205..a6f4fff60 100644
--- a/scripts/ci_publish_grpc_web.py
+++ b/scripts/ci_publish_grpc_web.py
@@ -1,9 +1,9 @@
-"""
-Changes the qcs-sdk-python dependency to qcs-sdk-python-grpc-web.
+"""Changes the qcs-sdk-python dependency to qcs-sdk-python-grpc-web.
 """
 
 from io import TextIOWrapper
-from os.path import dirname, realpath, join
+from os.path import dirname, join, realpath
+
 import toml
 
 workspace_path = dirname(dirname(realpath(__file__)))
@@ -35,4 +35,4 @@ def write(f: TextIOWrapper, data):
     updated = f.read().replace("__package__", '"pyquil_grpc_web"')
     f.seek(0)
     f.truncate()
-    f.write(updated)
\ No newline at end of file
+    f.write(updated)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e73d43d23..39d1c0bc8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,10 +13,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Set up Python 3.8
-        uses: actions/setup-python@v2
+      - name: Set up Python 3.9
+        uses: actions/setup-python@v4
         with:
-          python-version: '3.8'
+          python-version: '3.9'
       - uses: actions/cache@v2
         with:
           path: .venv
@@ -33,10 +33,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Set up Python 3.8
-        uses: actions/setup-python@v2
+      - name: Set up Python 3.9
+        uses: actions/setup-python@v4
         with:
-          python-version: '3.8'
+          python-version: '3.9'
       - uses: actions/cache@v2
         with:
           path: .venv
@@ -52,10 +52,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Set up Python 3.8
-        uses: actions/setup-python@v2
+      - name: Set up Python 3.9
+        uses: actions/setup-python@v4
         with:
-          python-version: '3.8'
+          python-version: '3.9'
       - uses: actions/cache@v2
         with:
           path: .venv
@@ -71,10 +71,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Set up Python 3.8
-        uses: actions/setup-python@v2
+      - name: Set up Python 3.9
+        uses: actions/setup-python@v4
         with:
-          python-version: '3.8'
+          python-version: '3.9'
       - uses: actions/cache@v2
         with:
           path: .venv
@@ -85,12 +85,27 @@ jobs:
           . scripts/ci_install_deps
           poetry run make check-types
 
+  check-dependencies:
+
+    name: Check dependencies for vulnerabilities
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Go
+      uses: actions/setup-go@v5
+    - name: Install OSV scanner
+      run: |
+        go install github.com/google/osv-scanner/cmd/osv-scanner@latest
+    - name: Run OSV scanner
+      run: |
+        osv-scanner --lockfile=./poetry.lock
+
   test-doctest:
     name: Run Doctests
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0-beta.1"]
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{ matrix.python-version }}
@@ -118,7 +133,7 @@ jobs:
       pull-requests: write # allows coverage bot to comment
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0-beta.1"]
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{ matrix.python-version }}
@@ -147,19 +162,19 @@ jobs:
           token:  ${{ secrets.GITHUB_TOKEN }}
 
   check-docker-image:
-    name: Check types
+    name: Check docker image
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Set up Python 3.8
+      - name: Set up Python 3.9
         uses: actions/setup-python@v2
         with:
-          python-version: '3.8'
+          python-version: '3.9'
       - uses: actions/cache@v2
         with:
           path: .venv
           key: poetry-${{ hashFiles('poetry.lock') }}
-      - name: Check types
+      - name: Check wheel installation
         run: |
           sudo apt update
           pip wheel . -w wheels
@@ -179,7 +194,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0-beta.1"]
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{ matrix.python-version }}
diff --git a/Dockerfile.test b/Dockerfile.test
index 942526384..08f9caabe 100644
--- a/Dockerfile.test
+++ b/Dockerfile.test
@@ -1,7 +1,7 @@
 # use multi-stage builds to independently pull dependency versions
 ARG quilc_version=1.20.0
 ARG qvm_version=1.17.1
-ARG python_version=3.8
+ARG python_version=3.9
 
 # use multi-stage builds to independently pull dependency versions
 FROM rigetti/quilc:$quilc_version as quilc
diff --git a/pyquil/conftest.py b/pyquil/conftest.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/test/e2e/test_e2e.py b/test/e2e/test_e2e.py
index d232b95f0..1f71606e1 100644
--- a/test/e2e/test_e2e.py
+++ b/test/e2e/test_e2e.py
@@ -15,18 +15,14 @@
 ##############################################################################
 from multiprocessing.pool import ThreadPool
 
-import nest_asyncio
 import numpy as np
 
 from pyquil import Program
-from pyquil.api import QuantumComputer, QPU
-from pyquil.gates import H, CNOT, MEASURE, RX
+from pyquil.api import QPU, QuantumComputer
+from pyquil.gates import CNOT, MEASURE, RX, H
 from pyquil.quilatom import MemoryReference
 from pyquil.quilbase import Declare
 
-nest_asyncio.apply()
-
-
 TEST_PROGRAM = Program(
     Declare("ro", "BIT", 2),
     H(0),
diff --git a/test/unit/__snapshots__/test_api.ambr b/test/unit/__snapshots__/test_api.ambr
index 5b886a8ce..f729ace63 100644
--- a/test/unit/__snapshots__/test_api.ambr
+++ b/test/unit/__snapshots__/test_api.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: test_local_rb_sequence
   list([
     '''
diff --git a/test/unit/__snapshots__/test_compiler.ambr b/test/unit/__snapshots__/test_compiler.ambr
index dbdd56a7e..fc4c291a0 100644
--- a/test/unit/__snapshots__/test_compiler.ambr
+++ b/test/unit/__snapshots__/test_compiler.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: test_transpile_qasm_2
   '''
   DECLARE ro BIT[2]
diff --git a/test/unit/__snapshots__/test_main.ambr b/test/unit/__snapshots__/test_main.ambr
index c3d209a4c..fc251bb4c 100644
--- a/test/unit/__snapshots__/test_main.ambr
+++ b/test/unit/__snapshots__/test_main.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: test_remove_reset_from_program
   '''
   DEFGATE XGATE AS MATRIX:
diff --git a/test/unit/__snapshots__/test_paulis.ambr b/test/unit/__snapshots__/test_paulis.ambr
index 5ccbe37d7..a39675f40 100644
--- a/test/unit/__snapshots__/test_paulis.ambr
+++ b/test/unit/__snapshots__/test_paulis.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: test_ordered
   '''
   CNOT 3 2
diff --git a/test/unit/__snapshots__/test_program.ambr b/test/unit/__snapshots__/test_program.ambr
index 9ea735b4a..c1739836b 100644
--- a/test/unit/__snapshots__/test_program.ambr
+++ b/test/unit/__snapshots__/test_program.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: test_with_loop
   '''
   DECLARE ro BIT[1]
diff --git a/test/unit/__snapshots__/test_quantum_computer.ambr b/test/unit/__snapshots__/test_quantum_computer.ambr
index da86b6ed2..83d1aa5d5 100644
--- a/test/unit/__snapshots__/test_quantum_computer.ambr
+++ b/test/unit/__snapshots__/test_quantum_computer.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: test_flip_array_to_prog
   '''
   RX(3.141592653589793) 0
diff --git a/test/unit/__snapshots__/test_quil.ambr b/test/unit/__snapshots__/test_quil.ambr
index c1cea031b..b5a4c0624 100644
--- a/test/unit/__snapshots__/test_quil.ambr
+++ b/test/unit/__snapshots__/test_quil.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: TestProgram.test_calibrations
   list([
     Calibration { instructions: [Gate(Gate { name: "X", parameters: [], qubits: [Fixed(0)], modifiers: [] })], modifiers: [], name: "Calibrate", parameters: [], qubits: [Fixed(0)] },
diff --git a/test/unit/__snapshots__/test_quilatom.ambr b/test/unit/__snapshots__/test_quilatom.ambr
index 59b6e43d9..7306933c3 100644
--- a/test/unit/__snapshots__/test_quilatom.ambr
+++ b/test/unit/__snapshots__/test_quilatom.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: TestFrame.test_out[With-FormalArgument]
   'One "FRAME"'
 # ---
diff --git a/test/unit/__snapshots__/test_quilbase.ambr b/test/unit/__snapshots__/test_quilbase.ambr
index c7811f422..7bc606745 100644
--- a/test/unit/__snapshots__/test_quilbase.ambr
+++ b/test/unit/__snapshots__/test_quilbase.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: TestArithmeticBinaryOp.test_out[ADD-left0-right0]
   'ADD a[0] b[0]'
 # ---
diff --git a/test/unit/__snapshots__/test_quilt.ambr b/test/unit/__snapshots__/test_quilt.ambr
index 8000893ae..2ad8289b7 100644
--- a/test/unit/__snapshots__/test_quilt.ambr
+++ b/test/unit/__snapshots__/test_quilt.ambr
@@ -1,3 +1,4 @@
+# serializer version: 1
 # name: test_round_trip_calibration_program[cz]
   '''
   DEFCAL CZ 110 111:
diff --git a/test/unit/conftest.py b/test/unit/conftest.py
index 3377eabcc..fcb8d236a 100644
--- a/test/unit/conftest.py
+++ b/test/unit/conftest.py
@@ -1,5 +1,5 @@
 import os
-from typing import Dict, Any
+from typing import Any, Dict
 
 import numpy as np
 import pytest
@@ -8,13 +8,13 @@
 from qcs_sdk.qvm import QVMClient
 
 from pyquil.api import (
-    QVMCompiler,
     BenchmarkConnection,
+    QVMCompiler,
 )
 from pyquil.external.rpcq import CompilerISA
 from pyquil.gates import I
 from pyquil.paulis import sX
-from pyquil.quantum_processor import QCSQuantumProcessor, CompilerQuantumProcessor
+from pyquil.quantum_processor import CompilerQuantumProcessor, QCSQuantumProcessor
 from pyquil.quantum_processor.transformers.graph_to_compiler_isa import (
     DEFAULT_1Q_GATES,
     DEFAULT_2Q_GATES,
@@ -22,7 +22,8 @@
     _transform_qubit_operation_to_gates,
 )
 from pyquil.quil import Program
-from test.unit.utils import DummyCompiler, CLIENT_PUBLIC_KEY, CLIENT_SECRET_KEY, SERVER_PUBLIC_KEY
+from test.unit.utils import DummyCompiler
+
 from .. import override_qcs_config
 
 TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
@@ -31,8 +32,7 @@
 
 @pytest.fixture
 def compiler_isa() -> CompilerISA:
-    """
-    Configures an arbitrary ``CompilerISA`` that may be used to initialize
+    """Configures an arbitrary ``CompilerISA`` that may be used to initialize
     a ``CompilerQuantumProcessor``. Developers should create specific test cases of
     ``CompilerISA`` as separate fixtures in conftest.py or in the test file.
     """
@@ -90,8 +90,7 @@ def compiler_quantum_processor(compiler_isa: CompilerISA) -> CompilerQuantumProc
 
 @pytest.fixture
 def aspen8_compiler_isa() -> CompilerISA:
-    """
-    Read the Aspen-8 QCS ``CompilerISA`` from file. This should be an exact conversion of
+    """Read the Aspen-8 QCS ``CompilerISA`` from file. This should be an exact conversion of
     qcs_aspen8_isa to a ``CompilerISA``.
     """
     return CompilerISA.parse_file(os.path.join(TEST_DATA_DIR, "compiler-isa-Aspen-8.json"))
@@ -99,8 +98,7 @@ def aspen8_compiler_isa() -> CompilerISA:
 
 @pytest.fixture
 def qcs_aspen8_isa() -> InstructionSetArchitecture:
-    """
-    Read the Aspen-8 QCS InstructionSetArchitecture from file and load it into
+    """Read the Aspen-8 QCS InstructionSetArchitecture from file and load it into
     the ``InstructionSetArchitecture`` QCS API client model.
     """
     with open(os.path.join(TEST_DATA_DIR, "qcs-isa-Aspen-8.json")) as f:
diff --git a/test/unit/test_api.py b/test/unit/test_api.py
index e5dcec1df..f7d098ca3 100644
--- a/test/unit/test_api.py
+++ b/test/unit/test_api.py
@@ -18,12 +18,12 @@
 
 import numpy as np
 
-from pyquil.external.rpcq import  CompilerISA, Qubit, Edge
-from pyquil.gates import CNOT, H, MEASURE, PHASE, RZ, RX, CZ
+from pyquil.external.rpcq import CompilerISA, Edge, Qubit
+from pyquil.gates import CNOT, CZ, MEASURE, PHASE, RX, RZ, H
 from pyquil.paulis import PauliTerm
 from pyquil.quil import Program
 from pyquil.quilatom import MemoryReference
-from pyquil.quilbase import Halt, Declare
+from pyquil.quilbase import Declare, Halt
 from pyquil.simulation.tools import program_unitary
 
 EMPTY_PROGRAM = Program()
diff --git a/test/unit/test_compiler.py b/test/unit/test_compiler.py
index 1cacf44f5..c44dea0f2 100644
--- a/test/unit/test_compiler.py
+++ b/test/unit/test_compiler.py
@@ -2,15 +2,18 @@
 from typing import Optional
 
 import pytest
+from qcs_sdk.qpu.translation import TranslationBackend
 from syrupy.assertion import SnapshotAssertion
 
 from pyquil import Program
-from pyquil.api._compiler import QPUCompiler
-from pyquil.gates import RX, MEASURE, RZ
+from pyquil.api._compiler import (
+    IncompatibleBackendForQuantumProcessorIDWarning,
+    QPUCompiler,
+    select_backend_for_quantum_processor_id,
+)
+from pyquil.gates import MEASURE, RX, RZ
 from pyquil.quilatom import FormalArgument
 from pyquil.quilbase import DefCalibration
-from pyquil.api._compiler import select_backend_for_quantum_processor_id, IncompatibleBackendForQuantumProcessorIDWarning
-from qcs_sdk.qpu.translation import TranslationBackend
 
 
 def simple_program():
@@ -57,4 +60,4 @@ def test_translation_backend_validation(quantum_processor_id: str, backend: Opti
     else:
         actual = select_backend_for_quantum_processor_id(quantum_processor_id, backend)
     assert actual == expected
-    
+
diff --git a/test/unit/test_compiler_client.py b/test/unit/test_compiler_client.py
index 3f52063c4..2e23a4ea2 100644
--- a/test/unit/test_compiler_client.py
+++ b/test/unit/test_compiler_client.py
@@ -17,13 +17,13 @@
 try:
     from unittest.mock import AsyncMock
 except ImportError:  # 3.7 requires this backport of AsyncMock
-    from mock import AsyncMock
+    from unittest.mock import AsyncMock
 
-from qcs_sdk import QCSClient
-from _pytest.monkeypatch import MonkeyPatch
 import pytest
+from _pytest.monkeypatch import MonkeyPatch
 from pytest import raises
 from pytest_mock import MockerFixture
+from qcs_sdk import QCSClient
 
 from pyquil.api._compiler_client import (
     CompilerClient,
diff --git a/test/unit/test_control_flow_graph.py b/test/unit/test_control_flow_graph.py
index 5ed97b0bb..996a05591 100644
--- a/test/unit/test_control_flow_graph.py
+++ b/test/unit/test_control_flow_graph.py
@@ -1,6 +1,6 @@
 from pyquil.control_flow_graph import BasicBlock
-from pyquil.quilbase import AbstractInstruction
 from pyquil.quil import Program
+from pyquil.quilbase import AbstractInstruction
 
 
 def test_control_flow_graph():
diff --git a/test/unit/test_diagnostics.py b/test/unit/test_diagnostics.py
index ab72dbdae..fb8bd1b2d 100644
--- a/test/unit/test_diagnostics.py
+++ b/test/unit/test_diagnostics.py
@@ -4,4 +4,4 @@
 
 def test_report():
     report = get_report()
-    assert report.split("\n").pop(0) == "pyQuil version: {}".format(pyquil.__version__)
+    assert report.split("\n").pop(0) == f"pyQuil version: {pyquil.__version__}"
diff --git a/test/unit/test_gates.py b/test/unit/test_gates.py
index 98f29d1db..60a82102d 100644
--- a/test/unit/test_gates.py
+++ b/test/unit/test_gates.py
@@ -1,4 +1,5 @@
 import pytest
+
 from pyquil.gates import (
     CNOT,
     CPHASE,
@@ -6,26 +7,26 @@
     CPHASE01,
     CPHASE10,
     CZ,
-    H,
-    I,
+    FSIM,
     ISWAP,
     PHASE,
+    PHASEDFSIM,
     PSWAP,
     RX,
+    RXX,
     RY,
+    RYY,
     RZ,
-    S,
+    RZZ,
+    SQISW,
     SWAP,
+    H,
+    I,
+    S,
     T,
     X,
     Y,
     Z,
-    SQISW,
-    FSIM,
-    PHASEDFSIM,
-    RZZ,
-    RXX,
-    RYY,
 )
 from pyquil.quilbase import _strip_modifiers
 
@@ -62,7 +63,7 @@ def five_param_twoq_gate(request):
 
 def test_oneq_gate(oneq_gate):
     g = oneq_gate(234)
-    assert g.out() == "{} 234".format(g.name)
+    assert g.out() == f"{g.name} 234"
 
     func_name = oneq_gate.__name__
     assert g.name == func_name
@@ -70,12 +71,12 @@ def test_oneq_gate(oneq_gate):
 
 def test_oneq_gate_kwarg(oneq_gate):
     g = oneq_gate(qubit=234)
-    assert g.out() == "{} 234".format(g.name)
+    assert g.out() == f"{g.name} 234"
 
 
 def test_param_oneq_gate(param_oneq_gate):
     g = param_oneq_gate(0.2, 234)
-    assert g.out() == "{}(0.2) 234".format(g.name)
+    assert g.out() == f"{g.name}(0.2) 234"
 
     func_name = param_oneq_gate.__name__
     assert g.name == func_name
@@ -83,12 +84,12 @@ def test_param_oneq_gate(param_oneq_gate):
 
 def test_param_oneq_gate_kwarg(param_oneq_gate):
     g = param_oneq_gate(angle=0.2, qubit=234)
-    assert g.out() == "{}(0.2) 234".format(g.name)
+    assert g.out() == f"{g.name}(0.2) 234"
 
 
 def test_twoq_gate(twoq_gate):
     g = twoq_gate(234, 567)
-    assert g.out() == "{} 234 567".format(g.name)
+    assert g.out() == f"{g.name} 234 567"
 
     func_name = twoq_gate.__name__
     assert g.name == func_name
@@ -104,12 +105,12 @@ def test_twoq_gate_kwarg(twoq_gate):
         raise ValueError()
 
     g = twoq_gate(**qubits)
-    assert g.out() == "{} 234 567".format(g.name)
+    assert g.out() == f"{g.name} 234 567"
 
 
 def test_param_twoq_gate(param_twoq_gate):
     g = param_twoq_gate(0.2, 234, 567)
-    assert g.out() == "{}(0.2) 234 567".format(g.name)
+    assert g.out() == f"{g.name}(0.2) 234 567"
 
     func_name = param_twoq_gate.__name__
     assert g.name == func_name
@@ -117,7 +118,7 @@ def test_param_twoq_gate(param_twoq_gate):
 
 def test_two_param_twoq_gate(two_param_twoq_gate):
     g = two_param_twoq_gate(0.2, 0.4, 234, 567)
-    assert g.out() == "{}(0.2, 0.4) 234 567".format(g.name)
+    assert g.out() == f"{g.name}(0.2, 0.4) 234 567"
 
     func_name = two_param_twoq_gate.__name__
     assert g.name == func_name
@@ -125,7 +126,7 @@ def test_two_param_twoq_gate(two_param_twoq_gate):
 
 def test_five_param_twoq_gate(five_param_twoq_gate):
     g = five_param_twoq_gate(0.2, 0.4, 0.1, 0.3, 0.5, 234, 567)
-    assert g.out() == "{}(0.2, 0.4, 0.1, 0.3, 0.5) 234 567".format(g.name)
+    assert g.out() == f"{g.name}(0.2, 0.4, 0.1, 0.3, 0.5) 234 567"
 
     func_name = five_param_twoq_gate.__name__
     assert g.name == func_name
@@ -140,7 +141,7 @@ def test_param_twoq_gate_kwarg(param_twoq_gate):
     else:
         raise ValueError()
     g = param_twoq_gate(0.2, **qubits)
-    assert g.out() == "{}(0.2) 234 567".format(g.name)
+    assert g.out() == f"{g.name}(0.2) 234 567"
 
 
 def test_controlled_gate():
diff --git a/test/unit/test_graph.py b/test/unit/test_graph.py
index 902420512..397ab92d9 100644
--- a/test/unit/test_graph.py
+++ b/test/unit/test_graph.py
@@ -1,18 +1,19 @@
+from typing import Any, Dict
+
 import networkx as nx
+
+from pyquil.external.rpcq import CompilerISA
+from pyquil.quantum_processor.graph import NxQuantumProcessor
+from pyquil.quantum_processor.transformers import graph_to_compiler_isa
 from pyquil.quantum_processor.transformers.graph_to_compiler_isa import (
-    compiler_isa_to_graph,
-    DEFAULT_2Q_GATES,
     DEFAULT_1Q_GATES,
+    DEFAULT_2Q_GATES,
+    compiler_isa_to_graph,
 )
-from pyquil.quantum_processor.transformers import graph_to_compiler_isa
-from pyquil.quantum_processor.graph import NxQuantumProcessor
-from pyquil.external.rpcq import CompilerISA
-from typing import Dict, Any
 
 
 def test_isa_from_graph_order():
-    """
-    Test that edge keys in the CompilerISA are properly formatted and sorted by
+    """Test that edge keys in the CompilerISA are properly formatted and sorted by
     the node id.
     """
     # since node 16 appears first, even though we ask for the edge (15,16) the networkx internal
@@ -26,8 +27,7 @@ def test_isa_from_graph_order():
 
 
 def test_compiler_isa_to_graph(compiler_isa: CompilerISA):
-    """
-    Test that compiler_isa_to_graph transforms a ``CompilerISA`` to an ``nx.Graph``
+    """Test that compiler_isa_to_graph transforms a ``CompilerISA`` to an ``nx.Graph``
     accurately and that an ``NxQuantumProcessor.qubit_topology`` is isomorphic to the
     raw ``nx.Graph``.
     """
@@ -40,8 +40,7 @@ def test_compiler_isa_to_graph(compiler_isa: CompilerISA):
 
 
 def test_graph_to_compiler_isa(compiler_isa: CompilerISA, noise_model_dict: Dict[str, Any]):
-    """
-    Test that ```compiler_isa_to_graph``` transforms ``CompilerISA`` to an ``nx.Graph`` and that
+    """Test that ```compiler_isa_to_graph``` transforms ``CompilerISA`` to an ``nx.Graph`` and that
     ``graph_to_compiler_isa`` transforms an ``nx.Device`` to a ``CompilerISA`` with default
     gates.
     """
diff --git a/test/unit/test_group.py b/test/unit/test_group.py
index c8ce0f08d..fd630d8b3 100644
--- a/test/unit/test_group.py
+++ b/test/unit/test_group.py
@@ -4,28 +4,27 @@
 
 from pyquil import Program
 from pyquil.experiment._group import (
-    get_results_by_qubit_groups,
-    merge_disjoint_experiments,
-    group_settings,
+    _max_tpb_overlap,
     _max_weight_operator,
     _max_weight_state,
-    _max_tpb_overlap,
+    get_results_by_qubit_groups,
+    group_settings,
+    merge_disjoint_experiments,
 )
 from pyquil.experiment._main import Experiment, _pauli_to_product_state
 from pyquil.experiment._result import ExperimentResult
 from pyquil.experiment._setting import (
     ExperimentSetting,
-    plusX,
+    TensorProductState,
     minusX,
-    plusZ,
     minusZ,
+    plusX,
     plusY,
-    TensorProductState,
+    plusZ,
     zeros_state,
 )
-from pyquil.gates import H
-from pyquil.gates import X, Z
-from pyquil.paulis import sZ, sX, sY, sI, PauliTerm
+from pyquil.gates import H, X, Z
+from pyquil.paulis import PauliTerm, sI, sX, sY, sZ
 
 
 def test_results_by_qubit_groups():
@@ -256,8 +255,7 @@ def test_group_experiments_greedy():
 
 def test_expt_settings_diagonal_in_tpb():
     def _expt_settings_diagonal_in_tpb(es1: ExperimentSetting, es2: ExperimentSetting):
-        """
-        Extends the concept of being diagonal in the same tpb to ExperimentSettings, by
+        """Extends the concept of being diagonal in the same tpb to ExperimentSettings, by
         determining if the pairs of in_states and out_operators are separately diagonal in the same
         tpb
         """
diff --git a/test/unit/test_latex.py b/test/unit/test_latex.py
index 069dbaf9d..3159ecde6 100644
--- a/test/unit/test_latex.py
+++ b/test/unit/test_latex.py
@@ -1,11 +1,11 @@
 import pytest
 
-from pyquil.quil import Program, Pragma
-from pyquil.quilbase import Declare, Measurement, JumpTarget, Jump
-from pyquil.quilatom import MemoryReference, Label
-from pyquil.gates import H, X, Y, XY, RX, CZ, CPHASE, SWAP, MEASURE, CNOT, WAIT, MOVE
-from pyquil.latex import to_latex, DiagramSettings
+from pyquil.gates import CNOT, CPHASE, CZ, MEASURE, MOVE, RX, SWAP, WAIT, XY, H, X, Y
+from pyquil.latex import DiagramSettings, to_latex
 from pyquil.latex._diagram import split_on_terminal_measures
+from pyquil.quil import Pragma, Program
+from pyquil.quilatom import Label, MemoryReference
+from pyquil.quilbase import Declare, Jump, JumpTarget, Measurement
 
 
 def test_to_latex():
@@ -44,14 +44,14 @@ def test_fail_on_forked():
 
 
 def test_gate_group_pragma():
-    "Check that to_latex does not fail on LATEX_GATE_GROUP pragma."
+    """Check that to_latex does not fail on LATEX_GATE_GROUP pragma."""
     p = Program()
     p.inst(Pragma("LATEX_GATE_GROUP", [], "foo"), X(0), X(0), Pragma("END_LATEX_GATE_GROUP"), X(1))
     _ = to_latex(p)
 
 
 def test_fail_on_bad_pragmas():
-    "Check that to_latex raises an error when pragmas are imbalanced."
+    """Check that to_latex raises an error when pragmas are imbalanced."""
     # missing END_LATEX_GATE_GROUP
     with pytest.raises(ValueError):
         _ = to_latex(Program(Pragma("LATEX_GATE_GROUP", [], "foo"), X(0)))
@@ -75,7 +75,7 @@ def test_fail_on_bad_pragmas():
 
 
 def test_warn_on_pragma_with_trailing_measures():
-    "Check that to_latex warns when measurement alignment conflicts with gate group pragma."
+    """Check that to_latex warns when measurement alignment conflicts with gate group pragma."""
     with pytest.warns(UserWarning):
         _ = to_latex(
             Program(
@@ -90,7 +90,6 @@ def test_warn_on_pragma_with_trailing_measures():
 
 def test_split_measures():
     """Check that we can split terminal measurements."""
-
     prog = Program(
         Declare("ro", "BIT"),
         X(0),
diff --git a/test/unit/test_main.py b/test/unit/test_main.py
index 3fa7fa160..e19449adf 100644
--- a/test/unit/test_main.py
+++ b/test/unit/test_main.py
@@ -1,24 +1,23 @@
 import numpy as np
 
 from pyquil import Program
-from pyquil.experiment._main import _remove_reset_from_program, _pauli_to_product_state
-from pyquil.experiment._program import (
-    parameterized_readout_symmetrization,
-    parameterized_single_qubit_measurement_basis,
-    parameterized_single_qubit_state_preparation,
-)
 from pyquil.experiment import (
+    Experiment,
     ExperimentSetting,
     TensorProductState,
-    Experiment,
     plusZ,
     read_json,
     to_json,
 )
+from pyquil.experiment._main import _pauli_to_product_state, _remove_reset_from_program
+from pyquil.experiment._program import (
+    parameterized_readout_symmetrization,
+    parameterized_single_qubit_measurement_basis,
+    parameterized_single_qubit_state_preparation,
+)
 from pyquil.gates import MEASURE, RESET, X, Y
 from pyquil.paulis import sI, sX, sY, sZ
 
-
 EXPERIMENT_REPR = """
 shots: 1
 active reset: disabled
diff --git a/test/unit/test_matrices.py b/test/unit/test_matrices.py
index 15502aaca..2d7eb541a 100644
--- a/test/unit/test_matrices.py
+++ b/test/unit/test_matrices.py
@@ -2,12 +2,12 @@
 
 from pyquil.simulation.matrices import (
     QUANTUM_GATES,
-    relaxation_operators,
+    bit_flip_operators,
+    bitphase_flip_operators,
     dephasing_operators,
     depolarizing_operators,
-    bit_flip_operators,
     phase_flip_operators,
-    bitphase_flip_operators,
+    relaxation_operators,
 )
 
 
diff --git a/test/unit/test_memory.py b/test/unit/test_memory.py
index ec0bbd23a..bcc5986ee 100644
--- a/test/unit/test_memory.py
+++ b/test/unit/test_memory.py
@@ -2,8 +2,8 @@
 
 from pyquil.experiment._memory import (
     merge_memory_map_lists,
-    pauli_term_to_preparation_memory_map,
     pauli_term_to_measurement_memory_map,
+    pauli_term_to_preparation_memory_map,
 )
 from pyquil.paulis import sX, sY
 from pyquil.quilatom import (
diff --git a/test/unit/test_noise.py b/test/unit/test_noise.py
index 09077cb93..2c06d67e3 100644
--- a/test/unit/test_noise.py
+++ b/test/unit/test_noise.py
@@ -3,32 +3,32 @@
 import numpy as np
 import pytest
 from pytest_mock import MockerFixture
-from qcs_sdk import ResultData, ExecutionData, RegisterData
+from qcs_sdk import ExecutionData, RegisterData, ResultData
 from qcs_sdk.qvm import QVMResultData
 
 from pyquil.api._qam import QAMExecutionResult
-from pyquil.gates import RZ, RX, I, CZ
+from pyquil.gates import CZ, RX, RZ, I
 from pyquil.noise import (
-    pauli_kraus_map,
-    damping_kraus_map,
-    dephasing_kraus_map,
-    tensor_kraus_maps,
-    _get_program_gates,
-    _decoherence_noise_model,
-    add_decoherence_noise,
-    combine_kraus_maps,
-    damping_after_dephasing,
     INFINITY,
-    apply_noise_model,
-    _noise_model_program_header,
+    NO_NOISE,
     KrausModel,
     NoiseModel,
-    corrupt_bitstring_probs,
-    correct_bitstring_probs,
-    estimate_bitstring_probs,
+    _decoherence_noise_model,
+    _get_program_gates,
+    _noise_model_program_header,
+    add_decoherence_noise,
+    apply_noise_model,
     bitstring_probs_to_z_moments,
+    combine_kraus_maps,
+    correct_bitstring_probs,
+    corrupt_bitstring_probs,
+    damping_after_dephasing,
+    damping_kraus_map,
+    dephasing_kraus_map,
     estimate_assignment_probs,
-    NO_NOISE,
+    estimate_bitstring_probs,
+    pauli_kraus_map,
+    tensor_kraus_maps,
 )
 from pyquil.quil import Pragma, Program
 from pyquil.quilbase import DefGate, Gate
diff --git a/test/unit/test_noise_gates.py b/test/unit/test_noise_gates.py
index d892802ce..0d7ef8d02 100644
--- a/test/unit/test_noise_gates.py
+++ b/test/unit/test_noise_gates.py
@@ -1,6 +1,7 @@
 import numpy as np
-from pyquil.gates import RZ, RX, I, CZ, ISWAP, CPHASE
-from pyquil.noise_gates import _get_qvm_noise_supported_gates, THETA
+
+from pyquil.gates import CPHASE, CZ, ISWAP, RX, RZ, I
+from pyquil.noise_gates import THETA, _get_qvm_noise_supported_gates
 
 
 def test_get_qvm_noise_supported_gates_from_compiler_isa(compiler_isa):
diff --git a/test/unit/test_numpy.py b/test/unit/test_numpy.py
index c9d789edd..aac2cccda 100644
--- a/test/unit/test_numpy.py
+++ b/test/unit/test_numpy.py
@@ -4,23 +4,23 @@
 import pytest
 
 from pyquil import Program
-from pyquil.gates import CCNOT, CNOT, H, MEASURE, RX, X
-from pyquil.paulis import sZ, sX
+from pyquil.gates import CCNOT, CNOT, MEASURE, RX, H, X
+from pyquil.paulis import sX, sZ
 from pyquil.pyqvm import PyQVM
 from pyquil.quilatom import MemoryReference
 from pyquil.quilbase import Declare, DefGate
 from pyquil.simulation._numpy import (
-    targeted_einsum,
     NumpyWavefunctionSimulator,
+    _term_expectation,
     all_bitstrings,
+    targeted_einsum,
     targeted_tensordot,
-    _term_expectation,
 )
 from pyquil.simulation._reference import ReferenceWavefunctionSimulator
 from pyquil.simulation.matrices import QUANTUM_GATES as GATES
 from test.unit.test_reference_wavefunction import (
-    _generate_random_program,
     _generate_random_pauli,
+    _generate_random_program,
 )
 
 
@@ -266,11 +266,14 @@ def kron(*matrices: np.ndarray) -> np.ndarray:
     A *args version of lambda args: functools.reduce(np.kron, args).
 
     Args:
+    ----
         *matrices: The matrices and controls to combine with the kronecker
             product.
 
     Returns:
+    -------
         The resulting matrix.
+
     """
     product = np.eye(1)
     for m in matrices:
diff --git a/test/unit/test_operator_estimation.py b/test/unit/test_operator_estimation.py
index f4b9e8c53..7a9489b6b 100644
--- a/test/unit/test_operator_estimation.py
+++ b/test/unit/test_operator_estimation.py
@@ -5,34 +5,32 @@
 import numpy as np
 import pytest
 
-from pyquil import Program
-from pyquil.api import WavefunctionSimulator
-from pyquil.api import QCSClient
-from pyquil import get_qc
+from pyquil import Program, get_qc
+from pyquil.api import QCSClient, WavefunctionSimulator
 from pyquil.experiment import (
-    ExperimentSetting,
     SIC0,
     SIC1,
     SIC2,
     SIC3,
-    TensorProductState,
     Experiment,
+    ExperimentSetting,
+    TensorProductState,
     minusY,
     minusZ,
     plusX,
     plusY,
     plusZ,
 )
-from pyquil.gates import CNOT, CZ, H, I, RX, RY, RZ, X, Y
+from pyquil.gates import CNOT, CZ, RX, RY, RZ, H, I, X, Y
 from pyquil.operator_estimation import (
+    _calibration_program,
     _one_q_sic_prep,
-    group_experiments,
-    measure_observables,
     _ops_bool_to_prog,
     _stats_from_measurements,
-    _calibration_program,
+    group_experiments,
+    measure_observables,
 )
-from pyquil.paulis import sI, sX, sY, sZ, PauliSum
+from pyquil.paulis import PauliSum, sI, sX, sY, sZ
 from pyquil.quilbase import Pragma
 
 
@@ -148,8 +146,7 @@ def test_sic_process_tomo(client_configuration: QCSClient):
 
 
 def test_measure_observables_symmetrize(client_configuration: QCSClient):
-    """
-    Symmetrization alone should not change the outcome on the QVM
+    """Symmetrization alone should not change the outcome on the QVM
     """
     expts = [
         ExperimentSetting(TensorProductState(), o1 * o2)
@@ -169,8 +166,7 @@ def test_measure_observables_symmetrize(client_configuration: QCSClient):
 
 
 def test_measure_observables_symmetrize_calibrate(client_configuration: QCSClient):
-    """
-    Symmetrization + calibration should not change the outcome on the QVM
+    """Symmetrization + calibration should not change the outcome on the QVM
     """
     expts = [
         ExperimentSetting(TensorProductState(), o1 * o2)
@@ -190,8 +186,7 @@ def test_measure_observables_symmetrize_calibrate(client_configuration: QCSClien
 
 
 def test_measure_observables_zero_expectation(client_configuration: QCSClient):
-    """
-    Testing case when expectation value of observable should be close to zero
+    """Testing case when expectation value of observable should be close to zero
     """
     qc = get_qc("2q-qvm", client_configuration=client_configuration)
     exptsetting = ExperimentSetting(plusZ(0), sX(0))
@@ -605,8 +600,7 @@ def test_expectations_sic3(client_configuration: QCSClient, use_seed: bool):
 
 
 def test_sic_conditions(client_configuration: QCSClient):
-    """
-    Test that the SIC states indeed yield SIC-POVMs
+    """Test that the SIC states indeed yield SIC-POVMs
     """
     wfn_sim = WavefunctionSimulator(client_configuration=client_configuration)
 
@@ -697,13 +691,13 @@ def test_measure_observables_grouped_expts(client_configuration: QCSClient, use_
 
 def _point_channel_fidelity_estimate(v, dim=2):
     """:param v: array of expectation values
-    :param dim: dimensionality of the Hilbert space"""
+    :param dim: dimensionality of the Hilbert space
+    """
     return (1.0 + np.sum(v) + dim) / (dim * (dim + 1))
 
 
 def test_bit_flip_channel_fidelity(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     """
     qc = get_qc("1q-qvm", client_configuration=client_configuration)
     if use_seed:
@@ -750,8 +744,7 @@ def test_bit_flip_channel_fidelity(client_configuration: QCSClient, use_seed: bo
 
 
 def test_dephasing_channel_fidelity(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     """
     qc = get_qc("1q-qvm", client_configuration=client_configuration)
     if use_seed:
@@ -795,8 +788,7 @@ def test_dephasing_channel_fidelity(client_configuration: QCSClient, use_seed: b
 
 
 def test_depolarizing_channel_fidelity(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     """
     qc = get_qc("1q-qvm", client_configuration=client_configuration)
     if use_seed:
@@ -842,8 +834,7 @@ def test_depolarizing_channel_fidelity(client_configuration: QCSClient, use_seed
 
 
 def test_unitary_channel_fidelity(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     """
     qc = get_qc("1q-qvm", client_configuration=client_configuration)
     if use_seed:
@@ -881,8 +872,7 @@ def test_unitary_channel_fidelity(client_configuration: QCSClient, use_seed: boo
 
 
 def test_bit_flip_channel_fidelity_readout_error(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     """
     qc = get_qc("1q-qvm", client_configuration=client_configuration)
     if use_seed:
@@ -930,8 +920,7 @@ def test_bit_flip_channel_fidelity_readout_error(client_configuration: QCSClient
 
 
 def test_dephasing_channel_fidelity_readout_error(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     """
     qc = get_qc("1q-qvm", client_configuration=client_configuration)
     if use_seed:
@@ -977,8 +966,7 @@ def test_dephasing_channel_fidelity_readout_error(client_configuration: QCSClien
 
 
 def test_depolarizing_channel_fidelity_readout_error(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     """
     qc = get_qc("1q-qvm", client_configuration=client_configuration)
     if use_seed:
@@ -1026,8 +1014,7 @@ def test_depolarizing_channel_fidelity_readout_error(client_configuration: QCSCl
 
 
 def test_unitary_channel_fidelity_readout_error(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     """
     qc = get_qc("1q-qvm", client_configuration=client_configuration)
     if use_seed:
@@ -1067,8 +1054,7 @@ def test_unitary_channel_fidelity_readout_error(client_configuration: QCSClient,
 
 
 def test_2q_unitary_channel_fidelity_readout_error(client_configuration: QCSClient, use_seed: bool):
-    """
-    We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
+    """We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity
     This tests if our dimensionality factors are correct, even in the presence
     of readout errors
     """
@@ -1584,7 +1570,8 @@ def test_corrected_statistics_2q_nontrivial_nonentangled_state(client_configurat
 
 def _point_state_fidelity_estimate(v, dim=2):
     """:param v: array of expectation values
-    :param dim: dimensionality of the Hilbert space"""
+    :param dim: dimensionality of the Hilbert space
+    """
     return (1.0 + np.sum(v)) / dim
 
 
diff --git a/test/unit/test_parameters.py b/test/unit/test_parameters.py
index 80dac81f1..bbe6eb97b 100644
--- a/test/unit/test_parameters.py
+++ b/test/unit/test_parameters.py
@@ -4,13 +4,13 @@
 
 from pyquil.quilatom import (
     Parameter,
-    quil_sin,
-    quil_cos,
-    quil_sqrt,
-    quil_exp,
     _contained_parameters,
     format_parameter,
     quil_cis,
+    quil_cos,
+    quil_exp,
+    quil_sin,
+    quil_sqrt,
     substitute,
     substitute_array,
 )
diff --git a/test/unit/test_paulis.py b/test/unit/test_paulis.py
index 242b13a9f..34cfe7fc7 100644
--- a/test/unit/test_paulis.py
+++ b/test/unit/test_paulis.py
@@ -24,30 +24,30 @@
 import numpy as np
 import pytest
 
-from pyquil.gates import RX, RZ, CNOT, H, X, PHASE
+from pyquil.gates import CNOT, PHASE, RX, RZ, H, X
 from pyquil.paulis import (
-    PauliTerm,
+    ID,
+    ZERO,
     PauliSum,
+    PauliTerm,
+    check_commutation,
+    commuting_sets,
     exponential_map,
+    exponentiate,
     exponentiate_commuting_pauli_sum,
     exponentiate_pauli_sum,
-    ID,
-    exponentiate,
-    trotterize,
+    is_identity,
     is_zero,
-    check_commutation,
-    commuting_sets,
-    term_with_coeff,
     sI,
     sX,
     sY,
     sZ,
-    ZERO,
-    is_identity,
+    term_with_coeff,
+    trotterize,
 )
 from pyquil.quil import Program
-from pyquil.simulation.tools import program_unitary, unitary_equal
 from pyquil.simulation import matrices
+from pyquil.simulation.tools import program_unitary, unitary_equal
 
 
 def isclose(a, b, rel_tol=1e-10, abs_tol=0.0):
diff --git a/test/unit/test_paulis_with_placeholders.py b/test/unit/test_paulis_with_placeholders.py
index b6aaef205..d6488a91b 100644
--- a/test/unit/test_paulis_with_placeholders.py
+++ b/test/unit/test_paulis_with_placeholders.py
@@ -24,24 +24,24 @@
 import numpy as np
 import pytest
 
-from pyquil.gates import RX, RZ, CNOT, H, X, PHASE
+from pyquil.gates import CNOT, PHASE, RX, RZ, H, X
 from pyquil.paulis import (
-    PauliTerm,
+    ID,
+    ZERO,
     PauliSum,
+    PauliTerm,
+    check_commutation,
+    commuting_sets,
     exponential_map,
-    exponentiate_commuting_pauli_sum,
-    ID,
     exponentiate,
-    trotterize,
+    exponentiate_commuting_pauli_sum,
     is_zero,
-    check_commutation,
-    commuting_sets,
-    term_with_coeff,
     sI,
     sX,
     sY,
     sZ,
-    ZERO,
+    term_with_coeff,
+    trotterize,
 )
 from pyquil.quil import Program, address_qubits
 from pyquil.quilatom import QubitPlaceholder
@@ -70,7 +70,7 @@ def test_get_qubits():
 def test_simplify_term_single():
     q0, q1, q2 = QubitPlaceholder.register(3)
     term = PauliTerm("Z", q0) * PauliTerm("I", q1) * PauliTerm("X", q2, 0.5j) * PauliTerm("Z", q0, 1.0)
-    assert term.id() == "X{}".format(q2)
+    assert term.id() == f"X{q2}"
     assert term.coefficient == 0.5j
 
 
@@ -80,14 +80,14 @@ def test_simplify_term_xz():
     term2 = -0.5 * PauliTerm("X", q0) * (-1.0) * PauliTerm("Z", q0)
     term3 = 0.5 * PauliTerm("X", q0) * PauliTerm("Z", q0)
     for term in [term1, term2, term3]:
-        assert term.id() == "Y{}".format(q0)
+        assert term.id() == f"Y{q0}"
         assert term.coefficient == -0.5j
 
 
 def test_simplify_term_multindex():
     q0, q2 = QubitPlaceholder.register(2)
     term = PauliTerm("X", q0, coefficient=-0.5) * PauliTerm("Z", q0, coefficient=-1.0) * PauliTerm("X", q2, 0.5)
-    assert term.id(sort_ops=False) == "Y{q0}X{q2}".format(q0=q0, q2=q2)
+    assert term.id(sort_ops=False) == f"Y{q0}X{q2}"
     assert term.coefficient == -0.25j
 
 
@@ -95,11 +95,9 @@ def test_simplify_sum_terms():
     q0 = QubitPlaceholder()
     sum_term = PauliSum([PauliTerm("X", q0, 0.5), PauliTerm("Z", q0, 0.5j)])
     str_sum_term = str(sum_term + sum_term)
-    assert str_sum_term == "(1+0j)*X{q0} + 1j*Z{q0}".format(q0=q0) or str_sum_term == "1j*Z{q0} + (1+0j)*X{q0}".format(
-        q0=q0
-    )
+    assert str_sum_term == f"(1+0j)*X{q0} + 1j*Z{q0}" or str_sum_term == f"1j*Z{q0} + (1+0j)*X{q0}"
     sum_term = PauliSum([PauliTerm("X", q0, 0.5), PauliTerm("X", q0, 0.5)])
-    assert str(sum_term.simplify()) == "(1+0j)*X{q0}".format(q0=q0)
+    assert str(sum_term.simplify()) == f"(1+0j)*X{q0}"
 
     # test the simplify on multiplication
     sum_term = PauliSum([PauliTerm("X", q0, 0.5), PauliTerm("X", q0, 0.5)])
diff --git a/test/unit/test_program.py b/test/unit/test_program.py
index 28abeda12..7f0004ea6 100644
--- a/test/unit/test_program.py
+++ b/test/unit/test_program.py
@@ -2,17 +2,16 @@
 from syrupy.assertion import SnapshotAssertion
 
 from pyquil import Program
-from pyquil.quil import AbstractInstruction, Declare, Measurement
-from pyquil.quilbase import Reset, ResetQubit, Delay, DelayFrames, DelayQubits, Frame, \
-    DefMeasureCalibration
-from pyquil.quilatom import Label, MemoryReference, Qubit
 from pyquil.experiment._program import (
     measure_qubits,
+    parameterized_readout_symmetrization,
     parameterized_single_qubit_measurement_basis,
     parameterized_single_qubit_state_preparation,
-    parameterized_readout_symmetrization,
 )
-from pyquil.gates import MEASURE, RX, RZ, H, DELAY
+from pyquil.gates import DELAY, MEASURE, RX, RZ, H
+from pyquil.quil import AbstractInstruction, Declare, Measurement
+from pyquil.quilatom import Label, MemoryReference, Qubit
+from pyquil.quilbase import DefMeasureCalibration, Delay, DelayFrames, DelayQubits, Frame, Reset, ResetQubit
 
 
 def test_measure_qubits():
@@ -172,8 +171,7 @@ def test_filter_quil_t():
 
 
 def test_compatibility_layer():
-    """
-    Test that the compatibility layer that transforms pyQuil instructions to quil instructions works as intended.
+    """Test that the compatibility layer that transforms pyQuil instructions to quil instructions works as intended.
     """
     # Note: `quil` re-orders some instructions in a program (e.g. by shuffling DECLAREs to the top). This isn't a
     # problem for the current set of instructions we're testing, but it's something to keep in mind if we add more.
diff --git a/test/unit/test_qcs.py b/test/unit/test_qcs.py
index e17b93bb5..b890d3a2c 100644
--- a/test/unit/test_qcs.py
+++ b/test/unit/test_qcs.py
@@ -1,15 +1,15 @@
-from typing import Dict, Any
-from pyquil.external.rpcq import make_edge_id
+from typing import Any, Dict
+
+from qcs_sdk.qpu.isa import InstructionSetArchitecture
+
+from pyquil.external.rpcq import CompilerISA, make_edge_id
+from pyquil.noise import NoiseModel
 from pyquil.quantum_processor import QCSQuantumProcessor
 from pyquil.quantum_processor.transformers import qcs_isa_to_compiler_isa
-from pyquil.noise import NoiseModel
-from pyquil.external.rpcq import CompilerISA
-from qcs_sdk.qpu.isa import InstructionSetArchitecture
 
 
 def test_qcs_isa_to_compiler_isa(qcs_aspen8_isa: InstructionSetArchitecture, aspen8_compiler_isa: CompilerISA):
-    """
-    Test that ``qcs_isa_to_compiler_isa`` accurately transforms an ``InstructionSetArchitecture``
+    """Test that ``qcs_isa_to_compiler_isa`` accurately transforms an ``InstructionSetArchitecture``
     to a ``CompilerISA``. The qubits and edges in the former should all be present in the latter.
 
     Note, this is a data driven test using fixtures defined in conftest.py. This
@@ -28,11 +28,9 @@ def test_qcs_isa_to_compiler_isa(qcs_aspen8_isa: InstructionSetArchitecture, asp
 
 
 def test_qcs_noise_model(qcs_aspen8_isa: InstructionSetArchitecture, noise_model_dict: Dict[str, Any]):
-    """
-    Test that ``NoiseModel.from_dict`` initializes a ``NoiseModel``, which users may, in turn,
+    """Test that ``NoiseModel.from_dict`` initializes a ``NoiseModel``, which users may, in turn,
     pass to ``QCSQuantumProcessor`` for later initializing a noisy QVM.
     """
-
     noise_model = NoiseModel.from_dict(noise_model_dict)
     device = QCSQuantumProcessor("Aspen-8", qcs_aspen8_isa, noise_model=noise_model)
     assert device.quantum_processor_id == "Aspen-8"
diff --git a/test/unit/test_qpu.py b/test/unit/test_qpu.py
index 8fd8958b9..4b6e477b8 100644
--- a/test/unit/test_qpu.py
+++ b/test/unit/test_qpu.py
@@ -1,16 +1,15 @@
-import pytest
-from unittest.mock import patch, MagicMock
+from unittest.mock import MagicMock, patch
 
 import numpy as np
-
-from pyquil.api import ConnectionStrategy, ExecutionOptions, RegisterMatrixConversionError, ExecutionOptionsBuilder
-from pyquil.api._qpu import QPU
-from pyquil.api._abstract_compiler import EncryptedProgram
-from pyquil.quil import Program
-from qcs_sdk.qpu.api import Register, ExecutionResult, ExecutionResults
+import pytest
 from qcs_sdk.qpu import MemoryValues
+from qcs_sdk.qpu.api import ExecutionResult, ExecutionResults, Register
 from rpcq.messages import ParameterSpec
 
+from pyquil.api import ConnectionStrategy, ExecutionOptions, ExecutionOptionsBuilder, RegisterMatrixConversionError
+from pyquil.api._abstract_compiler import EncryptedProgram
+from pyquil.api._qpu import QPU
+from pyquil.quil import Program
 from pyquil.quilatom import MemoryReference
 
 
@@ -134,8 +133,7 @@ class TestQPUExecutionOptions:
     def test_submit_with_class_options(
         self, mock_submit: MagicMock, mock_retrieve_results: MagicMock, mock_encrypted_program: EncryptedProgram
     ):
-        """
-        Asserts that a ``QPU``'s execution_options property is used for submission, appears in the returned
+        """Asserts that a ``QPU``'s execution_options property is used for submission, appears in the returned
         ``QPUExecuteResponse``, and is used for retrieval of results when execution options are not provided to
         ``QPU.execute``.
         """
@@ -172,8 +170,7 @@ def test_submit_with_class_options(
     def test_submit_with_options(
         self, mock_submit: MagicMock, mock_retrieve_results: MagicMock, mock_encrypted_program: EncryptedProgram
     ):
-        """
-        Asserts that execution_options provided to ``QPU.execute`` are used for submission, appear in the returned
+        """Asserts that execution_options provided to ``QPU.execute`` are used for submission, appear in the returned
         ``QPUExecuteResponse``, and are used for retrieval of results.
         """
         qpu = QPU(quantum_processor_id="test")
diff --git a/test/unit/test_quantum_computer.py b/test/unit/test_quantum_computer.py
index 361adb6f7..6b29ca790 100644
--- a/test/unit/test_quantum_computer.py
+++ b/test/unit/test_quantum_computer.py
@@ -1,15 +1,18 @@
 import itertools
 import random
-from test.unit.utils import DummyCompiler
 from typing import cast
 
 import networkx as nx
 import numpy as np
 import pytest
 import respx
+from qcs_sdk import QCSClient
+from qcs_sdk.qpu.isa import InstructionSetArchitecture
+from qcs_sdk.qvm import QVMClient
 from syrupy.assertion import SnapshotAssertion
 
 from pyquil import Program, list_quantum_computers
+from pyquil.api._qpu import QPU
 from pyquil.api._quantum_computer import (
     QuantumComputer,
     _check_min_num_trials_for_symmetrized_readout,
@@ -24,7 +27,6 @@
     _symmetrization,
     get_qc,
 )
-from pyquil.api._qpu import QPU
 from pyquil.api._qvm import QVM
 from pyquil.experiment import Experiment, ExperimentSetting
 from pyquil.experiment._main import _pauli_to_product_state
@@ -34,10 +36,7 @@
 from pyquil.pyqvm import PyQVM
 from pyquil.quantum_processor import NxQuantumProcessor
 from pyquil.quilbase import Declare, MemoryReference
-from qcs_sdk import QCSClient
-from qcs_sdk.qvm import QVMClient
-from qcs_sdk.qpu.isa import InstructionSetArchitecture
-from rpcq.messages import ParameterAref
+from test.unit.utils import DummyCompiler
 
 
 def test_flip_array_to_prog(snapshot: SnapshotAssertion):
@@ -828,8 +827,7 @@ def test_qc_expectation_on_qvm(client_configuration: QCSClient, qvm_client: QVMC
 
 
 def test_undeclared_memory_region(client_configuration: QCSClient, qvm_client: QVMClient, dummy_compiler: DummyCompiler):
-    """
-    Test for https://github.com/rigetti/pyquil/issues/1596
+    """Test for https://github.com/rigetti/pyquil/issues/1596
     """
     program = Program(
         """
@@ -852,8 +850,7 @@ def test_undeclared_memory_region(client_configuration: QCSClient, qvm_client: Q
 @pytest.mark.skip  # qcs_sdk client profiles do not support group accounts
 @respx.mock
 def test_get_qc_with_group_account(client_configuration: QCSClient, qvm_client: QVMClient, qcs_aspen8_isa: InstructionSetArchitecture):
-    """
-    Assert that a client may specify a ``QCSClientSettingsProfile`` representing a QCS group
+    """Assert that a client may specify a ``QCSClientSettingsProfile`` representing a QCS group
     account.
     """
     respx.get(
diff --git a/test/unit/test_quil.py b/test/unit/test_quil.py
index da19fb9fb..a7a438fad 100644
--- a/test/unit/test_quil.py
+++ b/test/unit/test_quil.py
@@ -19,78 +19,78 @@
 
 import numpy as np
 import pytest
-from syrupy.assertion import SnapshotAssertion
 import quil.instructions as quil_rs
+from syrupy.assertion import SnapshotAssertion
 
 from pyquil.gates import (
-    I,
-    X,
-    Y,
-    Z,
-    H,
-    T,
-    S,
-    RX,
-    RY,
-    RZ,
-    CNOT,
+    ADD,
+    AND,
     CCNOT,
-    PHASE,
+    CNOT,
+    CONVERT,
+    CPHASE,
     CPHASE00,
     CPHASE01,
     CPHASE10,
-    CPHASE,
-    SWAP,
     CSWAP,
+    DIV,
+    EQ,
+    EXCHANGE,
+    GE,
+    GT,
+    HALT,
+    IOR,
     ISWAP,
-    PSWAP,
+    LE,
+    LOAD,
+    LT,
     MEASURE,
-    HALT,
-    WAIT,
+    MOVE,
+    MUL,
+    NEG,
     NOP,
-    RESET,
     NOT,
-    AND,
-    MOVE,
-    EXCHANGE,
-    LOAD,
-    CONVERT,
+    PHASE,
+    PSWAP,
+    RESET,
+    RX,
+    RY,
+    RZ,
     STORE,
-    XOR,
-    IOR,
-    NEG,
-    ADD,
     SUB,
-    MUL,
-    DIV,
-    EQ,
-    GT,
-    GE,
-    LT,
-    LE,
+    SWAP,
+    WAIT,
+    XOR,
+    H,
+    I,
+    S,
+    T,
+    X,
+    Y,
+    Z,
 )
 from pyquil.paulis import exponential_map, sZ
 from pyquil.quil import (
     Program,
-    merge_programs,
-    merge_with_pauli_noise,
     address_qubits,
     get_classical_addresses_from_program,
+    merge_programs,
+    merge_with_pauli_noise,
     validate_supported_quil,
 )
 from pyquil.quilatom import Frame, MemoryReference, Parameter, QubitPlaceholder, Sub, quil_cos, quil_sin
 from pyquil.quilbase import (
     AbstractInstruction,
-    DefGate,
-    DefFrame,
-    Gate,
-    Qubit,
-    JumpWhen,
     Declare,
     DefCalibration,
+    DefFrame,
+    DefGate,
     DefMeasureCalibration,
     DefPermutationGate,
     DefWaveform,
+    Gate,
+    JumpWhen,
+    Qubit,
 )
 
 
@@ -712,7 +712,7 @@ def test_qubit_placeholder_new():
 
 def test_multiaddress():
     p = Program()
-    q0, q1 = [QubitPlaceholder() for _ in range(2)]
+    q0, q1 = (QubitPlaceholder() for _ in range(2))
     p += exponential_map(sZ(q0) * sZ(q1))(0.5)
 
     map1 = {q0: 0, q1: 1}
@@ -992,11 +992,9 @@ def test_get_classical_addresses_from_program():
 
 
 def test_get_classical_addresses_from_quil_program():
-    """
-    Tests that can get_classical_addresses_from_program can handle both MEASURE
+    """Tests that can get_classical_addresses_from_program can handle both MEASURE
     quil instructions with and without explicit classical registers.
     """
-
     p = Program("\n".join(["X 0", "MEASURE 0"]))
     assert get_classical_addresses_from_program(p) == {}
 
@@ -1118,8 +1116,8 @@ def test_calibrations(self, snapshot: SnapshotAssertion):
 
         calibrations = program.calibrations
         measure_calibrations = program.measure_calibrations
-        assert all((isinstance(cal, DefCalibration) for cal in program.calibrations))
-        assert all((isinstance(cal, DefMeasureCalibration) for cal in program.measure_calibrations))
+        assert all(isinstance(cal, DefCalibration) for cal in program.calibrations)
+        assert all(isinstance(cal, DefMeasureCalibration) for cal in program.measure_calibrations)
         assert calibrations == snapshot
         assert measure_calibrations == snapshot
 
diff --git a/test/unit/test_quilatom.py b/test/unit/test_quilatom.py
index 231ac729d..aa03c278e 100644
--- a/test/unit/test_quilatom.py
+++ b/test/unit/test_quilatom.py
@@ -1,10 +1,11 @@
-from typing import Sequence, Union
+from collections.abc import Sequence
+from typing import Union
 
+import numpy as np
 import pytest
 from syrupy.assertion import SnapshotAssertion
-import numpy as np
 
-from pyquil.quilatom import Add, FormalArgument, Frame, Qubit, Label, LabelPlaceholder, QubitPlaceholder, Parameter
+from pyquil.quilatom import Add, FormalArgument, Frame, Label, LabelPlaceholder, Parameter, Qubit, QubitPlaceholder
 
 
 @pytest.mark.parametrize(
diff --git a/test/unit/test_quilbase.py b/test/unit/test_quilbase.py
index 61f404cc0..099b8bf97 100644
--- a/test/unit/test_quilbase.py
+++ b/test/unit/test_quilbase.py
@@ -1,89 +1,100 @@
 import copy
+from collections.abc import Iterable
 from math import pi
-from typing import Any, List, Optional, Iterable, Tuple, Union
 from numbers import Complex, Number
+from typing import Any, List, Optional, Tuple, Union
 
 import numpy as np
 import pytest
 from syrupy.assertion import SnapshotAssertion
 
-from pyquil.quilatom import quil_cos, quil_sin
+from pyquil.api._compiler import QPUCompiler
 from pyquil.gates import X
+from pyquil.paulis import PauliSum, PauliTerm
 from pyquil.quil import Program
+from pyquil.quilatom import (
+    BinaryExp,
+    Expression,
+    Frame,
+    Mul,
+    Qubit,
+    TemplateWaveform,
+    Waveform,
+    WaveformReference,
+    quil_cos,
+    quil_sin,
+)
 from pyquil.quilbase import (
-    _convert_to_rs_instruction,
-    _convert_to_py_instruction,
     AbstractInstruction,
     ArithmeticBinaryOp,
     Capture,
     ClassicalAdd,
     ClassicalAnd,
-    ClassicalExclusiveOr,
-    ClassicalInclusiveOr,
-    ClassicalSub,
-    ClassicalMul,
-    ClassicalDiv,
     ClassicalComparison,
     ClassicalConvert,
+    ClassicalDiv,
+    ClassicalEqual,
     ClassicalExchange,
+    ClassicalExclusiveOr,
+    ClassicalGreaterEqual,
+    ClassicalGreaterThan,
+    ClassicalInclusiveOr,
+    ClassicalLessEqual,
+    ClassicalLessThan,
     ClassicalLoad,
     ClassicalMove,
+    ClassicalMul,
     ClassicalNeg,
     ClassicalNot,
     ClassicalStore,
-    ClassicalEqual,
-    ClassicalLessThan,
-    ClassicalLessEqual,
-    ClassicalGreaterThan,
-    ClassicalGreaterEqual,
+    ClassicalSub,
     Declare,
     DefCalibration,
     DefCircuit,
     DefFrame,
     DefGate,
-    DefWaveform,
-    DefPermutationGate,
     DefGateByPaulis,
     DefMeasureCalibration,
+    DefPermutationGate,
+    DefWaveform,
     DelayFrames,
     DelayQubits,
     Fence,
     FenceAll,
     FormalArgument,
     Gate,
+    Halt,
     Include,
     LogicalBinaryOp,
     Measurement,
     MemoryReference,
-    ParameterDesignator,
+    Nop,
     Parameter,
+    ParameterDesignator,
     Pragma,
     Pulse,
     QubitDesignator,
     RawCapture,
+    Reset,
+    ResetQubit,
     SetFrequency,
     SetPhase,
     SetScale,
     ShiftFrequency,
     ShiftPhase,
     SwapPhases,
-    Reset,
-    ResetQubit,
     UnaryClassicalInstruction,
     Wait,
-    Halt,
-    Nop,
+    _convert_to_py_instruction,
+    _convert_to_rs_instruction,
 )
-from pyquil.paulis import PauliSum, PauliTerm
-from pyquil.quilatom import BinaryExp, Mul, Frame, Qubit, Expression, Waveform, WaveformReference, TemplateWaveform
-from pyquil.api._compiler import QPUCompiler
 from pyquil.quiltwaveforms import (
+    BoxcarAveragerKernel,
+    DragGaussianWaveform,
+    ErfSquareWaveform,
     FlatWaveform,
     GaussianWaveform,
-    DragGaussianWaveform,
     HrmGaussianWaveform,
-    ErfSquareWaveform,
-    BoxcarAveragerKernel,
 )
 
 
diff --git a/test/unit/test_quilt.py b/test/unit/test_quilt.py
index a3d4b46c3..029b6534e 100644
--- a/test/unit/test_quilt.py
+++ b/test/unit/test_quilt.py
@@ -1,41 +1,36 @@
 import os
 from typing import Optional
 
-import pytest
 import numpy as np
+import pytest
 from syrupy.assertion import SnapshotAssertion
 
-from pyquil.quil import Program
 from pyquil.gates import RX
+from pyquil.quil import Program
 from pyquil.quilatom import (
+    FormalArgument,
     MemoryReference,
     Parameter,
-    FormalArgument,
-    Frame,
-)
-from pyquil.quiltwaveforms import (
-    FlatWaveform,
-    GaussianWaveform,
-    DragGaussianWaveform,
-    ErfSquareWaveform,
-    HrmGaussianWaveform,
-    BoxcarAveragerKernel,
-)
-from pyquil.quiltcalibrations import (
-    CalibrationError,
-    CalibrationMatch,
-    match_calibration,
 )
 from pyquil.quilbase import (
     AbstractInstruction,
-    Gate,
     DefCalibration,
     DefMeasureCalibration,
-    ShiftPhase,
-    DelayQubits,
-    Pulse,
+    Gate,
     Qubit,
 )
+from pyquil.quiltcalibrations import (
+    CalibrationMatch,
+    match_calibration,
+)
+from pyquil.quiltwaveforms import (
+    BoxcarAveragerKernel,
+    DragGaussianWaveform,
+    ErfSquareWaveform,
+    FlatWaveform,
+    GaussianWaveform,
+    HrmGaussianWaveform,
+)
 from test.unit.conftest import TEST_DATA_DIR
 
 
diff --git a/test/unit/test_qvm.py b/test/unit/test_qvm.py
index c4284b19d..2b4f00adb 100644
--- a/test/unit/test_qvm.py
+++ b/test/unit/test_qvm.py
@@ -1,7 +1,6 @@
 import numpy as np
 import pytest
-
-from qcs_sdk.qvm import QVMError, QVMClient
+from qcs_sdk.qvm import QVMClient, QVMError
 
 from pyquil import Program
 from pyquil.api import QVM
diff --git a/test/unit/test_reference_density.py b/test/unit/test_reference_density.py
index 780dbcd92..265f3d335 100644
--- a/test/unit/test_reference_density.py
+++ b/test/unit/test_reference_density.py
@@ -4,10 +4,9 @@
 
 import pyquil.simulation.matrices as qmats
 from pyquil import Program
-from pyquil.api import QCSClient
-from pyquil.api import QuantumComputer
-from pyquil.experiment import ExperimentSetting, Experiment, zeros_state
-from pyquil.gates import CNOT, H, I, MEASURE, PHASE, RX, RY, RZ, X
+from pyquil.api import QCSClient, QuantumComputer
+from pyquil.experiment import Experiment, ExperimentSetting, zeros_state
+from pyquil.gates import CNOT, MEASURE, PHASE, RX, RY, RZ, H, I, X
 from pyquil.operator_estimation import measure_observables
 from pyquil.paulis import sI, sX, sY, sZ
 from pyquil.pyqvm import PyQVM
diff --git a/test/unit/test_reference_wavefunction.py b/test/unit/test_reference_wavefunction.py
index 4a31cb314..938d95bc1 100644
--- a/test/unit/test_reference_wavefunction.py
+++ b/test/unit/test_reference_wavefunction.py
@@ -11,21 +11,21 @@
 from pyquil.gates import (
     CNOT,
     CPHASE,
-    H,
     HALT,
-    I,
     MEASURE,
     MOVE,
     PHASE,
+    QUANTUM_GATES,
     RESET,
     RX,
     RY,
     RZ,
     SWAP,
+    H,
+    I,
     X,
-    QUANTUM_GATES,
 )
-from pyquil.paulis import PauliTerm, exponentiate, sZ, sX, sI, sY
+from pyquil.paulis import PauliTerm, exponentiate, sI, sX, sY, sZ
 from pyquil.pyqvm import PyQVM
 from pyquil.quil import Program
 from pyquil.quilatom import MemoryReference
@@ -898,7 +898,7 @@ def _generate_random_program(n_qubits, length, include_measures=False):
             elif param == "angle":
                 param_val = random.uniform(-2 * pi, 2 * pi)
             else:
-                raise ValueError("Unknown gate parameter {}".format(param))
+                raise ValueError(f"Unknown gate parameter {param}")
 
             param_vals.append(param_val)
 
diff --git a/test/unit/test_result.py b/test/unit/test_result.py
index 35886499f..1f5155597 100644
--- a/test/unit/test_result.py
+++ b/test/unit/test_result.py
@@ -2,11 +2,11 @@
 
 from pyquil.experiment import plusX
 from pyquil.experiment._result import (
+    ExperimentResult,
+    ExperimentSetting,
     bitstrings_to_expectations,
     correct_experiment_result,
     ratio_variance,
-    ExperimentResult,
-    ExperimentSetting,
 )
 from pyquil.paulis import sZ
 
diff --git a/test/unit/test_rewrite_arithmetic.py b/test/unit/test_rewrite_arithmetic.py
deleted file mode 100644
index 0e803c410..000000000
--- a/test/unit/test_rewrite_arithmetic.py
+++ /dev/null
@@ -1,154 +0,0 @@
-from pyquil.quil import Program
-from pyquil.quilatom import Qubit, Frame
-from pyquil.quilbase import DefFrame
-from pyquil.api._rewrite_arithmetic import rewrite_arithmetic
-from rpcq.messages import (
-    ParameterAref,
-    ParameterSpec,
-    RewriteArithmeticResponse,
-)
-
-
-def test_rewrite_arithmetic_no_params():
-    prog = Program("X 0")
-    response = rewrite_arithmetic(prog)
-    assert response == RewriteArithmeticResponse(quil=Program("X 0").out())
-
-
-def test_rewrite_arithmetic_simple_mref():
-    prog = Program("DECLARE theta REAL", "RZ(theta) 0")
-    response = rewrite_arithmetic(prog)
-    assert response == RewriteArithmeticResponse(
-        original_memory_descriptors={"theta": ParameterSpec(length=1, type="REAL")},
-        quil=Program("DECLARE __P1 REAL[1]", "DECLARE theta REAL[1]", "RZ(__P1[0]) 0").out(),
-        recalculation_table={ParameterAref(index=0, name="__P1"): "theta[0]/(2*pi)"},
-    )
-
-
-def test_rewrite_arithmetic_duplicate_exprs():
-    prog = Program(
-        "DECLARE theta REAL",
-        "RZ(theta*1.5) 0",
-        "RX(theta*1.5) 0",  # this is not a native gate, but it is a protoquil program
-    )
-
-    response = rewrite_arithmetic(prog)
-
-    assert response == RewriteArithmeticResponse(
-        original_memory_descriptors={"theta": ParameterSpec(length=1, type="REAL")},
-        recalculation_table={ParameterAref(index=0, name="__P1"): "theta[0]*(1.5)/(2*pi)"},
-        quil=Program("DECLARE __P1 REAL[1]", "DECLARE theta REAL[1]", "RZ(__P1[0]) 0", "RX(__P1[0]) 0").out(),
-    )
-
-
-def test_rewrite_arithmetic_mixed():
-    prog = Program(
-        "DECLARE theta REAL",
-        "DECLARE beta REAL",
-        "RZ(3 * theta) 0",
-        "RZ(beta+theta) 0",
-    )
-    response = rewrite_arithmetic(prog)
-    assert response.original_memory_descriptors == {
-        "theta": ParameterSpec(length=1, type="REAL"),
-        "beta": ParameterSpec(length=1, type="REAL"),
-    }
-    assert response.recalculation_table == {
-        ParameterAref(index=0, name="__P2"): "3.0*theta[0]/(2*pi)",
-        ParameterAref(index=1, name="__P2"): "(beta[0] + theta[0])/(2*pi)",
-    }
-    assert (
-        Program(response.quil) == Program(
-            "DECLARE __P2 REAL[2]",
-            "DECLARE theta REAL[1]",
-            "DECLARE beta REAL[1]",
-            "RZ(__P2[0]) 0",
-            "RZ(__P2[1]) 0",
-        )
-    )
-
-
-def test_rewrite_arithmetic_set_scale():
-    prog = Program(
-        "DECLARE theta REAL",
-        'SET-SCALE 0 "rf" 1.0',
-        'SET-SCALE 0 "rf" theta',
-    )
-
-    response = rewrite_arithmetic(prog)
-
-    assert response == RewriteArithmeticResponse(
-        original_memory_descriptors={"theta": ParameterSpec(length=1, type="REAL")},
-        recalculation_table={ParameterAref(index=0, name="__P1"): "theta[0]/8"},
-        quil=Program(
-            "DECLARE __P1 REAL[1]",
-            "DECLARE theta REAL[1]",
-            'SET-SCALE 0 "rf" 1.0',
-            'SET-SCALE 0 "rf" __P1[0]',
-        ).out(),
-    )
-
-
-def test_rewrite_arithmetic_frequency():
-    fdefn0 = DefFrame(
-        frame=Frame([Qubit(0)], "rf"),
-        center_frequency=10.0,
-        sample_rate=20.0,
-    )
-    fdefn1 = DefFrame(
-        frame=Frame([Qubit(1)], "rf"),
-        sample_rate=20.0,
-    )
-    prog = Program(
-        fdefn0,
-        fdefn1,
-        "DECLARE theta REAL",
-        'SET-FREQUENCY 0 "rf" theta',
-        'SHIFT-FREQUENCY 0 "rf" theta',
-        'SET-FREQUENCY 1 "rf" theta',
-    )
-
-    response = rewrite_arithmetic(prog)
-
-    assert response.original_memory_descriptors == {"theta": ParameterSpec(length=1, type="REAL")}
-    assert response.recalculation_table == {
-        ParameterAref(index=0, name="__P1"): "(theta[0] - 10.0)/20.0",
-        ParameterAref(index=1, name="__P1"): "theta[0]/20.0",
-    }
-
-    response_program = Program(response.quil)
-    assert {response_program[0].out(), response_program[1].out()} == {"DECLARE __P1 REAL[2]", "DECLARE theta REAL[1]"}
-    assert response_program.frames[fdefn0.frame] == fdefn0
-    assert response_program.frames[fdefn1.frame] == fdefn1
-
-
-def test_rewrite_arithmetic_mixed_mutations():
-    fdefn = DefFrame(
-        frame=Frame([Qubit(0)], "rf"),
-        center_frequency=10.0,
-        sample_rate=20.0,
-    )
-    prog = Program(
-        fdefn,
-        "DECLARE theta REAL",
-        'SET-FREQUENCY 0 "rf" theta',
-        'SET-PHASE 0 "rf" theta',
-        'SET-SCALE 0 "rf" theta',
-    )
-
-    response = rewrite_arithmetic(prog)
-    assert response.original_memory_descriptors == {"theta": ParameterSpec(length=1, type="REAL")}
-    assert response.recalculation_table == {
-        ParameterAref(index=0, name="__P1"): "(theta[0] - 10.0)/20.0",
-        ParameterAref(index=1, name="__P1"): "theta[0]/(2*pi)",
-        ParameterAref(index=2, name="__P1"): "theta[0]/8",
-    }
-
-    response_program = Program(response.quil)
-    assert set(response_program[0:2].out().split("\n")) == {"DECLARE __P1 REAL[3]", "DECLARE theta REAL[1]", ""}
-    assert response_program.frames[fdefn.frame] == fdefn
-    assert [inst.out() for inst in response_program[2:]] == [
-        'SET-FREQUENCY 0 "rf" __P1[0]',
-        'SET-PHASE 0 "rf" __P1[1]',
-        'SET-SCALE 0 "rf" __P1[2]',
-    ]
diff --git a/test/unit/test_rpcq.py b/test/unit/test_rpcq.py
index 54ee2ef4e..3610c2fd5 100644
--- a/test/unit/test_rpcq.py
+++ b/test/unit/test_rpcq.py
@@ -1,8 +1,8 @@
-from pyquil.external.rpcq import Qubit, Edge, GateInfo, Supported1QGate, Supported2QGate
+from pyquil.external.rpcq import Edge, GateInfo, Qubit, Supported1QGate, Supported2QGate
 
 
 def test_qubit_dead_serialization():
-    "test that the qubit is marked dead if it has no gates"
+    """Test that the qubit is marked dead if it has no gates"""
     gate_info = GateInfo(
         operator=Supported1QGate.RX,
         parameters=[0.0],
@@ -25,7 +25,7 @@ def test_qubit_dead_serialization():
 
 
 def test_edge_dead_serialization():
-    "test that the edge is marked dead if it has no gates"
+    """Test that the edge is marked dead if it has no gates"""
     gate_info = GateInfo(
         operator=Supported2QGate.CZ,
         parameters=[],
diff --git a/test/unit/test_setting.py b/test/unit/test_setting.py
index d893eb9e9..d0d75c2ea 100644
--- a/test/unit/test_setting.py
+++ b/test/unit/test_setting.py
@@ -2,15 +2,14 @@
 from operator import mul
 
 import numpy as np
-import pytest
 
 from pyquil.experiment._main import _pauli_to_product_state
 from pyquil.experiment._setting import (
-    ExperimentSetting,
     SIC0,
     SIC1,
     SIC2,
     SIC3,
+    ExperimentSetting,
     TensorProductState,
     minusX,
     minusY,
diff --git a/test/unit/test_tools.py b/test/unit/test_tools.py
index 3ddf80849..8d5da2b3e 100644
--- a/test/unit/test_tools.py
+++ b/test/unit/test_tools.py
@@ -2,19 +2,19 @@
 import pytest
 
 from pyquil import Program
-from pyquil.experiment import plusX, minusZ
-from pyquil.gates import CCNOT, CNOT, CZ, H, MEASURE, PHASE, RX, RY, RZ, X, Y, Z
+from pyquil.experiment import minusZ, plusX
+from pyquil.gates import CCNOT, CNOT, CZ, MEASURE, PHASE, RX, RY, RZ, H, X, Y, Z
 from pyquil.paulis import sX, sY, sZ
 from pyquil.quilatom import MemoryReference, Parameter
 from pyquil.quilbase import Declare
 from pyquil.simulation import matrices as mat
 from pyquil.simulation.tools import (
-    qubit_adjacent_lifted_gate,
-    program_unitary,
-    lifted_gate_matrix,
     lifted_gate,
+    lifted_gate_matrix,
     lifted_pauli,
     lifted_state_operator,
+    program_unitary,
+    qubit_adjacent_lifted_gate,
 )
 
 
diff --git a/test/unit/test_wavefunction.py b/test/unit/test_wavefunction.py
index df859d2cc..c32f1be8d 100644
--- a/test/unit/test_wavefunction.py
+++ b/test/unit/test_wavefunction.py
@@ -1,11 +1,12 @@
-import pytest
-import numpy as np
 import itertools
 
+import numpy as np
+import pytest
+
 from pyquil.wavefunction import (
-    get_bitstring_from_index,
     Wavefunction,
     _octet_bits,
+    get_bitstring_from_index,
 )
 
 
diff --git a/test/unit/test_wavefunction_simulator.py b/test/unit/test_wavefunction_simulator.py
index 119f929b7..ab6fd3248 100644
--- a/test/unit/test_wavefunction_simulator.py
+++ b/test/unit/test_wavefunction_simulator.py
@@ -2,10 +2,9 @@
 import pytest
 
 from pyquil import Program
-from pyquil.api import WavefunctionSimulator
-from pyquil.api import QCSClient
-from pyquil.gates import H, CNOT
-from pyquil.paulis import PauliSum, sZ, sX
+from pyquil.api import QCSClient, WavefunctionSimulator
+from pyquil.gates import CNOT, H
+from pyquil.paulis import PauliSum, sX, sZ
 
 
 def test_wavefunction(client_configuration: QCSClient):
diff --git a/test/unit/utils.py b/test/unit/utils.py
index 071feff22..468c69d38 100644
--- a/test/unit/utils.py
+++ b/test/unit/utils.py
@@ -22,7 +22,6 @@ def patch_rpcq_client(*, mocker: MockerFixture, return_value: Any) -> MagicMock:
     :param return_value: The response that should come back from calling client.call()
     :return the instance of MagicMock standing in for rpc.Client that other functions will use.
     """
-
     client = mocker.MagicMock()
     client.call.return_value = return_value
 
@@ -55,3 +54,6 @@ def quil_to_native_quil(self, program: Program, *, protoquil=None):
 
     def native_quil_to_executable(self, nq_program: Program):
         return nq_program
+
+    def reset(self):
+        pass