diff --git a/templates/gitlab-ci-python.yml b/templates/gitlab-ci-python.yml
index a36dd6a4fc3357c6d732509db36dfc7dde9e3cd1..c9faab5f86a251ea9654be30773b93b2b88166c1 100644
--- a/templates/gitlab-ci-python.yml
+++ b/templates/gitlab-ci-python.yml
@@ -955,7 +955,7 @@ stages:
   - production
 
 ###############################################################################################
-#                                      Generic python job                                     #
+#                                      Generic python jobs                                    #
 ###############################################################################################
 .python-base:
   image: $PYTHON_IMAGE
@@ -980,6 +980,25 @@ stages:
     - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
     - cd ${PYTHON_PROJECT_DIR}
     - guess_build_system
+    - mkdir -p -m 777 reports
+
+.python-test:
+  extends: .python-base
+  stage: build
+  coverage: /^TOTAL.+?(\d+(?:\.\d+)?\%)$/
+  artifacts:
+    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
+    expire_in: 1 day
+    when: always
+    reports:
+      junit:
+        - "$PYTHON_PROJECT_DIR/reports/TEST-*.xml"
+      coverage_report:
+        coverage_format: cobertura
+        path: "$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml"
+    paths:
+      - "$PYTHON_PROJECT_DIR/reports/TEST-*.xml"
+      - "$PYTHON_PROJECT_DIR/reports/py-coverage.*"
 
 ###############################################################################################
 #                                      build stage                                             #
@@ -1001,7 +1020,6 @@ py-lint:
   extends: .python-base
   stage: build
   script:
-    - mkdir -p -m 777 reports
     - install_requirements
     - _pip install pylint_gitlab # codeclimate reports
     # run pylint and generate reports all at once
@@ -1062,7 +1080,6 @@ py-ruff:
   extends: .python-base
   stage: build
   script:
-    - mkdir -p -m 777 reports
     - |
       if [[ ${BANDIT_ENABLED} == "true" || ${PYLINT_ENABLED} == "true" || ${PYTHON_ISORT_ENABLED} == "true" ]]; then
         log_warn "Ruff can replace isort, Bandit, Pylint"
@@ -1095,7 +1112,6 @@ py-ruff-format:
   extends: .python-base
   stage: build
   script:
-    - mkdir -p -m 777 reports
     - |
       if [[ ${PYTHON_BLACK_ENABLED} == "true" ]]; then
         log_warn "Ruff can replace Black"
@@ -1115,7 +1131,6 @@ py-mypy:
   variables:
     MYPY_CACHE_DIR: "$CI_PROJECT_DIR/.cache/mypy"
   script:
-    - mkdir -p -m 777 reports
     - install_requirements
     - _pip install mypy mypy-to-codeclimate
     - _run mypy ${MYPY_ARGS} ${MYPY_FILES:-$(find -type f -name "*.py" -not -path "./.cache/*" -not -path "./.venv/*")} | tee reports/py-mypy.console.txt || true
@@ -1139,10 +1154,8 @@ py-mypy:
 #                                      test stage                                             #
 ###############################################################################################
 py-unittest:
-  extends: .python-base
-  stage: build
+  extends: .python-test
   script:
-    - mkdir -p -m 777 reports
     - install_requirements
     # code coverage
     - _pip install coverage
@@ -1151,20 +1164,6 @@ py-unittest:
     - _run coverage run -m xmlrunner discover -o "reports/" $UNITTEST_ARGS
     - _run coverage report -m
     - _run coverage xml -o "reports/py-coverage.cobertura.xml"
-  coverage: /^TOTAL.+?(\d+\%)$/
-  artifacts:
-    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
-    expire_in: 1 day
-    when: always
-    reports:
-      junit:
-        - "$PYTHON_PROJECT_DIR/reports/TEST-*.xml"
-      coverage_report:
-        coverage_format: cobertura
-        path: "$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml"
-    paths:
-      - "$PYTHON_PROJECT_DIR/reports/TEST-*.xml"
-      - "$PYTHON_PROJECT_DIR/reports/py-coverage.*"
   rules:
     # skip if $UNITTEST_ENABLED not set
     - if: '$UNITTEST_ENABLED != "true"'
@@ -1172,27 +1171,11 @@ py-unittest:
     - !reference [.test-policy, rules]
 
 py-pytest:
-  extends: .python-base
-  stage: build
+  extends: .python-test
   script:
-    - mkdir -p -m 777 reports
     - install_requirements
     - _pip install pytest pytest-cov coverage
     - _python -m pytest --junit-xml=reports/TEST-pytests.xml --cov --cov-report term  --cov-report xml:reports/py-coverage.cobertura.xml ${PYTEST_ARGS}
-  coverage: /^TOTAL.+?(\d+\%)$/
-  artifacts:
-    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
-    expire_in: 1 day
-    when: always
-    reports:
-      junit:
-        - "$PYTHON_PROJECT_DIR/reports/TEST-*.xml"
-      coverage_report:
-        coverage_format: cobertura
-        path: "$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml"
-    paths:
-      - "$PYTHON_PROJECT_DIR/reports/TEST-*.xml"
-      - "$PYTHON_PROJECT_DIR/reports/py-coverage.*"
   rules:
     # skip if $PYTEST_ENABLED not set
     - if: '$PYTEST_ENABLED != "true"'
@@ -1200,26 +1183,10 @@ py-pytest:
     - !reference [.test-policy, rules]
 
 py-nosetests:
-  extends: .python-base
-  stage: build
+  extends: .python-test
   script:
-    - mkdir -p -m 777 reports
     - install_requirements
     - _run nosetests --with-xunit --xunit-file=reports/TEST-nosetests.xml --with-coverage --cover-erase --cover-xml --cover-xml-file=reports/py-coverage.cobertura.xml ${NOSETESTS_ARGS}
-  coverage: /^TOTAL.+?(\d+\%)$/
-  artifacts:
-    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
-    expire_in: 1 day
-    when: always
-    reports:
-      junit:
-        - "$PYTHON_PROJECT_DIR/reports/TEST-*.xml"
-      coverage_report:
-        coverage_format: cobertura
-        path: "$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml"
-    paths:
-      - "$PYTHON_PROJECT_DIR/reports/TEST-*.xml"
-      - "$PYTHON_PROJECT_DIR/reports/py-coverage.*"
   rules:
     # skip if $NOSETESTS_ENABLED not set
     - if: '$NOSETESTS_ENABLED != "true"'
@@ -1233,7 +1200,6 @@ py-bandit:
   # force no dependencies
   dependencies: []
   script:
-    - mkdir -p -m 777 reports
     - install_requirements
     - _pip install bandit
     # CSV (for SonarQube)
@@ -1269,7 +1235,6 @@ py-trivy:
   # force no dependencies
   dependencies: []
   script:
-    - mkdir -p -m 777 reports
     - |
       if [[ -z "$PYTHON_TRIVY_DIST_URL" ]]
       then
@@ -1348,7 +1313,6 @@ py-sbom:
   dependencies: []
   needs: []
   script:
-    - mkdir -p -m 777 reports
     - |
       case "$PYTHON_BUILD_SYSTEM" in
         poetry*|pipenv*)