diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000000000000000000000000000000000000..85d6c253c145ec3e78086116e5e1f976b8a16b57
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,90 @@
+from typing import Sequence
+import nox
+
+nox.options.sessions = ["lint", "typecheck", "testsuite", "report_coverage"]
+
+
+def add_pystencils_git(session: nox.Session):
+    """Clone the pystencils 2.0 development branch and install it in the current session"""
+    cache_dir = session.cache_dir
+
+    pystencils_dir = cache_dir / "pystencils"
+    if not pystencils_dir.exists():
+        session.run_install(
+            "git",
+            "clone",
+            "--branch",
+            "v2.0-dev",
+            "--single-branch",
+            "git@i10git.cs.fau.de:pycodegen/pystencils.git",
+            pystencils_dir,
+            external=True,
+        )
+    session.install("-e", str(pystencils_dir))
+
+
+def editable_install(session: nox.Session, opts: Sequence[str] = ()):
+    add_pystencils_git(session)
+    if opts:
+        opts_str = "[" + ",".join(opts) + "]"
+    session.install("-e", f".{opts_str}")
+
+
+@nox.session(tags=["qa", "code-quality"])
+def lint(session: nox.Session):
+    """Lint code using flake8"""
+
+    session.install("flake8")
+    session.run("flake8", "src/pystencilssfg")
+
+
+@nox.session(tags=["qa", "code-quality"])
+def typecheck(session: nox.Session):
+    """Run MyPy for static type checking"""
+    editable_install(session)
+    session.install("mypy")
+    session.run("mypy", "src/pystencilssfg")
+
+
+@nox.session(tags=["tests"])
+def testsuite(session: nox.Session):
+    """Run the testsuite and measure coverage."""
+    editable_install(session, ["testsuite"])
+    session.run(
+        "pytest",
+        "-v",
+        "--cov=src/pystencilssfg",
+        "--cov-report=term",
+        "--cov-config=pyproject.toml",
+    )
+
+
+@nox.session(tags=["report"])
+def report_coverage(session: nox.Session):
+    """Produce HTML and XML coverage reports"""
+    session.install("coverage[toml]")
+    session.run("coverage", "html")
+    session.run("coverage", "xml")
+
+
+@nox.session(tags=["docs"])
+def docs(session: nox.Session):
+    """Build the documentation pages"""
+    editable_install(session, ["docs"])
+    session.chdir("docs")
+    session.run("make", "html", external=True)
+
+
+@nox.session(default=False)
+def dev(session: nox.Session):
+    """Set up the development environment at .venv"""
+
+    session.install("virtualenv")
+    session.run("virtualenv", ".venv", "--prompt", "pystencils-sfg")
+    session.run(
+        ".venv/bin/pip",
+        "install",
+        "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev",
+        external=True,
+    )
+    session.run(".venv/bin/pip", "install", "-e", ".[dev]", external=True)
diff --git a/pyproject.toml b/pyproject.toml
index b787da5c7180e32c6751bc8c78c2532f693382e2..da36a11c4a19d510faadc491045485594790c535 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,10 +23,15 @@ requires = [
 build-backend = "setuptools.build_meta"
 
 [project.optional-dependencies]
-tests = [
-    "flake8>=6.1.0",
-    "mypy>=1.7.0",
+dev = [
+    "flake8",
+    "mypy",
     "black",
+    "clang-format",
+]
+testsuite = [
+    "pytest",
+    "pytest-cov",
     "pyyaml",
     "requests",
     "fasteners",
@@ -60,7 +65,7 @@ omit = [
 
 [tool.coverage.report]
 exclude_also = [
-    "\\.\\.\\.",
+    "\\.\\.\\.\n",
     "if TYPE_CHECKING:",
     "@(abc\\.)?abstractmethod",
 ]