Repository: apytypes/apytypes (language: python)

Issue:
[pre-commit.ci] pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/igorshubovych/markdownlint-cli: v0.40.0 → v0.41.0](https://github.com/igorshubovych/markdownlint-cli/compare/v0.40.0...v0.41.0)
<!--pre-commit.ci end-->


Discussion / hints:
## [Codecov](https://app.codecov.io/gh/apytypes/apytypes/pull/453?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apytypes) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.58%. Comparing base [(`ccbf8be`)](https://app.codecov.io/gh/apytypes/apytypes/commit/ccbf8be4a3d53d763f1bcfb99fa768e15bfc3157?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apytypes) to head [(`b5c461f`)](https://app.codecov.io/gh/apytypes/apytypes/commit/b5c461fc5d4fe1876e069c2b6ebe7f5d3321bf63?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apytypes).
> Report is 1 commits behind head on main.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #453      +/-   ##
==========================================
+ Coverage   83.56%   83.58%   +0.01%     
==========================================
  Files          55       55              
  Lines        9987     9995       +8     
  Branches     2019     2021       +2     
==========================================
+ Hits         8346     8354       +8     
  Misses        287      287              
  Partials     1354     1354              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apytypes/apytypes/pull/453?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apytypes).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apytypes).


Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 10ef7465c..4be0ea28e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -18,7 +18,7 @@ repos:
     hooks:
     - id: clang-format
   - repo: https://github.com/igorshubovych/markdownlint-cli
-    rev: v0.40.0
+    rev: v0.41.0
     hooks:
     - id: markdownlint-fix
   - repo: https://github.com/pre-commit/pygrep-hooks
@@ -30,7 +30,11 @@ repos:
     - id: rst-directive-colons
     - id: rst-inline-touching-normal
     - id: text-unicode-replacement-char
-  - repo: https://github.com/psf/black
-    rev: 24.4.2
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    # Ruff version.
+    rev: v0.4.7
     hooks:
-    - id: black
+      # Run the linter.
+      - id: ruff
+      # Run the formatter.
+      - id: ruff-format
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 962308f61..4d6c538fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,11 +17,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   - Automatic broadcasting on array arithmetic
 - Support arithmetic between APyTypes classes and Python's built-in floats and ints.
 - Support for automatic stub generation added.
+- Docstrings are made compatible with
+  [doctest](https://docs.python.org/3/library/doctest.html), which is
+  run in the CI.
+- Linting using [ruff](https://docs.astral.sh/ruff/) is added to pre-commit and CI.
 
 ### Changed
 
 - Updated [nanobind](https://github.com/wjakob/nanobind) dependency from v1.9.2 to
   v2.0.0.
+- [ruff](https://docs.astral.sh/ruff/) is used instead of black to format the code.
 
 ### Fixed
 
diff --git a/benchmark/ci_fixed_array_benchmarks.py b/benchmark/ci_fixed_array_benchmarks.py
index b926e1951..7b4a51b10 100644
--- a/benchmark/ci_fixed_array_benchmarks.py
+++ b/benchmark/ci_fixed_array_benchmarks.py
@@ -1,6 +1,5 @@
 from apytypes import APyFixedArray, APyFixed
 import numpy as np
-import pytest
 
 
 def test_fixed_matrix_multiplication_20(benchmark):
diff --git a/benchmark/ci_fixed_scalar_benchmarks.py b/benchmark/ci_fixed_scalar_benchmarks.py
index 3ec8e9bea..0915392f6 100644
--- a/benchmark/ci_fixed_scalar_benchmarks.py
+++ b/benchmark/ci_fixed_scalar_benchmarks.py
@@ -1,6 +1,5 @@
 from apytypes import APyFixed
 import numpy as np
-import pytest
 
 
 def test_fixed_scalar_multiplication_short(benchmark):
diff --git a/benchmark/ci_float_array_benchmarks.py b/benchmark/ci_float_array_benchmarks.py
index 43631ac5f..f4cfed19e 100644
--- a/benchmark/ci_float_array_benchmarks.py
+++ b/benchmark/ci_float_array_benchmarks.py
@@ -1,6 +1,5 @@
 from apytypes import APyFloatArray, APyFloat
 import numpy as np
-import pytest
 
 
 def test_float_matrix_multiplication_20(benchmark):
diff --git a/benchmark/ci_float_scalar_benchmarks.py b/benchmark/ci_float_scalar_benchmarks.py
index c3baf350b..ead3765e6 100644
--- a/benchmark/ci_float_scalar_benchmarks.py
+++ b/benchmark/ci_float_scalar_benchmarks.py
@@ -1,6 +1,5 @@
 from apytypes import APyFloat
 import numpy as np
-import pytest
 
 
 def test_float_scalar_multiplication_short(benchmark):
diff --git a/comparison/comparison.py b/comparison/comparison.py
index af01bc688..5551756f4 100644
--- a/comparison/comparison.py
+++ b/comparison/comparison.py
@@ -1,5 +1,6 @@
-import importlib
 import timeit
+import matplotlib.pyplot as plt
+import numpy as np
 
 libraries = {
     "APyTypes": "compare_apytypes",
@@ -72,10 +73,6 @@
             print(f"{lib_name} {name}: Error")
 
 # %%
-import matplotlib.pyplot as plt
-import numpy as np
-from matplotlib.ticker import FormatStrFormatter
-
 fig, ax = plt.subplots(layout="constrained", figsize=(8, 10))
 
 x = np.arange(len(benchmarks))  # the label locations
diff --git a/docs/_plots/innerproductscale.py b/docs/_plots/innerproductscale.py
index 6650afc9c..1ebf1261a 100644
--- a/docs/_plots/innerproductscale.py
+++ b/docs/_plots/innerproductscale.py
@@ -1,5 +1,6 @@
 # %%
 import timeit
+import matplotlib.pyplot as plt
 
 CREATE_TWO_FIXED_15_VECTORS = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}) - 0.5, 1, 14); b = APyFixedArray.from_float(np.random.rand({l}) - 0.5, 1, 14)"
 CREATE_TWO_FIXED_31_VECTORS = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}) - 0.5, 1, 30); b = APyFixedArray.from_float(np.random.rand({l}) - 0.5, 1, 30)"
@@ -66,17 +67,13 @@
 for name, (func, setup) in benchmarks.items():
     results[name] = []
     for count in counts:
-        setup_tmp = (
-            f"from apytypes import APyFixedArray, APyFloatArray;"
-            + setup.format(l=count)
+        setup_tmp = "from apytypes import APyFixedArray, APyFloatArray;" + setup.format(
+            l=count
         )
         num, timing = timeit.Timer(stmt=func, setup=setup_tmp).autorange()
         results[name].append(num * count / timing)
 
 # %%
-import matplotlib.pyplot as plt
-import numpy as np
-from matplotlib.ticker import FormatStrFormatter
 
 fig, ax = plt.subplots(layout="constrained", figsize=(8, 8))
 
diff --git a/docs/_plots/matrixmultscale.py b/docs/_plots/matrixmultscale.py
index f9869904a..63516d6a4 100644
--- a/docs/_plots/matrixmultscale.py
+++ b/docs/_plots/matrixmultscale.py
@@ -1,5 +1,6 @@
 # %%
 import timeit
+import matplotlib.pyplot as plt
 
 CREATE_TWO_FIXED_15_MATRICES = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}, {l}) - 0.5, 1, 14); b = APyFixedArray.from_float(np.random.rand({l}, {l}) - 0.5, 1, 14)"
 CREATE_TWO_FIXED_31_MATRICES = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}, {l}) - 0.5, 1, 30); b = APyFixedArray.from_float(np.random.rand({l}, {l}) - 0.5, 1, 30)"
@@ -67,17 +68,13 @@
 for name, (func, setup) in benchmarks.items():
     results[name] = []
     for count in counts:
-        setup_tmp = (
-            f"from apytypes import APyFixedArray, APyFloatArray;"
-            + setup.format(l=count)
+        setup_tmp = "from apytypes import APyFixedArray, APyFloatArray;" + setup.format(
+            l=count
         )
         num, timing = timeit.Timer(stmt=func, setup=setup_tmp).autorange()
         results[name].append(num * (count**3) / timing)
 
 # %%
-import matplotlib.pyplot as plt
-import numpy as np
-from matplotlib.ticker import FormatStrFormatter
 
 fig, ax = plt.subplots(layout="constrained", figsize=(8, 8))
 
diff --git a/docs/_plots/numpyscale.py b/docs/_plots/numpyscale.py
index 12e046274..cc2b1e0e1 100644
--- a/docs/_plots/numpyscale.py
+++ b/docs/_plots/numpyscale.py
@@ -1,5 +1,7 @@
 # %%
 import timeit
+import matplotlib.pyplot as plt
+import numpy as np
 
 CREATE_TWO_FIXED_15_VECTORS = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}) - 0.5, 1, 14); b = APyFixedArray.from_float(np.random.rand({l}) - 0.5, 1, 14)"
 CREATE_TWO_FIXED_31_VECTORS = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}) - 0.5, 1, 30); b = APyFixedArray.from_float(np.random.rand({l}) - 0.5, 1, 20)"
@@ -150,17 +152,13 @@
 for name, (func, setup) in benchmarks.items():
     results[name] = []
     for count in counts:
-        setup_tmp = (
-            f"from apytypes import APyFixedArray, APyFloatArray;"
-            + setup.format(l=count)
+        setup_tmp = "from apytypes import APyFixedArray, APyFloatArray;" + setup.format(
+            l=count
         )
         num, timing = timeit.Timer(stmt=func, setup=setup_tmp).autorange()
         results[name].append(num * count / timing)
 
 # %%
-import matplotlib.pyplot as plt
-import numpy as np
-from matplotlib.ticker import FormatStrFormatter
 
 fig, ax = plt.subplots(3, 1, layout="constrained", figsize=(8, 16))
 
@@ -171,7 +169,9 @@
     axes_no = (
         0
         if "oating" in name and ("dition" in name or "sub" in name)
-        else 1 if "oating" in name and "location" else 2
+        else 1
+        if "oating" in name and "location"
+        else 2
     )
     ax[axes_no].plot(
         counts,
diff --git a/docs/_plots/overflow.py b/docs/_plots/overflow.py
index e2fff846f..14d71b56b 100644
--- a/docs/_plots/overflow.py
+++ b/docs/_plots/overflow.py
@@ -1,4 +1,4 @@
-from apytypes import APyFixedArray, OverflowMode
+from apytypes import APyFixedArray, OverflowMode  # noqa: F401
 import numpy as np
 import matplotlib.pyplot as plt
 from matplotlib.ticker import IndexLocator
diff --git a/docs/_plots/performancescale.py b/docs/_plots/performancescale.py
index 265bcf657..eb458a47d 100644
--- a/docs/_plots/performancescale.py
+++ b/docs/_plots/performancescale.py
@@ -1,5 +1,7 @@
 # %%
 import timeit
+import matplotlib.pyplot as plt
+import numpy as np
 
 CREATE_TWO_FIXED_VECTORS = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}) - 0.5, int_bits=10, frac_bits=10); b = APyFixedArray.from_float(np.random.rand({l}) - 0.5, int_bits=10, frac_bits=10)"
 CREATE_TWO_LONG_FIXED_VECTORS = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}) - 0.5, int_bits=150, frac_bits=150); b = APyFixedArray.from_float(np.random.rand({l}) - 0.5, int_bits=150, frac_bits=150)"
@@ -29,17 +31,13 @@
 for name, (func, setup) in benchmarks.items():
     results[name] = []
     for count in counts:
-        setup_tmp = (
-            f"from apytypes import APyFixedArray, APyFloatArray;"
-            + setup.format(l=count)
+        setup_tmp = "from apytypes import APyFixedArray, APyFloatArray;" + setup.format(
+            l=count
         )
         num, timing = timeit.Timer(stmt=func, setup=setup_tmp).autorange()
         results[name].append(num * count / timing)
 
 # %%
-import matplotlib.pyplot as plt
-import numpy as np
-from matplotlib.ticker import FormatStrFormatter
 
 fig, ax = plt.subplots(layout="constrained", figsize=(8, 6.5))
 
diff --git a/docs/_plots/quantization.py b/docs/_plots/quantization.py
index 11b2c66c0..e3547e70c 100644
--- a/docs/_plots/quantization.py
+++ b/docs/_plots/quantization.py
@@ -1,6 +1,7 @@
-from apytypes import APyFixedArray, QuantizationMode
+from apytypes import APyFixedArray, QuantizationMode  # noqa: F401
 import numpy as np
 import matplotlib.pyplot as plt
+import matplotlib.gridspec as gridspec
 from matplotlib.ticker import IndexLocator
 
 modes = [
@@ -72,8 +73,6 @@
 
 
 # %%
-import matplotlib.gridspec as gridspec
-
 a = np.arange(-4, 3.9, 0.125)
 ai = np.arange(-4, 3.1, 1)
 ah = np.arange(-3.5, 3.6, 1)
@@ -82,7 +81,7 @@
 aifx = APyFixedArray.from_float(ai, int_bits=4, frac_bits=3)
 ahfx = APyFixedArray.from_float(ah, int_bits=4, frac_bits=3)
 
-l = len(a) // 2
+half = len(a) // 2
 for q in modes:
     fig = plt.figure(figsize=(8, 4), layout="constrained")
     name = q.split(".")[1]
@@ -119,8 +118,10 @@
 
     ax = fig.add_subplot(gs[1, 1])
     err = a - aqfx
-    ax.hist(err[:l], bins, align="left", weights=1 / l * np.ones(l), rwidth=0.8)
-    ax.axvline(float(sum(err[:l])) / l, color="r")
+    ax.hist(
+        err[:half], bins, align="left", weights=1 / half * np.ones(half), rwidth=0.8
+    )
+    ax.axvline(float(sum(err[:half])) / half, color="r")
     ax.grid(True)
     ax.set_xlim(-1.1, 1.1)
     ax.set_ylim(0, 0.14)
@@ -129,8 +130,10 @@
 
     ax = fig.add_subplot(gs[2, 1])
     err = a - aqfx
-    ax.hist(err[l:], bins, align="left", weights=1 / l * np.ones(l), rwidth=0.8)
-    ax.axvline(float(sum(err[l:])) / l, color="r")
+    ax.hist(
+        err[half:], bins, align="left", weights=1 / half * np.ones(half), rwidth=0.8
+    )
+    ax.axvline(float(sum(err[half:])) / half, color="r")
     ax.grid(True)
     ax.set_xlim(-1.1, 1.1)
     ax.set_ylim(0, 0.14)
diff --git a/docs/_plots/transposescale.py b/docs/_plots/transposescale.py
index f206bddc0..f87856ce5 100644
--- a/docs/_plots/transposescale.py
+++ b/docs/_plots/transposescale.py
@@ -1,5 +1,6 @@
 # %%
 import timeit
+import matplotlib.pyplot as plt
 
 CREATE_TWO_FIXED_15_MATRICES = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}, {l}) - 0.5, 1, 14); b = APyFixedArray.from_float(np.random.rand({l}, {l}) - 0.5, 1, 14)"
 CREATE_TWO_FIXED_31_MATRICES = "import numpy as np; a = APyFixedArray.from_float(np.random.rand({l}, {l}) - 0.5, 1, 30); b = APyFixedArray.from_float(np.random.rand({l}, {l}) - 0.5, 1, 30)"
@@ -51,18 +52,13 @@
 for name, (func, setup) in benchmarks.items():
     results[name] = []
     for count in counts:
-        setup_tmp = (
-            f"from apytypes import APyFixedArray, APyFloatArray;"
-            + setup.format(l=count)
+        setup_tmp = "from apytypes import APyFixedArray, APyFloatArray;" + setup.format(
+            l=count
         )
         num, timing = timeit.Timer(stmt=func, setup=setup_tmp).autorange()
         results[name].append(num * (count**3) / timing)
 
 # %%
-import matplotlib.pyplot as plt
-import numpy as np
-from matplotlib.ticker import FormatStrFormatter
-
 fig, ax = plt.subplots(layout="constrained", figsize=(8, 8))
 
 
diff --git a/lib/apytypes/__init__.py b/lib/apytypes/__init__.py
index 50d9bfd25..1169f0a36 100644
--- a/lib/apytypes/__init__.py
+++ b/lib/apytypes/__init__.py
@@ -65,18 +65,24 @@
 If the operands do not share the same format, the resulting
 bit widths of the exponent and mantissa field will be the maximum of its inputs:
 
-.. code-block:: Python
+Examples
+--------
 
-    from apytypes import APyFloat
-    a = APyFixed.from_float(1.25, exp_bits=5, man_bits=2)
-    b = APyFixed.from_float(1.75, exp_bits=5, man_bits=2)
-    # Operands with same format, result will have exp_bits=5, man_bits=2
-    c = a + b
+>>> from apytypes import APyFloat
+>>> a = APyFloat.from_float(1.25, exp_bits=5, man_bits=2)
+>>> b = APyFloat.from_float(1.75, exp_bits=5, man_bits=2)
 
-    d = APyFixed.from_float(1.75, exp_bits=4, man_bits=4)
-    # Operands with different formats, result will have exp_bits=5, man_bits=4
-    e = a + d
+Operands with same format, result will have exp_bits=5, man_bits=2
 
+>>> a + b
+APyFloat(sign=0, exp=16, man=2, exp_bits=5, man_bits=2)
+
+>>> d = APyFloat.from_float(1.75, exp_bits=4, man_bits=4)
+
+Operands with different formats, result will have exp_bits=5, man_bits=4
+
+>>> a + d
+APyFloat(sign=0, exp=16, man=8, exp_bits=5, man_bits=4)
 
 If the operands of an arithmetic operation have IEEE-like biases, then the result will
 also have an IEEE-like bias -- based on the resulting number of exponent bits.
@@ -86,7 +92,8 @@
 .. math::
     \texttt{bias}_3 = \frac{\left ( \left (\texttt{bias}_1 + 1 \right ) / 2^{\texttt{exp_bits}_1} + \left (\texttt{bias}_2 + 1 \right ) / 2^{\texttt{exp_bits}_2} \right ) \times 2^{\texttt{exp_bits}_3}}{2} - 1,
 
-where exp_bits_1 and exp_bits_2 are the bit widths of the operands, bias_1 and bias_2 are the input biases, and exp_bits_3 is the target bit width.
+where :math:`\texttt{exp_bits}_1` and :math:`\texttt{exp_bits}_2` are the bit widths of the operands, :math:`\texttt{bias}_1` and :math:`\texttt{bias}_2` are the
+input biases, and :math:`\texttt{exp_bits}_3` is the target bit width.
 Note that this formula still results in an IEEE-like bias when the inputs use IEEE-like biases.
 """
 
@@ -110,30 +117,35 @@
 which is :class:`QuantizationMode.TIES_EVEN` by default. The mode however can be changed using the static method
 :func:`apytypes.set_float_quantization_mode`, or, preferably, by using a so-called quantization context.
 With a quantization context one can change the quantization mode used by all operations within a specific section
-in the code:
+in the code.
+
+Examples
+--------
 
-.. code-block:: Python
+>>> from apytypes import APyFloat, QuantizationMode
+>>> from apytypes import APyFloatQuantizationContext
 
-    import random
-    from apytypes import APyFloat, QuantizationMode
-    from apytypes import APyFloatQuantizationContext
+Addition using the default round to nearest, ties even
 
-    # Addition using the default round to nearest, ties even
-    a = APyFloat.from_float(random.uniform(-100, 100), exp_bits=5, man_bits=10)
-    b = APyFloat.from_float(random.uniform(-100, 100), exp_bits=5, man_bits=10)
-    c = a + b
+>>> a = APyFloat.from_float(0.123, exp_bits=5, man_bits=10)
+>>> b = APyFloat.from_float(3.21, exp_bits=5, man_bits=10)
+>>> a + b
+APyFloat(sign=0, exp=16, man=683, exp_bits=5, man_bits=10)
 
-    # Addition using round towards zero
-    m = QuantizationMode.TO_ZERO
-    with APyFloatQuantizationContext(quantization=mode):
-        d = a + b
+Addition using round towards zero
 
-    # Stochastic rounding with an optional seed
-    m = QuantizationMode.STOCH_WEIGHTED
-    s = 0x1234
-    with APyFloatQuantizationContext(quantization=mode, quantization_seed=s):
-        e = a + b
+>>> m = QuantizationMode.TO_ZERO
+>>> with APyFloatQuantizationContext(quantization=m):
+...     a + b
+APyFloat(sign=0, exp=16, man=682, exp_bits=5, man_bits=10)
 
+Stochastic rounding with an optional seed
+
+>>> m = QuantizationMode.STOCH_WEIGHTED
+>>> s = 0x1234
+>>> with APyFloatQuantizationContext(quantization=m, quantization_seed=s):
+...     a + b
+APyFloat(sign=0, exp=16, man=683, exp_bits=5, man_bits=10)
 
 The quantization mode is reverted automatically upon exiting the context.
 Nesting the contexts is also possible.
@@ -146,25 +158,28 @@
 of the operands, but with :class:`APyFloatAccumulatorContext` a custom accumulator can be simulated
 as seen in the example below.
 
-.. code-block:: Python
+Examples
+--------
+
+>>> import numpy as np
+>>> from apytypes import APyFloatArray, QuantizationMode
+>>> from apytypes import APyFloatAccumulatorContext
+
+>>> An = np.random.normal(1, 2, size=(100, 100))
+>>> A = APyFloatArray.from_float(An, exp_bits=5, man_bits=10)
 
-    import numpy as np
-    from apytypes import APyFloatArray, QuantizationMode
-    from apytypes import APyFloatAccumulatorContext
+>>> bn = np.random.uniform(0, 1, size=100),
+>>> b = APyFloatArray.from_float(bn, exp_bits=5, man_bits=10)
 
-    An = np.random.normal(1, 2, size=(100, 100))
-    A = APyFloatArray.from_float(An, exp_bits=5, man_bits=10)
+Normal matrix multiplication
 
-    bn = np.random.uniform(0, 1, size=100),
-    b = APyFloatArray.from_float(bn, exp_bits=5, man_bits=10)
+>>> c = A @ b.T
 
-    # Normal matrix multiplication
-    c = A @ b.T
+Matrix multiplication using stochastic quantization and a wider accumulator
 
-    # Matrix multiplication using stochastic quantization and a wider accumulator
-    m = QuantizationMode.STOCH_WEIGHTED
-    with APyFloatAccumulatorContext(exp_bits=6, man_bits=15, quantization=m):
-        d = A @ b.T
+>>> m = QuantizationMode.STOCH_WEIGHTED
+>>> with APyFloatAccumulatorContext(exp_bits=6, man_bits=15, quantization=m):
+...     d = A @ b.T
 
 
 If no quantization mode is specified to the accumulator context it will fallback to the mode set globally,
diff --git a/lib/apytypes/__init__.pyi b/lib/apytypes/__init__.pyi
index 93d6c49a4..f64acdf45 100644
--- a/lib/apytypes/__init__.pyi
+++ b/lib/apytypes/__init__.pyi
@@ -1,5 +1,6 @@
 import __future__
 
+from . import _apytypes as _apytypes, _version as _version
 from ._apytypes import (
     APyFixed as APyFixed,
     APyFixedAccumulatorContext as APyFixedAccumulatorContext,
@@ -11,6 +12,7 @@ from ._apytypes import (
     APyFloatQuantizationContext as APyFloatQuantizationContext,
     OverflowMode as OverflowMode,
     QuantizationMode as QuantizationMode,
+    _get_simd_version_str as _get_simd_version_str,
     get_float_quantization_mode as get_float_quantization_mode,
     get_float_quantization_seed as get_float_quantization_seed,
     set_float_quantization_mode as set_float_quantization_mode,
diff --git a/lib/apytypes/_apytypes.pyi b/lib/apytypes/_apytypes.pyi
index 25d3b4a0d..b9f95de1d 100644
--- a/lib/apytypes/_apytypes.pyi
+++ b/lib/apytypes/_apytypes.pyi
@@ -158,21 +158,22 @@ class APyFixed:
         """
         Retrieve underlying bit-pattern in an :class:`int`.
 
-        Returns
-        -------
-        :class:`int`
-
         Examples
         --------
-        .. code-block:: python
+        >>> from apytypes import APyFixed
+
+        Create fixed-point number `fx_a` of value -5.75
 
-            from apytypes import APyFixed
+        >>> fx_a = APyFixed.from_float(-5.75, int_bits=4, frac_bits=4)
 
-            # Create fixed-point number `fx_a` of value -5.75
-            fx_a = APyFixed.from_float(-5.75, int_bits=4, frac_bits=4)
+        Returns: 164 == 0xA4 == 0b10100100
 
-            # Returns: 164 == 0xA4 == 0b10100100
-            fx_a.to_bits()
+        >>> fx_a.to_bits()
+        164
+
+        Returns
+        -------
+        :class:`int`
         """
 
     @property
@@ -220,24 +221,25 @@ class APyFixed:
         other : :class:`APyFixed`
             The fixed-point number to test identicality against
 
-        Returns
-        -------
-        :class:`bool`
-
         Examples
         --------
-        .. code-block:: python
+        >>> from apytypes import APyFixed
+        >>> fx_a = APyFixed.from_float(2.0, int_bits=3, frac_bits=3)
+        >>> fx_b = APyFixed.from_float(2.0, int_bits=4, frac_bits=3)
 
-            from apytypes import APyFixed
+        `fx_a` and `fx_b` store the same fixed-point value
 
-            fx_a = APyFixed.from_float(2.0, int_bits=3, frac_bits=3)
-            fx_b = APyFixed.from_float(2.0, int_bits=4, frac_bits=3)
+        >>> fx_a == fx_b
+        True
 
-            # `fx_a` and `fx_b` store the same fixed-point value
-            assert fx_a == fx_b
+        `fx_a` and `fx_b` differ in the `int_bits` specifier
 
-            # `fx_a` and `fx_b` differ in the `int_bits` specifier
-            assert not(fx_a.is_identical(fx_b))
+        >>> fx_a.is_identical(fx_b)
+        False
+
+        Returns
+        -------
+        :class:`bool`
         """
 
     @property
@@ -274,28 +276,33 @@ class APyFixed:
         bits : int, optional
             Total number of bits in the result.
 
-        Returns
-        -------
-        :class:`APyFixed`
-
         Examples
         --------
-        .. code-block:: python
+        >>> from apytypes import APyFixed
+        >>> from apytypes import QuantizationMode
+        >>> from apytypes import OverflowMode
+        >>> fx = APyFixed.from_float(2.125, int_bits=3, frac_bits=3)
+
+        Truncation (2.0)
 
-            from apytypes import APyFixed
-            from apytypes import QuantizationMode
-            from apytypes import OverflowMode
+        >>> fx.cast(int_bits=3, frac_bits=2, quantization=QuantizationMode.TRN)
+        APyFixed(8, bits=5, int_bits=3)
 
-            fx = APyFixed.from_float(2.125, int_bits=3, frac_bits=3)
+        Rounding (2.25)
 
-            # Truncation (fx_a == 2.0)
-            fx_a = fx.cast(int_bits=3, frac_bits=2, quantization=QuantizationMode.TRN)
+        >>> fx.cast(
+        ...     int_bits=3, frac_bits=2, quantization=QuantizationMode.RND
+        ... )  # doctest: +SKIP
+        APyFixed(8, bits=5, int_bits=3)
 
-            # Quantization (fx_b == 2.25)
-            fx_b = fx.cast(int_bits=3, frac_bits=2, quantization=QuantizationMode.RND)
+        Two's complement overflowing (-1.875)
 
-            # Two's complement overflowing (fx_c == -1.875)
-            fx_c = fx.cast(int_bits=2, frac_bits=3, overflow=OverflowMode.WRAP)
+        >>> fx.cast(int_bits=2, frac_bits=3, overflow=OverflowMode.WRAP)
+        APyFixed(17, bits=5, int_bits=2)
+
+        Returns
+        -------
+        :class:`APyFixed`
         """
 
     @property
@@ -371,13 +378,20 @@ class APyFixed:
 
         Examples
         --------
-        .. code-block:: python
+        >>> from apytypes import APyFixed
+        >>> fx_a = APyFixed.from_float(1.234, int_bits=2, frac_bits=2)
 
-            from apytypes import APyFixed
+        Fixed-point `fx_a`, initialized from the floating-point value 1.234,
+        rounded to 1.25 as it is the closest representable number
 
-            # Fixed-point `fx_a`, initialized from the floating-point value 1.234,
-            # rounded to 1.25 as it is the closest representable number
-            fx_a = APyFixed.from_float(1.234, int_bits=2, frac_bits=2)
+        >>> fx_a
+        APyFixed(5, bits=4, int_bits=2)
+        >>> str(fx_a)
+        '1.25'
+
+        Returns
+        -------
+        :class:`APyFixed`
         """
 
     @staticmethod
@@ -409,22 +423,25 @@ class APyFixed:
 
         Examples
         --------
-        .. code-block:: python
-
-            from apytypes import APyFixed
+        >>> from apytypes import APyFixed
+
+        Larger fixed-point value initialization from a string (base-10)
+
+        >>> fx_a = APyFixed.from_str(
+        ...     "-1376018206341311063223476816643087998331620501540496640."
+        ...     "021222579872958058370179355618716816066859017361262100333952697594702"
+        ...     "314679773970519809467311447652539955943903993200932791396783892142688"
+        ...     "708904952458654442554723081083186210082207584128592922850820472478833"
+        ...     "257136662269306798708182072507551281664490003441493733349403017982015"
+        ...     "56238154807942919433116912841796875",
+        ...     bits=511,
+        ...     int_bits=199,
+        ...     base=10,
+        ... )
 
-            # Larger fixed-point value initialization from a string (base-10)
-            fx_a = APyFixed.from_str(
-                "-1376018206341311063223476816643087998331620501540496640."
-                "021222579872958058370179355618716816066859017361262100333952697594702"
-                "314679773970519809467311447652539955943903993200932791396783892142688"
-                "708904952458654442554723081083186210082207584128592922850820472478833"
-                "257136662269306798708182072507551281664490003441493733349403017982015"
-                "56238154807942919433116912841796875",
-                bits=511,
-                int_bits=199,
-                base=10
-            )
+        Returns
+        -------
+        :class:`APyFixed`
         """
 
 class APyFixedAccumulatorContext(ContextManager):
@@ -695,29 +712,29 @@ class APyFixedArray:
         bits : int, optional
             Total number of bits in the created fixed-point tensor.
 
-        Returns
-        -------
-        :class:`APyFixedArray`
-
         Examples
         --------
 
-        .. code-block:: python
+        >>> from apytypes import APyFixedArray
+
+        Array `a`, initialized from floating-point values.
 
-            from apytypes import APyFixedArray
+        >>> a = APyFixedArray.from_float([1.0, 1.25, 1.49], int_bits=2, frac_bits=2)
 
-            # Array `a`, initialized from floating-point values.
-            a = APyFixedArray.from_float([1.0, 1.25, 1.49], int_bits=2, frac_bits=2)
+        Array `b` (2 x 3 matrix), initialized from floating-point values.
 
-            # Array `b` (2 x 3 matrix), initialized from floating-point values.
-            b = APyFixedArray.from_float(
-                [
-                    [1.0, 2.0, 3.0],
-                    [4.0, 5.0, 6.0],
-                ],
-                bits=5,
-                frac_bits=0
-            )
+        >>> b = APyFixedArray.from_float(
+        ...     [
+        ...         [1.0, 2.0, 3.0],
+        ...         [4.0, 5.0, 6.0],
+        ...     ],
+        ...     bits=5,
+        ...     frac_bits=0,
+        ... )
+
+        Returns
+        -------
+        :class:`APyFixedArray`
         """
 
     @staticmethod
@@ -747,27 +764,28 @@ class APyFixedArray:
         bits : int, optional
             Total number of bits in the created fixed-point tensor.
 
-        Returns
-        -------
-        :class:`APyFixedArray`
-
         Examples
         --------
 
-        .. code-block:: python
+        >>> from apytypes import APyFixedArray
+        >>> import numpy as np
 
-            from apytypes import APyFixedArray
-            import numpy as np
+        Array `a`, initialized from NumPy ndarray
 
-            # Array `a`, initialized from NumPy ndarray
-            a = APyFixedArray.from_array(
-                np.array([
-                    [1.0, 2.0, 3.0],
-                    [4.0, 5.0, 6.0],
-                ]),
-                int_bits=10,
-                frac_bits=0
-            )
+        >>> a = APyFixedArray.from_array(
+        ...     np.array(
+        ...         [
+        ...             [1.0, 2.0, 3.0],
+        ...             [4.0, 5.0, 6.0],
+        ...         ]
+        ...     ),
+        ...     int_bits=10,
+        ...     frac_bits=0,
+        ... )
+
+        Returns
+        -------
+        :class:`APyFixedArray`
         """
 
     def __lshift__(self, shift_amnt: int) -> APyFixedArray: ...
@@ -825,18 +843,24 @@ class APyFloat:
     If the operands do not share the same format, the resulting
     bit widths of the exponent and mantissa field will be the maximum of its inputs:
 
-    .. code-block:: Python
+    Examples
+    --------
 
-        from apytypes import APyFloat
-        a = APyFixed.from_float(1.25, exp_bits=5, man_bits=2)
-        b = APyFixed.from_float(1.75, exp_bits=5, man_bits=2)
-        # Operands with same format, result will have exp_bits=5, man_bits=2
-        c = a + b
+    >>> from apytypes import APyFloat
+    >>> a = APyFloat.from_float(1.25, exp_bits=5, man_bits=2)
+    >>> b = APyFloat.from_float(1.75, exp_bits=5, man_bits=2)
 
-        d = APyFixed.from_float(1.75, exp_bits=4, man_bits=4)
-        # Operands with different formats, result will have exp_bits=5, man_bits=4
-        e = a + d
+    Operands with same format, result will have exp_bits=5, man_bits=2
 
+    >>> a + b
+    APyFloat(sign=0, exp=16, man=2, exp_bits=5, man_bits=2)
+
+    >>> d = APyFloat.from_float(1.75, exp_bits=4, man_bits=4)
+
+    Operands with different formats, result will have exp_bits=5, man_bits=4
+
+    >>> a + d
+    APyFloat(sign=0, exp=16, man=8, exp_bits=5, man_bits=4)
 
     If the operands of an arithmetic operation have IEEE-like biases, then the result will
     also have an IEEE-like bias -- based on the resulting number of exponent bits.
@@ -846,7 +870,8 @@ class APyFloat:
     .. math::
         \texttt{bias}_3 = \frac{\left ( \left (\texttt{bias}_1 + 1 \right ) / 2^{\texttt{exp_bits}_1} + \left (\texttt{bias}_2 + 1 \right ) / 2^{\texttt{exp_bits}_2} \right ) \times 2^{\texttt{exp_bits}_3}}{2} - 1,
 
-    where exp_bits_1 and exp_bits_2 are the bit widths of the operands, bias_1 and bias_2 are the input biases, and exp_bits_3 is the target bit width.
+    where :math:`\texttt{exp_bits}_1` and :math:`\texttt{exp_bits}_2` are the bit widths of the operands, :math:`\texttt{bias}_1` and :math:`\texttt{bias}_2` are the
+    input biases, and :math:`\texttt{exp_bits}_3` is the target bit width.
     Note that this formula still results in an IEEE-like bias when the inputs use IEEE-like biases.
     """
 
@@ -902,19 +927,18 @@ class APyFloat:
         bias : int, optional
             Bias. If not provided, *bias* is ``2**exp_bits - 1``.
 
-        Returns
-        -------
-        :class:`APyFloat`
-
         Examples
         --------
 
-        .. code-block:: python
+        >>> from apytypes import APyFloat
 
-            from apytypes import APyFloat
+        `a`, initialized from floating-point values.
 
-            # `a`, initialized from floating-point values.
-            a = APyFloat.from_float(1.35, exp_bits=10, man_bits=15)
+        >>> a = APyFloat.from_float(1.35, exp_bits=10, man_bits=15)
+
+        Returns
+        -------
+        :class:`APyFloat`
 
         See also
         --------
@@ -940,19 +964,18 @@ class APyFloat:
         bias : int, optional
             Bias. If not provided, *bias* is ``2**exp_bits - 1``.
 
-        Returns
-        -------
-        :class:`APyFloat`
-
         Examples
         --------
 
-        .. code-block:: python
+        >>> from apytypes import APyFloat
 
-            from apytypes import APyFloat
+        `a`, initialized to -1.5 from a bit pattern.
 
-            # `a`, initialized to -1.5 from a bit pattern.
-            a = APyFloat.from_bits(0b1_01111_10, exp_bits=5, man_bits=2)
+        >>> a = APyFloat.from_bits(0b1_01111_10, exp_bits=5, man_bits=2)
+
+        Returns
+        -------
+        :class:`APyFloat`
 
         See also
         --------
@@ -964,20 +987,22 @@ class APyFloat:
         """
         Get the bit-representation of an :class:`APyFloat`.
 
-        Returns
-        -------
-        :class:`int`
-
         Examples
         --------
 
-        .. code-block:: python
+        >>> from apytypes import APyFloat
 
-            from apytypes import APyFloat
+        `a`, initialized to -1.5 from a bit pattern.
 
-            # `a`, initialized to -1.5 from a bit pattern.
-            a = APyFloat.from_bits(0b1_01111_10, exp_bits=5, man_bits=2)
-            a.to_bits() == 0b1_01111_10 # True
+        >>> a = APyFloat.from_bits(0b1_01111_10, exp_bits=5, man_bits=2)
+        >>> a
+        APyFloat(sign=1, exp=15, man=2, exp_bits=5, man_bits=2)
+        >>> a.to_bits() == 0b1_01111_10
+        True
+
+        Returns
+        -------
+        :class:`int`
 
         See also
         --------
@@ -1411,25 +1436,28 @@ class APyFloatAccumulatorContext(ContextManager):
     of the operands, but with :class:`APyFloatAccumulatorContext` a custom accumulator can be simulated
     as seen in the example below.
 
-    .. code-block:: Python
+    Examples
+    --------
+
+    >>> import numpy as np
+    >>> from apytypes import APyFloatArray, QuantizationMode
+    >>> from apytypes import APyFloatAccumulatorContext
+
+    >>> An = np.random.normal(1, 2, size=(100, 100))
+    >>> A = APyFloatArray.from_float(An, exp_bits=5, man_bits=10)
 
-        import numpy as np
-        from apytypes import APyFloatArray, QuantizationMode
-        from apytypes import APyFloatAccumulatorContext
+    >>> bn = (np.random.uniform(0, 1, size=100),)
+    >>> b = APyFloatArray.from_float(bn, exp_bits=5, man_bits=10)
 
-        An = np.random.normal(1, 2, size=(100, 100))
-        A = APyFloatArray.from_float(An, exp_bits=5, man_bits=10)
+    Normal matrix multiplication
 
-        bn = np.random.uniform(0, 1, size=100),
-        b = APyFloatArray.from_float(bn, exp_bits=5, man_bits=10)
+    >>> c = A @ b.T
 
-        # Normal matrix multiplication
-        c = A @ b.T
+    Matrix multiplication using stochastic quantization and a wider accumulator
 
-        # Matrix multiplication using stochastic quantization and a wider accumulator
-        m = QuantizationMode.STOCH_WEIGHTED
-        with APyFloatAccumulatorContext(exp_bits=6, man_bits=15, quantization=m):
-            d = A @ b.T
+    >>> m = QuantizationMode.STOCH_WEIGHTED
+    >>> with APyFloatAccumulatorContext(exp_bits=6, man_bits=15, quantization=m):
+    ...     d = A @ b.T
 
 
     If no quantization mode is specified to the accumulator context it will fallback to the mode set globally,
@@ -1658,29 +1686,29 @@ class APyFloatArray:
         bias : int, optional
             Bias in the created floating-point tensor
 
-        Returns
-        -------
-        :class:`APyFloatArray`
-
         Examples
         --------
 
-        .. code-block:: python
+        >>> from apytypes import APyFloatArray
+
+        Array `a`, initialized from floating-point values.
 
-            from apytypes import APyFloatArray
+        >>> a = APyFloatArray.from_float([1.0, 1.25, 1.49], exp_bits=10, man_bits=15)
 
-            # Array `a`, initialized from floating-point values.
-            a = APyFloatArray.from_float([1.0, 1.25, 1.49], exp_bits=10, man_bits=15)
+        Array `lhs` (2 x 3 matrix), initialized from floating-point values.
 
-            # Array `lhs` (2 x 3 matrix), initialized from floating-point values.
-            lhs = APyFloatArray.from_float(
-                [
-                    [1.0, 2.0, 3.0],
-                    [4.0, 5.0, 6.0],
-                ],
-                exp_bits=5,
-                man_bits=2
-            )
+        >>> lhs = APyFloatArray.from_float(
+        ...     [
+        ...         [1.0, 2.0, 3.0],
+        ...         [4.0, 5.0, 6.0],
+        ...     ],
+        ...     exp_bits=5,
+        ...     man_bits=2,
+        ... )
+
+        Returns
+        -------
+        :class:`APyFloatArray`
         """
 
     @staticmethod
@@ -1698,33 +1726,34 @@ class APyFloatArray:
         ndarray : ndarray
             Values to initialize from. The tensor shape will be taken from the ndarray shape.
         exp_bits : int
-            Number of exponent bits in the created fixed-point tensor
+            Number of exponent bits in the created floating-point tensor
         man_bits : int
-            Number of mantissa bits in the created fixed-point tensor
+            Number of mantissa bits in the created floating-point tensor
         bias : int, optional
-            Bias in the created fixed-point tensor
-
-        Returns
-        -------
-        :class:`APyFloatArray`
+            Bias in the created floating-point tensor
 
         Examples
         --------
 
-        .. code-block:: python
+        >>> from apytypes import APyFloatArray
+        >>> import numpy as np
 
-            from apytypes import APyFloatArray
-            import numpy as np
+        Array `a`, initialized from NumPy ndarray
 
-            # Array `a`, initialized from NumPy ndarray
-            a = APyFloatArray.from_array(
-                np.array([
-                    [1.0, 2.0, 3.0],
-                    [4.0, 5.0, 6.0],
-                ]),
-                man_bits=10,
-                exp_bits=10
-            )
+        >>> a = APyFloatArray.from_array(
+        ...     np.array(
+        ...         [
+        ...             [1.0, 2.0, 3.0],
+        ...             [4.0, 5.0, 6.0],
+        ...         ]
+        ...     ),
+        ...     man_bits=10,
+        ...     exp_bits=10,
+        ... )
+
+        Returns
+        -------
+        :class:`APyFloatArray`
         """
 
     def __matmul__(self, rhs: APyFloatArray) -> APyFloatArray | APyFloat: ...
@@ -1901,30 +1930,35 @@ class APyFloatQuantizationContext(ContextManager):
     which is :class:`QuantizationMode.TIES_EVEN` by default. The mode however can be changed using the static method
     :func:`apytypes.set_float_quantization_mode`, or, preferably, by using a so-called quantization context.
     With a quantization context one can change the quantization mode used by all operations within a specific section
-    in the code:
+    in the code.
+
+    Examples
+    --------
+
+    >>> from apytypes import APyFloat, QuantizationMode
+    >>> from apytypes import APyFloatQuantizationContext
 
-    .. code-block:: Python
+    Addition using the default round to nearest, ties even
 
-        import random
-        from apytypes import APyFloat, QuantizationMode
-        from apytypes import APyFloatQuantizationContext
+    >>> a = APyFloat.from_float(0.123, exp_bits=5, man_bits=10)
+    >>> b = APyFloat.from_float(3.21, exp_bits=5, man_bits=10)
+    >>> a + b
+    APyFloat(sign=0, exp=16, man=683, exp_bits=5, man_bits=10)
 
-        # Addition using the default round to nearest, ties even
-        a = APyFloat.from_float(random.uniform(-100, 100), exp_bits=5, man_bits=10)
-        b = APyFloat.from_float(random.uniform(-100, 100), exp_bits=5, man_bits=10)
-        c = a + b
+    Addition using round towards zero
 
-        # Addition using round towards zero
-        m = QuantizationMode.TO_ZERO
-        with APyFloatQuantizationContext(quantization=mode):
-            d = a + b
+    >>> m = QuantizationMode.TO_ZERO
+    >>> with APyFloatQuantizationContext(quantization=m):
+    ...     a + b
+    APyFloat(sign=0, exp=16, man=682, exp_bits=5, man_bits=10)
 
-        # Stochastic rounding with an optional seed
-        m = QuantizationMode.STOCH_WEIGHTED
-        s = 0x1234
-        with APyFloatQuantizationContext(quantization=mode, quantization_seed=s):
-            e = a + b
+    Stochastic rounding with an optional seed
 
+    >>> m = QuantizationMode.STOCH_WEIGHTED
+    >>> s = 0x1234
+    >>> with APyFloatQuantizationContext(quantization=m, quantization_seed=s):
+    ...     a + b
+    APyFloat(sign=0, exp=16, man=683, exp_bits=5, man_bits=10)
 
     The quantization mode is reverted automatically upon exiting the context.
     Nesting the contexts is also possible.
diff --git a/pyproject.toml b/pyproject.toml
index ae85aa4c5..4811f5df0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -96,3 +96,57 @@ skip = [
 [tool.cibuildwheel.macos.environment]
 # MacOS 10.14 is the first MacOS release with full support for C++17
 MACOSX_DEPLOYMENT_TARGET = "10.14"
+
+[tool.ruff]
+# Exclude a variety of commonly ignored directories.
+exclude = [
+    ".git",
+    ".vscode",
+    "_build",
+    "build",
+]
+
+# Same as Black.
+line-length = 88
+indent-width = 4
+
+# Assume Python 3.9
+target-version = "py39"
+
+[tool.ruff.lint]
+# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`)  codes by default.
+# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
+# McCabe complexity (`C901`) by default.
+select = ["E4", "E7", "E9", "F"]
+ignore = ["F822",  # Should not be ignored, but as nanobind stubgen does not include __version__...
+          "E712"] # Test should be rewritten to not compare with False/True
+# 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]
+# Like Black, use double quotes for strings.
+quote-style = "double"
+
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "auto"
+
+# Enable auto-formatting of code examples in docstrings. Markdown,
+# reStructuredText code/literal blocks and doctests are all supported.
+docstring-code-format = true
+
+# Set the line length limit used when formatting code snippets in
+# docstrings.
+#
+# This only has an effect when the `docstring-code-format` setting is
+# enabled.
+docstring-code-line-length = "dynamic"
diff --git a/src/apyfixed_wrapper.cc b/src/apyfixed_wrapper.cc
index 9233be6fb..30ffd5fdb 100644
--- a/src/apyfixed_wrapper.cc
+++ b/src/apyfixed_wrapper.cc
@@ -173,21 +173,22 @@ void bind_fixed(nb::module_& m)
             R"pbdoc(
             Retrieve underlying bit-pattern in an :class:`int`.
 
-            Returns
-            -------
-            :class:`int`
-
             Examples
             --------
-            .. code-block:: python
+            >>> from apytypes import APyFixed
+
+            Create fixed-point number `fx_a` of value -5.75
 
-                from apytypes import APyFixed
+            >>> fx_a = APyFixed.from_float(-5.75, int_bits=4, frac_bits=4)
 
-                # Create fixed-point number `fx_a` of value -5.75
-                fx_a = APyFixed.from_float(-5.75, int_bits=4, frac_bits=4)
+            Returns: 164 == 0xA4 == 0b10100100
 
-                # Returns: 164 == 0xA4 == 0b10100100
-                fx_a.to_bits()
+            >>> fx_a.to_bits()
+            164
+
+            Returns
+            -------
+            :class:`int`
             )pbdoc"
         )
         .def_prop_ro("bits", &APyFixed::bits, R"pbdoc(
@@ -227,24 +228,25 @@ void bind_fixed(nb::module_& m)
             other : :class:`APyFixed`
                 The fixed-point number to test identicality against
 
-            Returns
-            -------
-            :class:`bool`
-
             Examples
             --------
-            .. code-block:: python
+            >>> from apytypes import APyFixed
+            >>> fx_a = APyFixed.from_float(2.0, int_bits=3, frac_bits=3)
+            >>> fx_b = APyFixed.from_float(2.0, int_bits=4, frac_bits=3)
 
-                from apytypes import APyFixed
+            `fx_a` and `fx_b` store the same fixed-point value
 
-                fx_a = APyFixed.from_float(2.0, int_bits=3, frac_bits=3)
-                fx_b = APyFixed.from_float(2.0, int_bits=4, frac_bits=3)
+            >>> fx_a == fx_b
+            True
 
-                # `fx_a` and `fx_b` store the same fixed-point value
-                assert fx_a == fx_b
+            `fx_a` and `fx_b` differ in the `int_bits` specifier
 
-                # `fx_a` and `fx_b` differ in the `int_bits` specifier
-                assert not(fx_a.is_identical(fx_b))
+            >>> fx_a.is_identical(fx_b)
+            False
+
+            Returns
+            -------
+            :class:`bool`
             )pbdoc")
         .def_prop_ro("is_zero", &APyFixed::is_zero, R"pbdoc(
             True if the value equals zero, false otherwise.
@@ -279,28 +281,31 @@ void bind_fixed(nb::module_& m)
             bits : int, optional
                 Total number of bits in the result.
 
-            Returns
-            -------
-            :class:`APyFixed`
-
             Examples
             --------
-            .. code-block:: python
+            >>> from apytypes import APyFixed
+            >>> from apytypes import QuantizationMode
+            >>> from apytypes import OverflowMode
+            >>> fx = APyFixed.from_float(2.125, int_bits=3, frac_bits=3)
+
+            Truncation (2.0)
 
-                from apytypes import APyFixed
-                from apytypes import QuantizationMode
-                from apytypes import OverflowMode
+            >>> fx.cast(int_bits=3, frac_bits=2, quantization=QuantizationMode.TRN)
+            APyFixed(8, bits=5, int_bits=3)
 
-                fx = APyFixed.from_float(2.125, int_bits=3, frac_bits=3)
+            Rounding (2.25)
 
-                # Truncation (fx_a == 2.0)
-                fx_a = fx.cast(int_bits=3, frac_bits=2, quantization=QuantizationMode.TRN)
+            >>> fx.cast(int_bits=3, frac_bits=2, quantization=QuantizationMode.RND) # doctest: +SKIP
+            APyFixed(8, bits=5, int_bits=3)
 
-                # Quantization (fx_b == 2.25)
-                fx_b = fx.cast(int_bits=3, frac_bits=2, quantization=QuantizationMode.RND)
+            Two's complement overflowing (-1.875)
 
-                # Two's complement overflowing (fx_c == -1.875)
-                fx_c = fx.cast(int_bits=2, frac_bits=3, overflow=OverflowMode.WRAP)
+            >>> fx.cast(int_bits=2, frac_bits=3, overflow=OverflowMode.WRAP)
+            APyFixed(17, bits=5, int_bits=2)
+
+            Returns
+            -------
+            :class:`APyFixed`
             )pbdoc"
         )
         .def_prop_ro("_vector_size", &APyFixed::vector_size)
@@ -400,13 +405,20 @@ void bind_fixed(nb::module_& m)
 
             Examples
             --------
-            .. code-block:: python
+            >>> from apytypes import APyFixed
+            >>> fx_a = APyFixed.from_float(1.234, int_bits=2, frac_bits=2)
 
-                from apytypes import APyFixed
+            Fixed-point `fx_a`, initialized from the floating-point value 1.234,
+            rounded to 1.25 as it is the closest representable number
 
-                # Fixed-point `fx_a`, initialized from the floating-point value 1.234,
-                # rounded to 1.25 as it is the closest representable number
-                fx_a = APyFixed.from_float(1.234, int_bits=2, frac_bits=2)
+            >>> fx_a
+            APyFixed(5, bits=4, int_bits=2)
+            >>> str(fx_a)
+            '1.25'
+
+            Returns
+            -------
+            :class:`APyFixed`
             )pbdoc"
         )
         .def_static(
@@ -438,22 +450,25 @@ void bind_fixed(nb::module_& m)
 
             Examples
             --------
-            .. code-block:: python
-
-                from apytypes import APyFixed
-
-                # Larger fixed-point value initialization from a string (base-10)
-                fx_a = APyFixed.from_str(
-                    "-1376018206341311063223476816643087998331620501540496640."
-                    "021222579872958058370179355618716816066859017361262100333952697594702"
-                    "314679773970519809467311447652539955943903993200932791396783892142688"
-                    "708904952458654442554723081083186210082207584128592922850820472478833"
-                    "257136662269306798708182072507551281664490003441493733349403017982015"
-                    "56238154807942919433116912841796875",
-                    bits=511,
-                    int_bits=199,
-                    base=10
-                )
+            >>> from apytypes import APyFixed
+
+            Larger fixed-point value initialization from a string (base-10)
+
+            >>> fx_a = APyFixed.from_str(
+            ...     "-1376018206341311063223476816643087998331620501540496640."
+            ...     "021222579872958058370179355618716816066859017361262100333952697594702"
+            ...     "314679773970519809467311447652539955943903993200932791396783892142688"
+            ...     "708904952458654442554723081083186210082207584128592922850820472478833"
+            ...     "257136662269306798708182072507551281664490003441493733349403017982015"
+            ...     "56238154807942919433116912841796875",
+            ...     bits=511,
+            ...     int_bits=199,
+            ...     base=10
+            ... )
+
+            Returns
+            -------
+            :class:`APyFixed`
             )pbdoc"
         );
 }
diff --git a/src/apyfixedarray_wrapper.cc b/src/apyfixedarray_wrapper.cc
index 0e7ecc1ad..35e79f2c6 100644
--- a/src/apyfixedarray_wrapper.cc
+++ b/src/apyfixedarray_wrapper.cc
@@ -379,29 +379,29 @@ void bind_fixed_array(nb::module_& m)
             bits : int, optional
                 Total number of bits in the created fixed-point tensor.
 
-            Returns
-            -------
-            :class:`APyFixedArray`
-
             Examples
             --------
 
-            .. code-block:: python
+            >>> from apytypes import APyFixedArray
+
+            Array `a`, initialized from floating-point values.
 
-                from apytypes import APyFixedArray
+            >>> a = APyFixedArray.from_float([1.0, 1.25, 1.49], int_bits=2, frac_bits=2)
 
-                # Array `a`, initialized from floating-point values.
-                a = APyFixedArray.from_float([1.0, 1.25, 1.49], int_bits=2, frac_bits=2)
+            Array `b` (2 x 3 matrix), initialized from floating-point values.
 
-                # Array `b` (2 x 3 matrix), initialized from floating-point values.
-                b = APyFixedArray.from_float(
-                    [
-                        [1.0, 2.0, 3.0],
-                        [4.0, 5.0, 6.0],
-                    ],
-                    bits=5,
-                    frac_bits=0
-                )
+            >>> b = APyFixedArray.from_float(
+            ...     [
+            ...         [1.0, 2.0, 3.0],
+            ...         [4.0, 5.0, 6.0],
+            ...     ],
+            ...     bits=5,
+            ...     frac_bits=0
+            ... )
+
+            Returns
+            -------
+            :class:`APyFixedArray`
             )pbdoc"
         )
         .def_static(
@@ -431,27 +431,26 @@ void bind_fixed_array(nb::module_& m)
             bits : int, optional
                 Total number of bits in the created fixed-point tensor.
 
-            Returns
-            -------
-            :class:`APyFixedArray`
-
             Examples
             --------
 
-            .. code-block:: python
+            >>> from apytypes import APyFixedArray
+            >>> import numpy as np
 
-                from apytypes import APyFixedArray
-                import numpy as np
+            Array `a`, initialized from NumPy ndarray
 
-                # Array `a`, initialized from NumPy ndarray
-                a = APyFixedArray.from_array(
-                    np.array([
-                        [1.0, 2.0, 3.0],
-                        [4.0, 5.0, 6.0],
-                    ]),
-                    int_bits=10,
-                    frac_bits=0
-                )
+            >>> a = APyFixedArray.from_array(
+            ...     np.array([
+            ...         [1.0, 2.0, 3.0],
+            ...         [4.0, 5.0, 6.0],
+            ...     ]),
+            ...     int_bits=10,
+            ...     frac_bits=0
+            ... )
+
+            Returns
+            -------
+            :class:`APyFixedArray`
             )pbdoc"
         )
 
diff --git a/src/apyfloat_wrapper.cc b/src/apyfloat_wrapper.cc
index 17519b634..bab71a713 100644
--- a/src/apyfloat_wrapper.cc
+++ b/src/apyfloat_wrapper.cc
@@ -71,19 +71,18 @@ void bind_float(nb::module_& m)
             bias : int, optional
                 Bias. If not provided, *bias* is ``2**exp_bits - 1``.
 
-            Returns
-            -------
-            :class:`APyFloat`
-
             Examples
             --------
 
-            .. code-block:: python
+            >>> from apytypes import APyFloat
+
+            `a`, initialized from floating-point values.
 
-                from apytypes import APyFloat
+            >>> a = APyFloat.from_float(1.35, exp_bits=10, man_bits=15)
 
-                # `a`, initialized from floating-point values.
-                a = APyFloat.from_float(1.35, exp_bits=10, man_bits=15)
+            Returns
+            -------
+            :class:`APyFloat`
 
             See also
             --------
@@ -112,19 +111,18 @@ void bind_float(nb::module_& m)
             bias : int, optional
                 Bias. If not provided, *bias* is ``2**exp_bits - 1``.
 
-            Returns
-            -------
-            :class:`APyFloat`
-
             Examples
             --------
 
-            .. code-block:: python
+            >>> from apytypes import APyFloat
 
-                from apytypes import APyFloat
+            `a`, initialized to -1.5 from a bit pattern.
 
-                # `a`, initialized to -1.5 from a bit pattern.
-                a = APyFloat.from_bits(0b1_01111_10, exp_bits=5, man_bits=2)
+            >>> a = APyFloat.from_bits(0b1_01111_10, exp_bits=5, man_bits=2)
+
+            Returns
+            -------
+            :class:`APyFloat`
 
             See also
             --------
@@ -135,20 +133,22 @@ void bind_float(nb::module_& m)
         .def("to_bits", &APyFloat::to_bits, R"pbdoc(
             Get the bit-representation of an :class:`APyFloat`.
 
-            Returns
-            -------
-            :class:`int`
-
             Examples
             --------
 
-            .. code-block:: python
+            >>> from apytypes import APyFloat
+
+            `a`, initialized to -1.5 from a bit pattern.
 
-                from apytypes import APyFloat
+            >>> a = APyFloat.from_bits(0b1_01111_10, exp_bits=5, man_bits=2)
+            >>> a
+            APyFloat(sign=1, exp=15, man=2, exp_bits=5, man_bits=2)
+            >>> a.to_bits() == 0b1_01111_10
+            True
 
-                # `a`, initialized to -1.5 from a bit pattern.
-                a = APyFloat.from_bits(0b1_01111_10, exp_bits=5, man_bits=2)
-                a.to_bits() == 0b1_01111_10 # True
+            Returns
+            -------
+            :class:`int`
 
             See also
             --------
diff --git a/src/apyfloatarray_wrapper.cc b/src/apyfloatarray_wrapper.cc
index 375b3c588..88830da50 100644
--- a/src/apyfloatarray_wrapper.cc
+++ b/src/apyfloatarray_wrapper.cc
@@ -353,29 +353,29 @@ void bind_float_array(nb::module_& m)
             bias : int, optional
                 Bias in the created floating-point tensor
 
-            Returns
-            -------
-            :class:`APyFloatArray`
-
             Examples
             --------
 
-            .. code-block:: python
+            >>> from apytypes import APyFloatArray
 
-                from apytypes import APyFloatArray
+            Array `a`, initialized from floating-point values.
 
-                # Array `a`, initialized from floating-point values.
-                a = APyFloatArray.from_float([1.0, 1.25, 1.49], exp_bits=10, man_bits=15)
+            >>> a = APyFloatArray.from_float([1.0, 1.25, 1.49], exp_bits=10, man_bits=15)
 
-                # Array `lhs` (2 x 3 matrix), initialized from floating-point values.
-                lhs = APyFloatArray.from_float(
-                    [
-                        [1.0, 2.0, 3.0],
-                        [4.0, 5.0, 6.0],
-                    ],
-                    exp_bits=5,
-                    man_bits=2
-                )
+            Array `lhs` (2 x 3 matrix), initialized from floating-point values.
+
+            >>> lhs = APyFloatArray.from_float(
+            ...     [
+            ...         [1.0, 2.0, 3.0],
+            ...         [4.0, 5.0, 6.0],
+            ...     ],
+            ...     exp_bits=5,
+            ...     man_bits=2
+            ... )
+
+            Returns
+            -------
+            :class:`APyFloatArray`
             )pbdoc"
         )
         .def_static(
@@ -399,28 +399,26 @@ void bind_float_array(nb::module_& m)
             bias : int, optional
                 Bias in the created floating-point tensor
 
-            Returns
-            -------
-            :class:`APyFloatArray`
-
             Examples
             --------
 
-            .. code-block:: python
+            >>> from apytypes import APyFloatArray
+            >>> import numpy as np
 
-                from apytypes import APyFloatArray
-                import numpy as np
+            Array `a`, initialized from NumPy ndarray
 
-                # Array `a`, initialized from NumPy ndarray
-                a = APyFloatArray.from_array(
-                    np.array([
-                        [1.0, 2.0, 3.0],
-                        [4.0, 5.0, 6.0],
-                    ]),
-                    man_bits=10,
-                    exp_bits=10
-                )
+            >>> a = APyFloatArray.from_array(
+            ...     np.array([
+            ...         [1.0, 2.0, 3.0],
+            ...         [4.0, 5.0, 6.0],
+            ...     ]),
+            ...     man_bits=10,
+            ...     exp_bits=10
+            ... )
 
+            Returns
+            -------
+            :class:`APyFloatArray`
             )pbdoc"
         )
 
diff --git a/stubgen.sh b/stubgen.sh
index 5ea1c6ad6..ac106eb0d 100755
--- a/stubgen.sh
+++ b/stubgen.sh
@@ -1,3 +1,3 @@
 #!/usr/bin/env sh
-python3 -m nanobind.stubgen -m apytypes -o lib/apytypes/__init__.pyi
+python3 -m nanobind.stubgen -P -m apytypes -o lib/apytypes/__init__.pyi
 python3 -m nanobind.stubgen -m apytypes._apytypes -o lib/apytypes/_apytypes.pyi


Test patch:
diff --git a/.github/workflows/stub-and-doctests.yml b/.github/workflows/stub-and-doctests.yml
new file mode 100644
index 000000000..356c4a68b
--- /dev/null
+++ b/.github/workflows/stub-and-doctests.yml
@@ -0,0 +1,45 @@
+name: Stub and doc tests
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }}
+  cancel-in-progress: true
+
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+    branches: [ "main" ]
+
+jobs:
+  build:
+    strategy:
+      fail-fast: false
+    runs-on: "ubuntu-latest"
+
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+    - name: Set up Python 3.10
+      uses: actions/setup-python@v5
+      with:
+        python-version: "3.10"
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        python -m pip install numpy nanobind pytest typing-extensions ruff
+    - name: Install APyTypes
+      run: |
+        git fetch --tags
+        python -m pip install --no-deps -v .
+    - name: Create and check stubs
+      # uses: nickcharlton/diff-check@main
+      # with:
+      #   command: |
+      #     ./stubgen.sh
+      #     ruff format lib/apytypes/*.pyi
+      run: |
+        ./stubgen.sh
+        ruff format lib/apytypes/*.pyi
+    - name: Test with doctest
+      run: |
+        python -m doctest lib/apytypes/*.pyi
diff --git a/lib/test/apyfixed/test_arithmetic.py b/lib/test/apyfixed/test_arithmetic.py
index 518006977..234248f14 100644
--- a/lib/test/apyfixed/test_arithmetic.py
+++ b/lib/test/apyfixed/test_arithmetic.py
@@ -416,7 +416,6 @@ def test_leading_signs():
 
 
 def test_leading_fractional_zeros():
-
     # Code coverage test
     assert APyFixed(0x00, bits=8, frac_bits=0).leading_fractional_zeros == 0
 
diff --git a/lib/test/apyfixed/test_overflow.py b/lib/test/apyfixed/test_overflow.py
index f5f643113..ce201f0d9 100644
--- a/lib/test/apyfixed/test_overflow.py
+++ b/lib/test/apyfixed/test_overflow.py
@@ -1,8 +1,6 @@
 from apytypes import APyFixed
 from apytypes import OverflowMode
 
-import pytest
-
 
 def test_overflow_twos_complement():
     print(float(APyFixed(0xFF, 8, 8)))
diff --git a/lib/test/apyfixedarray/test_arithmetic.py b/lib/test/apyfixedarray/test_arithmetic.py
index 35ba11ab8..876059b0f 100644
--- a/lib/test/apyfixedarray/test_arithmetic.py
+++ b/lib/test/apyfixedarray/test_arithmetic.py
@@ -426,7 +426,6 @@ def test_array_prod():
 
 
 def test_transpose():
-
     # High-dimensional transor transposition not implemented
     with pytest.raises(ValueError, match="Not implemented: high-dimensional"):
         APyFixedArray([[[1]]], 1, 0).T
diff --git a/lib/test/apyfixedarray/test_constructors.py b/lib/test/apyfixedarray/test_constructors.py
index f5c456d7b..36ca1abe4 100644
--- a/lib/test/apyfixedarray/test_constructors.py
+++ b/lib/test/apyfixedarray/test_constructors.py
@@ -161,10 +161,10 @@ def test_from_numpy_raises():
     np = pytest.importorskip("numpy")
     a = np.asarray([[0.0, 1.0]], dtype="half")
     with pytest.raises(TypeError, match="APyFixedArray::_set_values_from_ndarray"):
-        b = APyFixedArray.from_float(a, 14, 4)
+        APyFixedArray.from_float(a, 14, 4)
     a = np.asarray([[0.0, 1.0]])
     with pytest.raises(TypeError, match="APyFixedArray::_set_bits_from_ndarray"):
-        b = APyFixedArray(a, 14, 4)
+        APyFixedArray(a, 14, 4)
 
 
 def test_c_striding():
diff --git a/lib/test/apyfixedarray/test_matmul.py b/lib/test/apyfixedarray/test_matmul.py
index 2c053a47b..180658267 100644
--- a/lib/test/apyfixedarray/test_matmul.py
+++ b/lib/test/apyfixedarray/test_matmul.py
@@ -7,7 +7,6 @@
 
 
 def test_dimension_mismatch_raises():
-
     ##
     # Vector inner product mismatch
     #
diff --git a/lib/test/apyfixedarray/test_representation.py b/lib/test/apyfixedarray/test_representation.py
index 75f7314f3..472133924 100644
--- a/lib/test/apyfixedarray/test_representation.py
+++ b/lib/test/apyfixedarray/test_representation.py
@@ -1,7 +1,5 @@
 from apytypes import APyFixedArray
 
-import pytest
-
 
 def test_array_representation():
     assert (
diff --git a/lib/test/apyfloat/test_arithmetic.py b/lib/test/apyfloat/test_arithmetic.py
index 60bd33ef7..6bd305e5e 100644
--- a/lib/test/apyfloat/test_arithmetic.py
+++ b/lib/test/apyfloat/test_arithmetic.py
@@ -43,7 +43,8 @@ def test_add_representable(exp, man, sign, lhs, rhs):
     print(repr(APyFloat.from_float(f, e, m)))
     assert float(
         eval(
-            expr := f"APyFloat.from_float({sign[0]}{lhs}, {exp[0]}, {man[0]}) + APyFloat.from_float({sign[1]}{rhs}, {exp[1]}, {man[1]})"
+            expr
+            := f"APyFloat.from_float({sign[0]}{lhs}, {exp[0]}, {man[0]}) + APyFloat.from_float({sign[1]}{rhs}, {exp[1]}, {man[1]})"
         )
     ) == (float(eval(f"{sign[0]}{lhs} + {sign[1]}{rhs}")))
     res = eval(expr)
@@ -434,7 +435,8 @@ def test_mul_mixed(exp, man, sign, lhs, rhs):
     expr = None
     assert float(
         eval(
-            expr := f"APyFloat.from_float({sign[0]}{lhs}, {exp[0]}, {man[0]}) * APyFloat.from_float({sign[1]}{rhs}, {exp[1]}, {man[1]})"
+            expr
+            := f"APyFloat.from_float({sign[0]}{lhs}, {exp[0]}, {man[0]}) * APyFloat.from_float({sign[1]}{rhs}, {exp[1]}, {man[1]})"
         )
     ) == (float(eval(f"{sign[0]}{lhs} * {sign[1]}{rhs}")))
     res = eval(expr)
@@ -702,7 +704,8 @@ def test_div_mixed_formats(exp, man, sign, lhs, rhs):
     expr = None
     assert float(
         eval(
-            expr := f"APyFloat.from_float({sign[0]}{lhs}, {exp[0]}, {man[0]}) / APyFloat.from_float({sign[1]}{rhs}, {exp[1]}, {man[1]})"
+            expr
+            := f"APyFloat.from_float({sign[0]}{lhs}, {exp[0]}, {man[0]}) / APyFloat.from_float({sign[1]}{rhs}, {exp[1]}, {man[1]})"
         )
     ) == (float(eval(f"{sign[0]}{lhs} / {sign[1]}{rhs}")))
     res = eval(expr)
diff --git a/lib/test/apyfloatarray/test_arithmetic.py b/lib/test/apyfloatarray/test_arithmetic.py
index 50c4cc914..34e9930a9 100644
--- a/lib/test/apyfloatarray/test_arithmetic.py
+++ b/lib/test/apyfloatarray/test_arithmetic.py
@@ -702,7 +702,6 @@ def test_array_rdiv_scalar():
 
 @pytest.mark.float_array
 def test_transpose():
-
     # High-dimensional transor transposition not implemented
     with pytest.raises(ValueError, match="Not implemented: high-dimensional"):
         APyFloatArray([[[1]]], [[[1]]], [[[1]]], 3, 3).T
diff --git a/lib/test/apyfloatarray/test_constructors.py b/lib/test/apyfloatarray/test_constructors.py
index 5ddad92c1..2b1f58972 100644
--- a/lib/test/apyfloatarray/test_constructors.py
+++ b/lib/test/apyfloatarray/test_constructors.py
@@ -162,7 +162,7 @@ def test_from_numpy_raises():
     np = pytest.importorskip("numpy")
     a = np.asarray([[1e-323, float("inf")], [float("nan"), 0.0]], dtype="half")
     with pytest.raises(TypeError, match="APyFloatArray::_set_values_from_ndarray"):
-        b = APyFloatArray.from_float(a, 14, 60)
+        APyFloatArray.from_float(a, 14, 60)
 
 
 def test_c_striding():
diff --git a/lib/test/apyfloatarray/test_matmul.py b/lib/test/apyfloatarray/test_matmul.py
index 2643f3143..697b5ec89 100644
--- a/lib/test/apyfloatarray/test_matmul.py
+++ b/lib/test/apyfloatarray/test_matmul.py
@@ -12,7 +12,6 @@
 
 @pytest.mark.float_array
 def test_dimension_mismatch_raises():
-
     ##
     # Vector inner product mismatch
     #
diff --git a/lib/test/conftest.py b/lib/test/conftest.py
index 5771088e9..57d5f70dd 100644
--- a/lib/test/conftest.py
+++ b/lib/test/conftest.py
@@ -11,4 +11,4 @@ def pytest_collection_finish(session):
 
         print(f" * NumPy version: {np.__version__}")
     except ImportError:
-        print(f" * No NumPy support detected")
+        print(" * No NumPy support detected")
diff --git a/lib/test/test_broadcasting.py b/lib/test/test_broadcasting.py
index dd89db05f..0543e9d25 100644
--- a/lib/test/test_broadcasting.py
+++ b/lib/test/test_broadcasting.py
@@ -1,7 +1,6 @@
 from apytypes import APyFixedArray, APyFloatArray
 
 import pytest
-from typing import List, Tuple
 from itertools import product
 
 
diff --git a/lib/test/test_contextmanagers.py b/lib/test/test_contextmanagers.py
index 16c3ed8d0..93e28e16d 100644
--- a/lib/test/test_contextmanagers.py
+++ b/lib/test/test_contextmanagers.py
@@ -1,5 +1,17 @@
-import apytypes
-from apytypes import *
+from apytypes import (
+    APyFixedCastContext,
+    QuantizationMode,
+    OverflowMode,
+    APyFixed,
+    APyFixedArray,
+    APyFloatAccumulatorContext,
+    APyFixedAccumulatorContext,
+    APyFloatQuantizationContext,
+    get_float_quantization_mode,
+    get_float_quantization_seed,
+    set_float_quantization_mode,
+    set_float_quantization_seed,
+)
 import pytest
 
 
@@ -242,15 +254,15 @@ def test_raises(self):
     def test_with_quantization_context(self):
         """Test that the APyFloatAccumulatorContext interacts correctly with the APyFloatQuanizationContext."""
         with APyFloatQuantizationContext(QuantizationMode.TO_POS):
-            assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_POS
+            assert get_float_quantization_mode() == QuantizationMode.TO_POS
 
             with APyFloatAccumulatorContext(exp_bits=5, man_bits=3):
-                assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_POS
+                assert get_float_quantization_mode() == QuantizationMode.TO_POS
 
             with APyFloatAccumulatorContext(
                 exp_bits=5, man_bits=3, quantization=QuantizationMode.TO_NEG
             ):
-                assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_POS
+                assert get_float_quantization_mode() == QuantizationMode.TO_POS
 
 
 class TestQuantizationContext:
@@ -263,78 +275,64 @@ class TestQuantizationContext:
 
     def setup_class(self):
         """Save the current quantization mode so that it can be restored later for other tests."""
-        self.default_mode = apytypes.get_float_quantization_mode()
+        self.default_mode = get_float_quantization_mode()
 
     def teardown_class(self):
         """Restore quantization mode."""
-        apytypes.set_float_quantization_mode(self.default_mode)
+        set_float_quantization_mode(self.default_mode)
 
     def test_default_mode(self):
-        assert apytypes.get_float_quantization_mode() == QuantizationMode.TIES_EVEN
+        assert get_float_quantization_mode() == QuantizationMode.TIES_EVEN
 
     def test_prerequisite(self):
         """Make sure get/set works for the quantization mode to begin with."""
-        apytypes.set_float_quantization_mode(QuantizationMode.TO_POS)
-        assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_POS
-        apytypes.set_float_quantization_mode(QuantizationMode.TO_NEG)
-        assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_NEG
+        set_float_quantization_mode(QuantizationMode.TO_POS)
+        assert get_float_quantization_mode() == QuantizationMode.TO_POS
+        set_float_quantization_mode(QuantizationMode.TO_NEG)
+        assert get_float_quantization_mode() == QuantizationMode.TO_NEG
 
-        apytypes.set_float_quantization_seed(5)
-        assert apytypes.get_float_quantization_seed() == 5
-        apytypes.set_float_quantization_seed(1 << 57)
-        assert apytypes.get_float_quantization_seed() == (1 << 57)
+        set_float_quantization_seed(5)
+        assert get_float_quantization_seed() == 5
+        set_float_quantization_seed(1 << 57)
+        assert get_float_quantization_seed() == (1 << 57)
 
     def test_not_nested(self):
         """Single layer of context."""
         # Test setting a non-stochastic quantization mode
-        apytypes.set_float_quantization_mode(QuantizationMode.TO_POS)
+        set_float_quantization_mode(QuantizationMode.TO_POS)
         with APyFloatQuantizationContext(QuantizationMode.TO_NEG):
-            assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_NEG
-        assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_POS
+            assert get_float_quantization_mode() == QuantizationMode.TO_NEG
+        assert get_float_quantization_mode() == QuantizationMode.TO_POS
 
         # Test setting a stochastic quantization mode without changing the seed
-        apytypes.set_float_quantization_seed(123)
+        set_float_quantization_seed(123)
         with APyFloatQuantizationContext(QuantizationMode.STOCH_EQUAL):
-            assert (
-                apytypes.get_float_quantization_mode() == QuantizationMode.STOCH_EQUAL
-            )
-            assert apytypes.get_float_quantization_seed() == 123
-        assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_POS
-        assert apytypes.get_float_quantization_seed() == 123
+            assert get_float_quantization_mode() == QuantizationMode.STOCH_EQUAL
+            assert get_float_quantization_seed() == 123
+        assert get_float_quantization_mode() == QuantizationMode.TO_POS
+        assert get_float_quantization_seed() == 123
 
         # Test setting a stochastic quantization mode and changing the seed
         with APyFloatQuantizationContext(QuantizationMode.STOCH_WEIGHTED, 77):
-            assert (
-                apytypes.get_float_quantization_mode()
-                == QuantizationMode.STOCH_WEIGHTED
-            )
-            assert apytypes.get_float_quantization_seed() == 77
-        assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_POS
-        assert apytypes.get_float_quantization_seed() == 123
+            assert get_float_quantization_mode() == QuantizationMode.STOCH_WEIGHTED
+            assert get_float_quantization_seed() == 77
+        assert get_float_quantization_mode() == QuantizationMode.TO_POS
+        assert get_float_quantization_seed() == 123
 
     def test_nested(self):
         """Nested context layers."""
-        apytypes.set_float_quantization_mode(QuantizationMode.TO_POS)
-        apytypes.set_float_quantization_seed(123)
+        set_float_quantization_mode(QuantizationMode.TO_POS)
+        set_float_quantization_seed(123)
         with APyFloatQuantizationContext(QuantizationMode.STOCH_WEIGHTED, 456):
-            assert (
-                apytypes.get_float_quantization_mode()
-                == QuantizationMode.STOCH_WEIGHTED
-            )
-            assert apytypes.get_float_quantization_seed() == 456
+            assert get_float_quantization_mode() == QuantizationMode.STOCH_WEIGHTED
+            assert get_float_quantization_seed() == 456
             with APyFloatQuantizationContext(QuantizationMode.STOCH_EQUAL, 789):
-                assert (
-                    apytypes.get_float_quantization_mode()
-                    == QuantizationMode.STOCH_EQUAL
-                )
-                assert apytypes.get_float_quantization_seed() == 789
-            assert (
-                apytypes.get_float_quantization_mode()
-                == QuantizationMode.STOCH_WEIGHTED
-            )
-            assert apytypes.get_float_quantization_seed() == 456
-        assert apytypes.get_float_quantization_mode() == QuantizationMode.TO_POS
-        assert apytypes.get_float_quantization_seed() == 123
+                assert get_float_quantization_mode() == QuantizationMode.STOCH_EQUAL
+                assert get_float_quantization_seed() == 789
+            assert get_float_quantization_mode() == QuantizationMode.STOCH_WEIGHTED
+            assert get_float_quantization_seed() == 456
+        assert get_float_quantization_mode() == QuantizationMode.TO_POS
+        assert get_float_quantization_seed() == 123
 
     def test_raised_exception(self):
         """Make sure an exception is raised if a seed is given for APyFloatQuantizationContext with a non-stochastic quantization mode."""