diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c4bee32d9ccc61227af740382e87d4f8f5df3774..11a0ee01be424756d5efacd61fc2583929378b95 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,12 +3,34 @@
 #   * Author: (c) 2011-present GitLab B.V.
 #   * URL: https://docs.gitlab.com/ee/ci/docker/using_kaniko.html#building-a-docker-image-with-kaniko
 #   * License: CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0)
+stages:
+    - test
+    - build
+
+variables:
+  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+
+testing:
+    image: python:latest
+    cache: 
+      paths:
+        - .cache/pip
+    stage: test
+    before_script:
+        - pip install pipenv
+        - pipenv requirements --dev > requirements.txt
+        - pip install -r requirements.txt 
+    script:
+        - black --check $CI_PROJECT_DIR
+        - mypy .
 
 docker-build:
     image:
         name: gcr.io/kaniko-project/executor:debug
         entrypoint: [""]
     stage: build
+    # Run build stage regardless of the status of the test stage
+    when: always
     before_script:
         - mkdir -p /kaniko/.docker
         - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
diff --git a/README.md b/README.md
index 9dd644dc1d3b309f83482f8d2d0d43fa2302c1fb..88295a7d4e27878a3368dae5a0994705a340036f 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,17 @@ Alembic must always be up to date in order to update the database schema.
 Please run `alembic upgrade head` after pulling changes from the repository.
 ---
 
+## Compliance
+
+### Formatting
+
+We use [black](https://github.com/psf/black) as formatting tool.
+Use `black .` to run the formatter.
+
+### Static Type Checking
+
+Run `mypy .` to let [mypy](https://www.mypy-lang.org/) validate the Python types.
+
 ## How to move around in this repo
 
 ## `/portal/`: Main Application
diff --git a/portal/__init__.py b/portal/__init__.py
index 642054e1eae3ce4ca477b4d960c33113987b6bb5..99937d5f6c96730cb3a75a2f8c00ab45734ba5c5 100644
--- a/portal/__init__.py
+++ b/portal/__init__.py
@@ -9,7 +9,7 @@ from flask_sqlalchemy import SQLAlchemy
 
 from .db_config import get_database_uri, get_uri_for_sqlite
 
-db = SQLAlchemy()
+db: SQLAlchemy = SQLAlchemy()
 
 from .model import *
 
diff --git a/portal/model/FlaskConfigEntry.py b/portal/model/FlaskConfigEntry.py
index 378aed8320af2ee381777e3eb8bcbee363bd60b1..bfd3cd9243325bfe148e217d6fbe76d5e915109e 100644
--- a/portal/model/FlaskConfigEntry.py
+++ b/portal/model/FlaskConfigEntry.py
@@ -1,9 +1,12 @@
 from flask import Flask
+from flask_sqlalchemy import DefaultMeta
 
 from portal import db
 
+BaseModel: DefaultMeta = db.Model
 
-class FlaskConfigEntry(db.Model):
+
+class FlaskConfigEntry(BaseModel):
     """
     A configuration entry for Flask (a key-value pair) as persisted in a database
     """
diff --git a/portal/model/Retrievable.py b/portal/model/Retrievable.py
index 1b7c6495c41b7e1aace3d3628fa627b34ae3b56a..b15aa785dfc5b6f29957927fb13c2beda329b501 100644
--- a/portal/model/Retrievable.py
+++ b/portal/model/Retrievable.py
@@ -1,12 +1,16 @@
 from abc import abstractmethod
 from typing import Any, List, Optional, Self
+from flask_sqlalchemy import DefaultMeta
+from sqlalchemy import Column
 
 from flask import g
 
 from portal import db
 
+BaseModel: DefaultMeta = db.Model
 
-class Retrievable(db.Model):
+
+class Retrievable(BaseModel):
     """
     A type whose instances can be retrieved from the database.
 
@@ -17,7 +21,7 @@ class Retrievable(db.Model):
     __abstract__ = True
 
     @classmethod
-    def get_canonical_order_column(cls) -> Optional[db.Column]:
+    def get_canonical_order_column(cls) -> Optional[Column]:
         """
         Return the colum by which instances of this type should be canonically ordered
         when retrieving them all from the database.