diff --git a/conftest.py b/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1d0cdda1b64ec9eff93367589fe3a08d975deb7
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,11 @@
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def prepare_composer(doctest_namespace):
+    from pystencilssfg import SfgContext, SfgComposer
+
+    #   Place a composer object in the environment for doctests
+
+    sfg = SfgComposer(SfgContext())
+    doctest_namespace["sfg"] = sfg
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 84c2b779b553fe22fa8ef23a2f0a4872c9edb57c..11d64fd5a142eab3f1558c52944b772f94a5f266 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -27,6 +27,7 @@ extensions = [
     "myst_parser",
     "sphinx.ext.autodoc",
     "sphinx.ext.napoleon",
+    "sphinx.ext.doctest",
     "sphinx.ext.intersphinx",
     "sphinx_autodoc_typehints",
     "sphinx_design",
@@ -64,6 +65,13 @@ intersphinx_mapping = {
 autodoc_member_order = "bysource"
 autodoc_typehints = "description"
 
+#   Doctest Setup
+
+doctest_global_setup = '''
+from pystencilssfg import SfgContext, SfgComposer
+sfg = SfgComposer(SfgContext())
+'''
+
 
 #   Prepare code generation examples
 
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..94a3a6c5cd76967b094d0e26bab983291057461d
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,8 @@
+[pytest]
+testpaths = src/pystencilssfg tests/
+python_files = "test_*.py"
+#   Need to ignore the generator scripts, otherwise they would be executed
+#   during test collection
+addopts = --doctest-modules --ignore=tests/generator_scripts/scripts
+
+doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py
index 7fda6c9ae2b115bbafdc40ee670625ceec61a578..6a2472067ddb746a4d0700599c1541491d86e4a9 100644
--- a/src/pystencilssfg/composer/basic_composer.py
+++ b/src/pystencilssfg/composer/basic_composer.py
@@ -280,6 +280,18 @@ class SfgBasicComposer(SfgIComposer):
         """Create a variable with given name and data type."""
         return AugExpr(create_type(dtype)).var(name)
 
+    def vars(self, names: str, dtype: UserTypeSpec) -> tuple[AugExpr, ...]:
+        """Create multiple variables with given names and the same data type.
+
+        Example:
+
+        >>> sfg.vars("x, y, z", "float32")
+        (x, y, z)
+
+        """
+        varnames = names.split(",")
+        return tuple(self.var(n.strip(), dtype) for n in varnames)
+
     def init(self, lhs: VarLike) -> SfgInplaceInitBuilder:
         """Create a C++ in-place initialization.
 
@@ -298,10 +310,37 @@ class SfgBasicComposer(SfgIComposer):
         """
         return SfgInplaceInitBuilder(_asvar(lhs))
 
-    def expr(self, fmt: str, *deps, **kwdeps):
+    def expr(self, fmt: str, *deps, **kwdeps) -> AugExpr:
         """Create an expression while keeping track of variables it depends on.
-        
-        
+
+        This method is meant to be used similarly to `str.format`; in fact,
+        it calls `str.format` internally and therefore supports all of its
+        formatting features.
+        In addition, however, the format arguments are scanned for *variables*
+        (e.g. created using `var`), which are attached to the expression.
+        This way, *pystencils-sfg* keeps track of any variables an expression depends on.
+
+        :Example:
+
+            >>> x, y, z, w = sfg.vars("x, y, z, w", "float32")
+            >>> expr = sfg.expr("{} + {} * {}", x, y, z)
+            >>> expr
+            x + y * z
+
+            You can look at the expression's dependencies:
+
+            >>> sorted(expr.depends, key=lambda v: v.name)
+            [x: float, y: float, z: float]
+
+            If you use an existing expression to create a larger one, the new expression
+            inherits all variables from its parts:
+
+            >>> expr2 = sfg.expr("{} + {}", expr, w)
+            >>> expr2
+            x + y * z + w
+            >>> sorted(expr2.depends, key=lambda v: v.name)
+            [w: float, x: float, y: float, z: float]
+
         """
         return AugExpr.format(fmt, *deps, **kwdeps)
 
@@ -338,7 +377,9 @@ class SfgBasicComposer(SfgIComposer):
 
     def set_param(self, param: VarLike | sp.Symbol, expr: ExprLike):
         depends = _depends(expr)
-        var = _asvar(param) if isinstance(param, _VarLike) else param
+        var: SfgVar | sp.Symbol = (
+            _asvar(param) if isinstance(param, _VarLike) else param
+        )
         return SfgDeferredParamSetter(var, depends, str(expr))
 
     def map_param(
@@ -351,7 +392,9 @@ class SfgBasicComposer(SfgIComposer):
         side object from one or multiple right-hand side dependencies."""
         if isinstance(depends, _VarLike):
             depends = [depends]
-        lhs_var = _asvar(param) if isinstance(param, _VarLike) else param
+        lhs_var: SfgVar | sp.Symbol = (
+            _asvar(param) if isinstance(param, _VarLike) else param
+        )
         return SfgDeferredParamMapping(
             lhs_var, set(_asvar(v) for v in depends), mapping
         )
@@ -363,7 +406,7 @@ class SfgBasicComposer(SfgIComposer):
             lhs_components: Vector components as a list of symbols.
             rhs: A `SrcVector` object representing a vector data structure.
         """
-        components = [
+        components: list[SfgVar | sp.Symbol] = [
             (_asvar(c) if isinstance(c, _VarLike) else c) for c in lhs_components
         ]
         return SfgDeferredVectorMapping(components, rhs)
diff --git a/src/pystencilssfg/ir/source_components.py b/src/pystencilssfg/ir/source_components.py
index 2eab9935ae7cc5be7fa2d6a766ed22bd2ee121dd..859f9266de18fb952405721892136bde3fda3fd9 100644
--- a/src/pystencilssfg/ir/source_components.py
+++ b/src/pystencilssfg/ir/source_components.py
@@ -250,7 +250,7 @@ class SfgVar:
         return self._name
 
     def __repr__(self) -> str:
-        return f"SfgVar( {self._name}, {repr(self._dtype)} )"
+        return f"{self._name}: {self._dtype}"
 
 
 SymbolLike_T = TypeVar("SymbolLike_T", bound=KernelParameter)
diff --git a/src/pystencilssfg/lang/expressions.py b/src/pystencilssfg/lang/expressions.py
index ca5a78c4d0794d33a2b22827b7a5e9d5a09e47d4..948bbd68601a26e91afc61af3fe771cc85ef2b00 100644
--- a/src/pystencilssfg/lang/expressions.py
+++ b/src/pystencilssfg/lang/expressions.py
@@ -132,6 +132,9 @@ class AugExpr:
         else:
             return str(self._bound)
 
+    def __repr__(self) -> str:
+        return str(self)
+
     def _bind(self, expr: DependentExpression):
         if self._bound is not None:
             raise SfgException("Attempting to bind an already-bound AugExpr.")