diff --git a/pymbolic/geometric_algebra/__init__.py b/pymbolic/geometric_algebra/__init__.py
index 0758af2a35cde1a4772843abbefe7ef72473ae34..6a6ced186be52ae49514f9fa294a1df0af9ba6c3 100644
--- a/pymbolic/geometric_algebra/__init__.py
+++ b/pymbolic/geometric_algebra/__init__.py
@@ -49,10 +49,6 @@ Spaces
 
 .. autoclass:: Space
 
-    .. autoattribute:: dimensions
-    .. autoattribute:: is_orthogonal
-    .. autoattribute:: is_euclidean
-
 .. autofunction:: get_euclidean_space
 
 Multivectors
@@ -192,6 +188,13 @@ class Space:
     """
     .. autoattribute :: basis_names
     .. autoattribute :: metric_matrix
+
+    .. autoproperty :: dimensions
+    .. autoproperty :: is_orthogonal
+    .. autoproperty :: is_euclidean
+
+    .. automethod:: bits_and_sign
+    .. automethod:: blade_bits_to_str
     """
 
     basis_names: Sequence[str]
@@ -199,53 +202,60 @@ class Space:
 
     metric_matrix: np.ndarray
     """
-    A *(dims,dims)*-shaped matrix, whose *(i,j)*-th entry represents the
+    A *(dims, dims)*-shaped matrix, whose *(i, j)*-th entry represents the
     inner product of basis vector *i* and basis vector *j*.
     """
 
-    def __init__(self, basis=None, metric_matrix=None):
+    def __init__(self,
+                 basis: Sequence[str] | int | None = None,
+                 metric_matrix: np.ndarray | None = None) -> None:
         """
         :arg basis: A sequence of names of basis vectors, or an integer (the
             number of dimensions) to use the default names ``e0`` through ``eN``.
-        :arg metric_matrix: See :attr:`metric_matrix`.
-            If *None*, the Euclidean metric is assumed.
+        :arg metric_matrix: See :attr:`metric_matrix`. If *None*, the Euclidean
+            metric is assumed.
         """
 
         if basis is None and metric_matrix is None:
-            raise TypeError("at least one of 'basis' and 'metric_matrix' "
-                    "must be passed")
-
-        if basis is None:
-            basis = int(metric_matrix.shape[0])
+            raise TypeError(
+                "At least one of 'basis' and 'metric_matrix' must be given")
 
         from numbers import Integral
-        if isinstance(basis, Integral):
-            basis = [f"e{i}" for i in range(basis)]
+        if basis is None:
+            assert metric_matrix is not None
+            basis_names = [f"e{i}" for i in range(metric_matrix.shape[0])]
+        elif isinstance(basis, Integral):
+            basis_names = [f"e{i}" for i in range(basis)]
+        else:
+            assert not isinstance(basis, int)
+            basis_names = list(basis)
 
         if metric_matrix is None:
-            metric_matrix = np.eye(len(basis), dtype=object)
+            metric_matrix = np.eye(len(basis_names), dtype=object)
 
         if not (
                 len(metric_matrix.shape) == 2
-                and all(dim == len(basis) for dim in metric_matrix.shape)):
-            raise ValueError("metric_matrix has the wrong shape")
+                and all(dim == len(basis_names) for dim in metric_matrix.shape)):
+            raise ValueError(
+                f"'metric_matrix' has the wrong shape: {metric_matrix.shape}")
 
-        object.__setattr__(self, "basis_names", basis)
+        object.__setattr__(self, "basis_names", basis_names)
         object.__setattr__(self, "metric_matrix", metric_matrix)
 
     @property
     def dimensions(self) -> int:
+        """The dimension of the space."""
         return len(self.basis_names)
 
     @memoize_method
-    def bits_and_sign(self, basis_indices):
+    def bits_and_sign(self, basis_indices: Sequence[int]) -> tuple[int, int]:
         # assert no repetitions
         assert len(set(basis_indices)) == len(basis_indices)
 
         sorted_basis_indices = tuple(sorted(
                 (bindex, num)
                 for num, bindex in enumerate(basis_indices)))
-        blade_permutation = [num for bindex, num in sorted_basis_indices]
+        blade_permutation = [num for _, num in sorted_basis_indices]
 
         bits = 0
         for bi in basis_indices:
@@ -256,22 +266,25 @@ class Space:
     @property
     @memoize_method
     def is_orthogonal(self):
+        """*True* if the metric is orthogonal (i.e. diagonal)."""
         return (self.metric_matrix - np.diag(np.diag(self.metric_matrix)) == 0).all()
 
     @property
     @memoize_method
-    def is_euclidean(self):
+    def is_euclidean(self) -> bool:
+        """*True* if the metric matrix corresponds to the Eucledian metric."""
         return (self.metric_matrix == np.eye(self.metric_matrix.shape[0])).all()
 
-    def blade_bits_to_str(self, bits, outer_operator="^"):
+    def blade_bits_to_str(self, bits: int, outer_operator: str = "^") -> str:
         return outer_operator.join(
                     name
                     for bit_num, name in enumerate(self.basis_names)
                     if bits & (1 << bit_num))
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         if self is get_euclidean_space(self.dimensions):
             return f"Space({self.dimensions})"
+
         elif self.is_euclidean:
             return f"Space({self.basis_names!r})"
         else:
@@ -279,9 +292,8 @@ class Space:
 
 
 @memoize
-def get_euclidean_space(n):
-    """Return the canonical *n*-dimensional Euclidean :class:`Space`.
-    """
+def get_euclidean_space(n: int) -> Space:
+    """Return the canonical *n*-dimensional Euclidean :class:`Space`."""
     return Space(n)
 
 # }}}
@@ -564,6 +576,7 @@ class MultiVector(Generic[CoeffT]):
             data = {0: cast(CoeffT, data)}
 
         if space is None:
+            assert isinstance(dimensions, int)
             space = get_euclidean_space(dimensions)
         else:
             if dimensions is not None and space.dimensions != dimensions:
@@ -579,8 +592,12 @@ class MultiVector(Generic[CoeffT]):
             # data is in non-normalized non-bits tuple form
             new_data: dict[int, CoeffT] = {}
             for basis_indices, coeff in data.items():
+                assert isinstance(basis_indices, tuple)
+
                 bits, sign = space.bits_and_sign(basis_indices)
-                new_coeff = new_data.setdefault(bits, cast(CoeffT, 0)) + sign*coeff
+                new_coeff = cast(CoeffT,
+                    new_data.setdefault(bits, cast(CoeffT, 0))  # type: ignore[operator]
+                    + sign*coeff)
 
                 if is_zero(new_coeff):
                     del new_data[bits]