Ga naar inhoud

Statische Analyse - Linting, Formatting & Type Checking

NOTES

Deze pagina plaatst statische analyse vroeg in de module — nog vóór de testpiramide. De redenering:

  • Statische analyse geeft onmiddellijk nuttige feedback in VS Code, zonder dat je code moet uitvoeren.
  • Het is een natuurlijke brug naar de DevOps-module: linting en type checking zijn de eerste quality gates in een CI-pipeline.
  • Tooling als Ruff en mypy/ty integreert rechtstreeks met het Python-ecosysteem dat cursisten al kennen.

Plaats in het lessenplan: vooraan, aansluitend op of parallel met de DevOps-start. De testpiramide en unit tests volgen hierna.

Ondernemerscompetenties: Kwaliteit bewaken, nauwkeurig werken, systematisch problemen oplossen.

Beoordeling competentie → criteria:

  • Codekwaliteit bewaken
    • → Past linting, formatting en type checking toe als eerste codekwaliteitsstap.
  • Tooling integreren in eigen workflow
    • → Configureert Ruff en een type checker in VS Code en in een eenvoudige CI-pipeline.

Waarom statische analyse?

Statische analyse bekijkt je code als tekst — zonder de code ook maar één keer uit te voeren. Een tool als Ruff of mypy bouwt een model van je broncode en zoekt naar patronen die op problemen wijzen. Dat is het tegenovergestelde van dynamische analyse: testen die de code wél uitvoeren en controleren wat er dan gebeurt.

Het grote voordeel zit in de feedback-loop: typ een fout en je editor reageert meteen — nog vóór je eerste run, vóór je eerste test, vóór iemand anders je code ooit ziet.

Er is tegenwoordig nog een extra reden: AI coding agents. Een AI-assistent genereert in seconden code, maar die output wil je niet blind vertrouwen. Statische analyse biedt dan een snelle, objectieve controle: ruff check en mypy geven een ondubbelzinnig groen of rood, zonder dat iemand de code moet nalezen. En omdat de foutmeldingen zo precies zijn (bestand, regel, code), kan de agent er zelf mee aan de slag — genereren, checken, fixen, opnieuw checken — tot de checks groen zijn.

  • Eerste verdedigingslinie in een kwaliteitsstrategie.
  • Vroeg feedback: fouten en inconsistenties zichtbaar tijdens het typen in de editor.
  • Direct nuttig in teamcontext: afdwingbare stijlregels, geen discussie over formatting.
  • Brug naar DevOps: linting en type checking als eerste quality gates in CI.

De tools in je project

De voorbeelden hieronder draaien de tools los met uvx — daarmee test je een tool uit zonder ze te installeren. In een echt project zet je de tools echter best vast als dev-dependencies, zodat elke teamgenoot — en CI — dezelfde versie gebruikt:

Terminal
$ uv add --dev ruff ty

De --dev-vlag maakt van Ruff en ty ontwikkelingsafhankelijkheden: nodig tijdens het coderen, niet om de applicatie te draaien. Collega's en CI krijgen ze binnen via uv sync. Wil je een tool écht globaal installeren, dan gebruik je uv tool install <tool> — daarna volstaat het gewone commando (ruff check .).

In een project vervang je uvx door uv run: uv run ruff check .. Datzelfde commando zie je straks terug in de CI-pipeline.


Linting

Wat is linting?

Een linter vergelijkt je code met een verzameling regels en rapporteert elke overtreding — telkens met een code die je kan opzoeken (bv. F401) en meestal meteen kan herstellen. De regels lopen van puur esthetisch (spaties, regellengte) tot echte bugs.

  • Controle op codeconventies, potentiële bugs en code smells.
  • Gaat verder dan enkel stijl: detecteert ongebruikte variabelen, onbereikbare code, foute imports, ...

Ruff

Ruff is vandaag de meest gebruikte linter in het Python-ecosysteem — en de tool die we in deze opleiding standaard gebruiken. Het combineert de functionaliteit van een tiental oudere tools in één snelle executable:

  • Moderne, razendsnelle linter én formatter in één tool.
  • Geschreven in Rust — ordes van grootte sneller dan oudere Python-linters.
  • Van Astral — hetzelfde bedrijf achter uv (onze package manager). Consistentie in de toolchain.
  • Vervangt Flake8, isort, pyupgrade, pydocstyle, ... in één dependency.
  • Configuratie via pyproject.toml:
pyproject.toml
[tool.ruff]
# Target Python-versie
target-version = "py314"

[tool.ruff.lint]
# Activeer gewenste regels
select = ["E", "F", "I", "N", "W"]

De gekozen codes zijn regelgroepen: E en W volgen PEP 8 (stijl), F vindt potentiële bugs (ongebruikte imports, ongedefinieerde namen), I sorteert imports en N controleert naamgeving. Die code zie je telkens terug in de output — zo weet je meteen om welk soort probleem het gaat. Met uv run ruff check . lint je het hele project; --fix herstelt automatisch wat corrigeerbaar is.

Voorbeeld: wat Ruff ziet

Stel, je schrijft deze code — het ziet er op het eerste gezicht oké uit:

users.py
import os
import json  # (1)

def get_users():
    users = []
    data = json.load("users.json")
    for name in data:
        user = name.upper()
        users.append(usr)  # (2)
    return users
  1. os is geïmporteerd maar nooit gebruikt — Ruff vinkt dat af met F401. Opvallend: de json-regel haalt de linter wél door — json.load bestaat, maar verwacht een geopend bestand, geen pad-string. Dat is een runtime-fout; ook statische analyse vangt niet alles.
  2. Typfout: usr bestaat niet; de variabele heet user. Ruff detecteert ongedefinieerde namen.
Terminal
$ uvx ruff check users.py
users.py:1:8: F401 [*] `os` imported but unused
users.py:9:23: F821 Undefined name `usr`
Found 2 errors.
[*] 1 fixable with the `--fix` option.

Zonder de code ook maar één keer uit te voeren, weet je al dat er twee problemen zijn. Dat is de kracht van linting.

VS Code-integratie

Installeer de extensie Ruff (charliermarsh.ruff). Je krijgt dezelfde waarschuwingen live in de editor — rode en gele kronkellijntjes onder de probleemcode, met uitleg in het Problems-paneel (Cmd+Shift+M).

Stel Ruff in als default formatter in .vscode/settings.json:

{
  "editor.defaultFormatter": "charliermarsh.ruff",
  "ruff.lint.enable": true
}

Andere en oudere linters

Vóór Ruff bestond linting uit een handvol losse tools, elk met een eigen configuratie en een eigen CI-stap. Je komt die namen nog geregeld tegen in oudere codebases en tutorials — herken ze, maar gebruik ze niet meer:

Tool Wat het doet Status vandaag
Flake8 Linting (pyflakes + pycodestyle + mccabe) Vervangen door Ruff
Pylint Uitgebreide statische analyse Nog actief, trager
isort Import-sortering Vervangen door Ruff
pydocstyle Docstring-conventies Vervangen door Ruff
Waarom niet gewoon Flake8 + isort + Black?

Enkele jaren geleden was de standaardcombinatie Flake8 (linting) + isort (imports) + Black (formatting). Dat zijn drie tools met elk hun eigen configuratie, versiebeheer en CI-stap.

Ruff doet het werk van alle drie — én meer — in één tool, met één configuratieblok, en is 10–100× sneller.


Formatting

Wat is formatting?

Formatting draait om consistentie, niet om smaak: een formatter herschrijft je code automatisch naar één vaste stijl, zodat élk bestand er hetzelfde uitziet — van wie het ook komt. Jij typt, de formatter beslist.

  • Automatisch herschrijven van code naar een consistente stijl.
  • Geen discussie meer over spaties, aanhalingstekens of regellengte — de formatter beslist.
  • Belangrijk in teamcontext: elke commit ziet er hetzelfde uit, diffs blijven leesbaar.

Ruff Format

Het verschil met linten: ruff check zegt wat er niet mag, ruff format herschrijft de code naar hoe het eruit moet zien. In de editor gebeurt dat automatisch; in CI gebruik je ruff format --check . — die wijzigt niets, maar faalt zolang er bestanden niet netjes zijn.

  • Ruff heeft een ingebouwde formatter (ruff format) die dezelfde stijlregels volgt als Black.
  • Zelfde configuratieblok in pyproject.toml, minimale toevoeging:
pyproject.toml
[tool.ruff.format]
# Zelfde instellingen als Black als standaard
quote-style = "double"
indent-style = "space"
line-ending = "auto"

Voorbeeld: voor en na formattering

Zelfs als code werkt, kan de opmaak rommelig en inconsistent zijn:

report.py — voor formatting
def generate_report(data,title, include_header=True  ):
      if include_header:
        print(  f"=== {title.upper()} ===" )
      for item in data:
          print(f"- {item['name']}: {item['value']}")

result=[   {"name":'Alice',"value":42},
{"name":"Bob","value":7}  ]

generate_report(result,"Mijn Rapport")

Geen syntaxfouten — het draait. Maar kijk naar de wisselende aanhalingstekens, willekeurige spaties, en inconsistente inspringing. In een teamcontext leidt dit tot ruis in diffs en eindeloze stijldiscussies.

Terminal
$ uvx ruff format report.py
1 file reformatted
report.py — na ruff format
def generate_report(data, title, include_header=True):
    if include_header:
        print(f"=== {title.upper()} ===")
    for item in data:
        print(f"- {item['name']}: {item['value']}")


result = [{"name": "Alice", "value": 42}, {"name": "Bob", "value": 7}]

generate_report(result, "Mijn Rapport")

Alle inconsistenties zijn verdwenen — dubbele quotes, consistente spaties rond operatoren, nette inspringing. De formatter beslist, het team discussieert niet meer.

VS Code-integratie

Zet format-on-save aan zodat elke keer dat je opslaat de code automatisch geformatteerd wordt:

.vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "charliermarsh.ruff"
}

Je typt, je slaat op (Cmd+S), en de code staat meteen netjes. Geen extra stap nodig.

Andere en oudere formatters

Hetzelfde verhaal als bij de linters: Black was jarenlang de standaard en Ruff Format volgt dezelfde stijl — maar geïntegreerd in de tool die we al gebruiken. De overige namen duiken enkel nog op in oude documentatie:

Tool Wat het doet Status vandaag
Black De-facto standaard formatter tot ±2023 Vervangen door Ruff Format
isort Import-sortering (ook door Ruff gedaan) Vervangen door Ruff
autopep8 PEP 8-correcties Zelden nog gebruikt

Statische Type Checking

Wat is static type checking?

Type hints zijn optioneel — Python draait even goed zonder. Maar hints maken je code leesbaarder én geven de checker houvast om voor elke variabele in je programma te berekenen welk type er op dat moment zit — zonder de code uit te voeren. Daardoor vangt type checking hele klassen bugs af die je anders pas veel later (of nooit) zou ontdekken.

  • Python is dynamisch getypeerd: types worden pas gecontroleerd tijdens uitvoering.
  • Met type hints (def greet(name: str) -> str:) voeg je optionele type-informatie toe.
  • Een static type checker leest die hints en detecteert typefouten zonder de code te runnen.
  • Vangt hele klassen fouten af: None-gerelateerde bugs, verkeerde argumenttypes, ontbrekende attributen.

Python probeert het beste van beide te combineren — snel ontwikkelen met dynamische types, en zekerheid waar het loont. Wanneer loont statische typing?

Situatie Dynamisch (zonder hints) Statisch (met hints + checker)
Snel prototypen Minder werk — gewoon coderen Extra werk, beperkte meerwaarde
Klein script Prima — houdt het simpel Overkill
Grote codebase Kwetsbaar — types verglijden ongemerkt Fouten vroeg gevonden
Refactoring Geen vangnet Elke breuk komt aan het licht
Teamcontext Types impliciet — veel heen-en-weer lezen Hints documenteren de code

Voorbeeld: een bug die pas in productie opduikt

zonder_type_hints.py
def get_discount(price, percentage):
    return price * (1 - percentage / 100)


def format_price(amount):
    return f"€ {amount:.2f}"


# Dit lijkt te werken...
print(format_price(get_discount(100, 20)))  # € 80.00

# ...tot iemand dit doet:
print(format_price(get_discount(100, "20")))  # TypeError op regel 3!

De fout zit in get_discount: percentage wordt gedeeld door 100, maar niemand garandeert dat het een getal is. Met type hints had de type checker dit gevonden voordat de code ooit draaide:

met_type_hints.py
def get_discount(price: float, percentage: float) -> float:
    return price * (1 - percentage / 100)


def format_price(amount: float) -> str:
    return f"€ {amount:.2f}"


print(format_price(get_discount(100, "20")))
#                    ^^^^^^^^^^^^^^^^^^^^^^^
#  error: Argument 2 to "get_discount" has incompatible type "str";
#  expected "float"                                      [arg-type]

Zonder de code te runnen zie je de fout al. Geen productie-incident, geen late-night debug-sessie.

Veelgemaakte fout: str = None

def greet(name: str = None):  # Fout: None is geen string
    ...

None is een waarde, geen string — elke type checker vinkt dit af. Wil je een écht optioneel argument, gebruik dan een union:

def greet(name: str | None = None):
    if name is None:
        name = "stranger"
    return f"Hello {name}"
VS Code-integratie

Type checking in VS Code werkt out-of-the-box via Pylance (Microsoft's taalserver, ingebouwd in de Python-extensie). Die gebruikt Pyright onder de motorkap — je krijgt live typefouten in de editor zonder iets te installeren.

Stel zelf hoe streng Pylance mag zijn in .vscode/settings.json:

{
  "python.analysis.typeCheckingMode": "basic"
}
  • "off": geen type checking
  • "basic": bij twijfel reken je het correct — een goede start
  • "strict": bij twijfel reken je het fout — maximaal streng

Leerzaam extraatje: zet inlay hints aan en Pylance toont de afgeleide types inline in je code:

{
  "python.analysis.inlayHints.functionReturnTypes": true,
  "python.analysis.inlayHints.variableTypes": true
}

Tip: de extensie Error Lens toont foutmeldingen direct op de regel zelf, in plaats van enkel in het Problems-paneel.

Wil je expliciet mypy of ty gebruiken, dan kan dat via een task of de bijbehorende extensie:

  • Mypy: extensie matangover.mypy
  • Ty: nog in ontwikkeling; voor nu via de terminal (uvx ty .)

Voorlopig is Pylance/Pyright meer dan voldoende voor de type checking in je editor. De mypy/ty-stap komt later in CI aan bod.

Mypy

Mypy is al meer dan tien jaar de referentie voor Python type checking en wordt nog steeds actief onderhouden. Je draait ze met uvx mypy ., configureert ze via [tool.mypy] in pyproject.toml, en elke foutmelding bestaat uit bestand, regelnummer en foutcode:

Terminal
$ uvx mypy met_type_hints.py
met_type_hints.py:9: error: Argument 2 to "get_discount" has incompatible type "str"; expected "float" [arg-type]
Found 1 error in 1 file
  • De gevestigde standaard voor Python static type checking.
  • Ontwikkeld door Dropbox, later overgenomen door de Python-community.
  • Zeer volwassen, uitgebreide documentatie, grote community.
  • Ietwat trager op grote codebases (Python-gebaseerd).

Ty

Ty levert dezelfde foutmeldingen als mypy, maar in Rust-snelheid — en binnen het ecosysteem dat we al gebruiken (uv, Ruff). Draaien is identiek (uvx ty .), met dezelfde output en foutcodes; het verschil zit vooral in snelheid en toekomstperspectief.

  • Nieuwe static type checker van Astral — hetzelfde bedrijf als uv en Ruff.
  • Geschreven in Rust, dus aanzienlijk sneller dan mypy.
  • Momenteel in actieve ontwikkeling; nog niet volledig feature-compleet.
  • Het voordeel van één ecosysteem: uv + Ruff + ty delen dezelfde filosofie, snelheid en configuratiestijl.
Mypy of ty — wat kiezen we?

Beide zijn geldige keuzes in 2026:

  • Mypy is volwassener, beter gedocumenteerd en de veiligste keuze voor productiecode.
  • Ty is sneller, moderner en past in het Astral-ecosysteem dat we al gebruiken (uv, Ruff).

Voor deze opleiding is de keuze minder belangrijk dan het concept. Wat telt is dát je static type checking toepast — de tool is inwisselbaar. We tonen voorbeelden met beide, maar gebruiken in oefeningen en CI-configuraties hoofdzakelijk ty om consistent te blijven met de rest van de Astral-toolchain.

Wil je mypy gebruiken in je eigen projecten, dan is dat uiteraard ook prima.

Andere type checkers

Naast mypy en ty bestaan nog andere checkers, elk gebouwd door een groot techbedrijf voor het eigen ecosysteem. De kans dat je ze nodig hebt is klein, maar de namen duiken soms op in vacatures en documentatie:

Tool Ontwikkeld door Kenmerk
Pyright Microsoft Gebruikt door VS Code's Pylance
Pyre Meta (Facebook) Geoptimaliseerd voor zeer grote codebases
Pytype Google Inferentie zonder type hints

Integratie in CI/CD

Alles wat je lokaal in de editor ziet, kan je ook als geautomatiseerde controles laten draaien op elke push of pull request. Zo bewaakt de repository zelf de kwaliteit: niemand kan nog "vergeten" te linten of te typen — de pipeline doet het voor je, op een schone omgeving.

  • Linting, formatting en type checking zijn de eerste stappen in elke CI-pipeline.
  • Ze zijn snel (seconden, geen minuten) — dus ideale fast-fail gates.
  • Dit is een eerste concrete toepassing van CI, nog vóór we tests toevoegen.
  • Later, in de DevOps-module, breiden we deze pipeline uit met testen en deployment.

GitHub Actions — minimale pipeline

Het volledige bestand past in vijftien regels: de helft is boilerplate, de drie run-regels zijn de eigenlijke kwaliteitscontroles:

.github/workflows/quality.yml
name: Code Quality

on: [push, pull_request]  # (1)

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4  # (2)
      - uses: astral-sh/setup-uv@v5  # (3)
      - run: uv sync --frozen  # (4)
      - run: uv run ruff check .  # (5)
      - run: uv run ruff format --check .  # (6)
      - run: uv run ty .  # (7)
  1. De workflow draait bij elke push en elk pull request — dus ook op branches, vóór een merge.
  2. Haalt de code van de repository binnen op de runner.
  3. Installeert uv op de runner.
  4. Installeert alle dependencies uit uv.lock, inclusief de dev-tools (Ruff, ty).
  5. Lint: faalt de check, dan faalt de workflow.
  6. Format: --check wijzigt niets, maar faalt zolang er bestanden anders geformatteerd zouden worden.
  7. Type checking met ty — of mypy, de keuze is inwisselbaar.

Drie commando's, drie quality gates: faalt er één, dan hangt er een rood kruis aan je push of pull request — en de logs vertellen precies welke regel geschonden is. Zo belandt een typefout of een slordig geformatteerd bestand nooit ongemerkt in main.

Oefening: probeer het zelf

  1. Voeg ruff en ty toe aan je project als dev-dependencies: uv add --dev ruff ty.
  2. Schrijf een klein bestand met bewust slechte code: een ongebruikte import, een variabele die niet bestaat en wisselende aanhalingstekens.
  3. Draai uv run ruff check . — los op wat je kan met uv run ruff check --fix . en draai daarna nog eens. Wat blijft er over en waarom?
  4. Draai uv run ruff format . en bekijk wat er veranderde.
  5. Voeg aan één functie type hints toe en draai uv run ty . — kan je de checker groen krijgen?

Samenvatting

Drie gereedschappen, één doel: fouten zo vroeg mogelijk vangen — liefst vóór je collega's ermee in aanraking komen. Linting en formatting houden code consistent en controleerbaar; type checking vangt een hele klasse fouten die linting niet ziet. Geen van de drie voert je code uit, dus geen van de drie vervangt testen.

  • Statische analyse is de eerste stap in een codekwaliteitsstrategie.
  • Ruff dekt linting én formatting in één snelle tool.
  • De tools zet je het best vast als dev-dependencies in je project — teamgenoten en CI draaien dan exact dezelfde versies.
  • Een static type checker (mypy of ty) detecteert typefouten zonder de code te runnen.
  • Beide integreer je direct in VS Code voor real-time feedback én in CI als quality gate.
  • Ze vormen ook de ideale feedback-loop voor AI coding agents: objectieve checks waarmee de agent zijn eigen output verifieert en verbetert.
  • Dit is geen vervanging van testen — het is een aanvulling die fouten vangt vóórdat je tests überhaupt relevant zijn.

Verdere lezing