diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 241614390b38a3321f8fcf818592e76f9590ef3f..20272ac5a1a967adf4f32aa65d6778176d3cf38b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -19,6 +19,15 @@ build-template:
     - cookiecutter --no-input --overwrite-if-exists --output-dir . .
     - cd my_awesome_app
     - git init
+    - git config user.email "ci-runner@example.com"
+    - git config user.name "CI Runner"
+    - source ./setup.sh
+    - ls -lah
+    - tox --version
+    - pip install --upgrade tox
+    - tox -e fix
+    - rm -r .venv
+    - rm -r .tox
   # cannot use needs, for artifacts on child pipeline so must regenerate template!
   artifacts:
     paths:
diff --git a/cookiecutter.json b/cookiecutter.json
index 35e5d91c6ad2f6d25e5c40fd3d73251ac85909c4..8165b9764968afc310e18f5850f8b960c9c95a88 100644
--- a/cookiecutter.json
+++ b/cookiecutter.json
@@ -1,6 +1,7 @@
 {
     "project_name": "My Awesome App",
     "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}",
+    "project_package": "{{ cookiecutter.project_name.lower()|replace(' ', '-')|replace('_', '-')|replace('.', '-')|trim() }}",
     "project_url": "https://git.astron.nl/{{ cookiecutter.project_slug }}",
     "description": "An example package for CI/CD working group"
 }
diff --git a/project.gitlab-ci.yml b/project.gitlab-ci.yml
index 2d6eb4238cb4eed070a86b57386656f0806b1f4b..740497a5bdb6a0b706f980cba8867aa4f45d7ee9 100644
--- a/project.gitlab-ci.yml
+++ b/project.gitlab-ci.yml
@@ -5,6 +5,7 @@
 # The generated gitlab-ci.yml from this `build-template` job is used for the actual
 # trigger include to prevent including jobs that still contain template arguments
 
+
 trigger_prepare:
   rules:
     - if: $CI_PIPELINE_SOURCE == "parent_pipeline"
@@ -15,17 +16,7 @@ default:
   before_script:
     - cd my_awesome_app
 
-run_black:
-  needs:
-    - pipeline: $PARENT_PIPELINE_ID
-      job: build-template
-
-run_flake8:
-  needs:
-    - pipeline: $PARENT_PIPELINE_ID
-      job: build-template
-
-run_pylint:
+run_lint:
   needs:
     - pipeline: $PARENT_PIPELINE_ID
       job: build-template
@@ -128,4 +119,4 @@ publish_to_readthedocs:
 release_job:
   needs:
     - pipeline: $PARENT_PIPELINE_ID
-      job: build-template
\ No newline at end of file
+      job: build-template
diff --git a/{{cookiecutter.project_slug}}/.dockerignore b/{{cookiecutter.project_slug}}/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..141f90da2fac6459ee10150e4cd3e12aff08a48b
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/.dockerignore
@@ -0,0 +1,4 @@
+.tox
+build
+*.egg-info
+.venv
diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore
index d5c650e6aee0559e7deb69be50a0dc534473d458..3f64dc50366ebf99b714795cd8bd590a452bc05a 100644
--- a/{{cookiecutter.project_slug}}/.gitignore
+++ b/{{cookiecutter.project_slug}}/.gitignore
@@ -2,6 +2,7 @@ dist/*
 *.egg-info
 *.pyc
 .tox
+.venv
 
 .coverage
 coverage.xml
diff --git a/{{cookiecutter.project_slug}}/.gitlab-ci.yml b/{{cookiecutter.project_slug}}/.gitlab-ci.yml
index 337675520d9e8488af18c0e56178f643b637c926..b4e3610be0cd53f508c1915a5d45989564a1cbdb 100644
--- a/{{cookiecutter.project_slug}}/.gitlab-ci.yml
+++ b/{{cookiecutter.project_slug}}/.gitlab-ci.yml
@@ -36,22 +36,10 @@ trigger_prepare:
     strategy: depend
     include: .prepare.gitlab-ci.yml
 
-run_black:
+run_lint:
   stage: lint
   script:
-    - tox -e black
-  allow_failure: true
-
-run_flake8:
-  stage: lint
-  script:
-    - tox -e pep8
-  allow_failure: true
-
-run_pylint:
-  stage: lint
-  script:
-    - tox -e pylint
+    - tox -e lint
   allow_failure: true
 
 # build_extensions:
@@ -91,7 +79,7 @@ run_unit_tests:
     - tox -e py3${PY_VERSION}
   parallel:
     matrix: # use the matrix for testing
-      - PY_VERSION: [8, 9, 10, 11]
+      - PY_VERSION: [9, 10, 11, 12]
 
 # Run code coverage on the base image thus also performing unit tests
 run_unit_tests_coverage:
diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ce46e6463265d2ad9704a24ae7f467cb7b50d8d9
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
@@ -0,0 +1,22 @@
+default_stages: [ pre-commit, pre-push ]
+default_language_version:
+  python: python3
+exclude: '^docs/.*\.py$'
+
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.5.0
+    hooks:
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+      - id: check-yaml
+      - id: check-toml
+      - id: detect-private-key
+  - repo: local
+    hooks:
+      - id: tox-lint
+        name: tox-lint (local)
+        entry: tox
+        language: python
+        types: [file, python]
+        args: ["-e",  "lint", "--"]
diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md
index eb72cc45252e58acb7ef6c402be0444c062aa212..25f215207307771b9fefe5658653fe4816828721 100644
--- a/{{cookiecutter.project_slug}}/README.md
+++ b/{{cookiecutter.project_slug}}/README.md
@@ -29,8 +29,16 @@ from {{cookiecutter.project_slug}} import cool_module
 cool_module.greeter()   # prints "Hello World"
 ```
 
-## Contributing
+## Development
 
+### Development environment
+
+To setup and activte the develop environment run ```source ./setup.sh``` from within the source directory.
+
+If PyCharm is used, this only needs to be done once.
+Afterward the Python virtual env can be setup within PyCharm.
+
+### Contributing
 To contribute, please create a feature branch and a "Draft" merge request.
 Upon completion, the merge request should be marked as ready and a reviewer
 should be assigned.
diff --git a/{{cookiecutter.project_slug}}/bin/install-hooks/pre-commit.sh b/{{cookiecutter.project_slug}}/bin/install-hooks/pre-commit.sh
new file mode 100755
index 0000000000000000000000000000000000000000..792a3aabef83dc4ebf0c94635ae1de0e7412c479
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/bin/install-hooks/pre-commit.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+if [ ! -f "setup.sh" ]; then
+  echo "pre-commit.sh must be executed with repository root as working directory!"
+  exit 1
+fi
+
+pre-commit install --hook-type pre-push
diff --git a/{{cookiecutter.project_slug}}/docker/ci-runner/Dockerfile b/{{cookiecutter.project_slug}}/docker/ci-runner/Dockerfile
index 6cb46437656aad64d8292c0a10f6edaffffba8e3..6063354805d6e2e35534ee5d09f80b51f856495b 100644
--- a/{{cookiecutter.project_slug}}/docker/ci-runner/Dockerfile
+++ b/{{cookiecutter.project_slug}}/docker/ci-runner/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.12
+FROM python:3.13
 
 RUN python -m pip install --upgrade pip
 RUN python -m pip install --upgrade tox twine
diff --git a/{{cookiecutter.project_slug}}/docker/{{cookiecutter.project_slug}}/Dockerfile b/{{cookiecutter.project_slug}}/docker/{{cookiecutter.project_slug}}/Dockerfile
index ceea7e721fc3acaa124184e709986e04dbd9a3dd..1f7eb275020d13ed45cb7b0983949c12161ff953 100644
--- a/{{cookiecutter.project_slug}}/docker/{{cookiecutter.project_slug}}/Dockerfile
+++ b/{{cookiecutter.project_slug}}/docker/{{cookiecutter.project_slug}}/Dockerfile
@@ -1,6 +1,6 @@
 ARG BUILD_ENV=no_copy
 
-FROM python:3.11 AS build_no_copy
+FROM python:3.13 AS build_no_copy
 ADD ../../requirements.txt .
 COPY ../.. /work
 RUN rm -r /work/dist | true
@@ -8,11 +8,11 @@ RUN python -m pip install --user tox
 WORKDIR /work
 RUN python -m tox -e build
 
-FROM python:3.11 AS build_copy
+FROM python:3.13 AS build_copy
 COPY dist /work/dist
 
 FROM build_${BUILD_ENV} AS build
 
-FROM python:3.11-slim
+FROM python:3.13-slim
 COPY --from=build /work/dist /dist
 RUN python -m pip install /dist/*.whl
diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml
index 063c6b8c424a023260e012745fb447470329ade1..f9ef33d81867a69427453175611c432118349119 100644
--- a/{{cookiecutter.project_slug}}/pyproject.toml
+++ b/{{cookiecutter.project_slug}}/pyproject.toml
@@ -1,6 +1,6 @@
 [build-system]
 requires = [
-    "setuptools>=62.6",
+    "setuptools>=70.0",
     "setuptools_scm[toml]>=8.0",
     "wheel"
 ]
@@ -9,5 +9,76 @@ build-backend = "setuptools.build_meta"
 [tool.setuptools_scm]
 version_file = "{{cookiecutter.project_slug}}/_version.py"
 
-[tool.pylint]
-ignore = "_version.py"
+[tool.ruff]
+exclude = [
+    ".venv",
+    ".git",
+    ".tox",
+    "dist",
+    "docs",
+    "*lib/python*",
+    "*egg",
+    "_version.py"
+]
+
+[tool.ruff.lint]
+ignore = ["E203"]
+
+[tool.tox]
+# Generative environment list to test all supported Python versions
+requires = ["tox>=4.21"]
+env_list = ["fix", "coverage", "lint", "format", "py{13, 12, 11, 10}"]
+
+[tool.tox.env_run_base]
+package = "editable"
+deps = [
+    "-r{toxinidir}/requirements.txt",
+    "-r{toxinidir}/tests/requirements.txt"]
+set_env = { LANGUAGE = "en_US", LC_ALL = "en_US.UTF-8", PYTHONWARNINGS = "default::DeprecationWarning" }
+commands = [["python", "--version"], ["python", "-m", "pytest", "tests/{posargs}"]]
+
+[tool.tox.env.fix]
+description = "format the code base to adhere to our styles, and complain about what we cannot do automatically"
+skip_install = true
+deps = ["pre-commit-uv>=4.1.1"]
+commands = [["pre-commit", "run", "--all-files", "--show-diff-on-failure"]]
+
+[tool.tox.env.coverage]
+commands = [
+    ["python", "--version"],
+    ["python", "-m", "pytest", "--cov-report", "term", "--cov-report", "xml", "--cov-report", "html", "--cov={{cookiecutter.project_slug}}", "tests/{posargs}"]]
+
+# Command prefixes to reuse the same virtualenv for all linting jobs.
+[tool.tox.env.lint]
+deps = [
+    "ruff",
+    "-r{toxinidir}/tests/requirements.txt"]
+commands = [
+    ["python", "-m", "ruff", "--version"],
+    ["python", "-m", "ruff", "check", { replace = "posargs", default = ["{{cookiecutter.project_slug}}", "tests"], extend = true }]
+]
+
+[tool.tox.env.format]
+deps = [
+    "ruff",
+    "-r{toxinidir}/tests/requirements.txt"]
+commands = [
+    ["python", "-m", "ruff", "format", "-v", { replace = "posargs", default = ["{{cookiecutter.project_slug}}", "tests"], extend = true }]
+]
+
+[tool.tox.env.docs]
+deps = [
+    "-r{toxinidir}/requirements.txt",
+    "-r{toxinidir}/docs/requirements.txt"]
+# unset LC_ALL / LANGUAGE from testenv, would fail sphinx otherwise
+set_env = ""
+changedir = "{tox_root}"
+commands = [
+    ["python", "docs/cleanup.py"],
+    ["sphinx-build", "-b", "html", "docs/source", "docs/build/html"]
+]
+
+[tool.tox.env.build]
+package = "wheel"
+deps = ["build>=0.8.0"]
+commands = [["python", "-m", "build"]]
diff --git a/{{cookiecutter.project_slug}}/requirements.txt b/{{cookiecutter.project_slug}}/requirements.txt
index 8f81bc285d0db9ea0945151fed0cd47eec4ee6a2..24ce15ab7ead32f98c7ac3edcd34bb2010ff4326 100644
--- a/{{cookiecutter.project_slug}}/requirements.txt
+++ b/{{cookiecutter.project_slug}}/requirements.txt
@@ -1,2 +1 @@
-importlib-metadata>=0.12, <5.0;python_version<"3.8"
 numpy
diff --git a/{{cookiecutter.project_slug}}/setup.cfg b/{{cookiecutter.project_slug}}/setup.cfg
index 5755b7a8b2f78bfd0d3f0d2651353b67220e5913..1755bbc3daeb8065aa1addc1b75f1bd0584858e6 100644
--- a/{{cookiecutter.project_slug}}/setup.cfg
+++ b/{{cookiecutter.project_slug}}/setup.cfg
@@ -1,5 +1,5 @@
 [metadata]
-name = {{cookiecutter.project_slug}}
+name = {{cookiecutter.project_package}}
 description = {{cookiecutter.description}}
 long_description = file: README.md
 long_description_content_type = text/markdown
@@ -15,11 +15,10 @@ classifiers =
     Programming Language :: Python
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3 :: Only
-    Programming Language :: Python :: 3.8
-    Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
     Programming Language :: Python :: 3.11
     Programming Language :: Python :: 3.12
+    Programming Language :: Python :: 3.13
     Topic :: Internet :: WWW/HTTP
     Topic :: Internet :: WWW/HTTP :: Dynamic Content
     Topic :: Scientific/Engineering
@@ -28,10 +27,6 @@ classifiers =
 [options]
 include_package_data = true
 packages = find:
-python_requires = >=3.8
+python_requires = >=3.10
 install_requires = file: requirements.txt
 
-[flake8]
-max-line-length = 88
-extend-ignore = E203
-exclude=.venv,.git,.tox,dist,docs,*lib/python*,*egg,_version.py
diff --git a/{{cookiecutter.project_slug}}/setup.py b/{{cookiecutter.project_slug}}/setup.py
index b908cbe55cb344569d32de1dfc10ca7323828dc5..10fdaec810e96f0f1cbedb4a5ddf532c03f50dc4 100644
--- a/{{cookiecutter.project_slug}}/setup.py
+++ b/{{cookiecutter.project_slug}}/setup.py
@@ -1,3 +1,7 @@
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+""" Setuptools entry point  """
 import setuptools
 
 setuptools.setup()
diff --git a/{{cookiecutter.project_slug}}/setup.sh b/{{cookiecutter.project_slug}}/setup.sh
new file mode 100755
index 0000000000000000000000000000000000000000..4548910b6ba00d9f564aea2ac5ddfab50c1d77db
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/setup.sh
@@ -0,0 +1,30 @@
+#! /usr/bin/env bash
+#
+# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+#
+
+# This file's directory is used to determine the station control directory
+# location.
+if [ -z ${BASH_SOURCE} ]; then
+  BASH_SOURCE=${(%):-%x}
+fi
+
+ABSOLUTE_PATH=$(realpath $(dirname ${BASH_SOURCE}))
+
+# Create a virtual environment directory if it doesn't exist
+VENV_DIR="${ABSOLUTE_PATH}/.venv"
+if [ ! -d "$VENV_DIR" ]; then
+    echo "Creating virtual environment..."
+    python3 -m venv "$VENV_DIR"
+fi
+
+# Activate the virtual environment
+source "$VENV_DIR/bin/activate"
+python -m pip install pre-commit
+python -m pip install "tox>=4.21.0"
+
+# Install git pre-commit pre-push hook
+if [ ! -f "${ABSOLUTE_PATH}/.git/hooks/pre-push.legacy" ]; then
+  source "${ABSOLUTE_PATH}/bin/install-hooks/pre-commit.sh"
+fi
diff --git a/{{cookiecutter.project_slug}}/tests/requirements.txt b/{{cookiecutter.project_slug}}/tests/requirements.txt
index f14d0b907ce977320ba250046dfab52a3101800a..b507faf8c6cc09660c41c58caa26d318e94cfd4d 100644
--- a/{{cookiecutter.project_slug}}/tests/requirements.txt
+++ b/{{cookiecutter.project_slug}}/tests/requirements.txt
@@ -1,7 +1,2 @@
-autopep8 >= 1.7.0 # MIT
-black >= 22.0.0 # MIT
-build >= 0.8.0 # MIT
-flake8 >= 5.0.0 # MIT
-pylint >= 2.15.0 # GPLv2
 pytest >= 7.0.0 # MIT
 pytest-cov >= 3.0.0 # MIT
diff --git a/{{cookiecutter.project_slug}}/tox.ini b/{{cookiecutter.project_slug}}/tox.ini
deleted file mode 100644
index 5132bd5451807aea8ab5480b5c5671b5daf67c1a..0000000000000000000000000000000000000000
--- a/{{cookiecutter.project_slug}}/tox.ini
+++ /dev/null
@@ -1,58 +0,0 @@
-[tox]
-# Generative environment list to test all supported Python versions
-envlist = py3{9,10,11,12,13},black,pep8,pylint
-min_version = 4.3.3
-requires =
-    tox-ignore-env-name-mismatch >= 0.2.0
-
-[testenv]
-usedevelop = True
-package = wheel
-wheel_build_env = .pkg
-
-setenv =
-    PYTHONWARNINGS=default::DeprecationWarning
-deps =
-    -r{toxinidir}/requirements.txt
-    -r{toxinidir}/tests/requirements.txt
-commands =
-    {envpython} --version
-    {envpython} -m pytest tests/{posargs}
-
-[testenv:coverage]
-commands =
-    {envpython} --version
-    {envpython} -m pytest --cov-report term --cov-report xml --cov-report html --cov={{cookiecutter.project_slug}} tests/{posargs}
-
-# Use generative name and command prefixes to reuse the same virtualenv
-# for all linting jobs.
-[testenv:{pep8,black,pylint,format}]
-usedevelop = False
-package = editable
-envdir = {toxworkdir}/linting
-commands =
-    pep8: {envpython} -m flake8 --version
-    pep8: {envpython} -m flake8 {{cookiecutter.project_slug}} tests
-    black: {envpython} -m black --version
-    black: {envpython} -m black --check --diff {{cookiecutter.project_slug}} tests
-    pylint: {envpython} -m pylint --version
-    pylint: {envpython} -m pylint {{cookiecutter.project_slug}} tests
-    format: {envpython} -m autopep8 -v -aa --in-place --recursive {{cookiecutter.project_slug}}
-    format: {envpython} -m autopep8 -v -aa --in-place --recursive tests
-    format: {envpython} -m black -v {{cookiecutter.project_slug}} tests
-
-[testenv:docs]
-; unset LC_ALL / LANGUAGE from testenv, would fail sphinx otherwise
-setenv =
-deps =
-    -r{toxinidir}/requirements.txt
-    -r{toxinidir}/docs/requirements.txt
-changedir = {toxinidir}
-commands =
-    {envpython} docs/cleanup.py
-    sphinx-build -b html docs/source docs/build/html
-
-[testenv:build]
-usedevelop = False
-deps = build
-commands = {envpython} -m build