Ga naar inhoud

Type Annotaties in Python

NOTES

Deze pagina legt de theoretische en praktische basis voor type annotations, vóór we in FastAPI/Pydantic induiken. De redenering:

  • FastAPI en Pydantic draaien volledig op type hints - zonder deze basis begrijpen studenten enkel de magie, niet het mechanisme.
  • De static_analysis-pagina in de Testing-module toont de tooling (mypy, ty); deze pagina zoomt uit naar het waarom en hoe.
  • De pagina is zelfstandig bruikbaar: geen FastAPI-kennis vereist, enkel Python-basis.

Plaats in het lessenplan: eerste of tweede les van de Backend-module, vóór de eerste FastAPI-pagina.

Ondernemerscompetenties: Systematisch problemen oplossen, nauwkeurig werken, technisch inzicht vertalen naar kwaliteit.

Beoordeling competentie → criteria:

  • Type annotations correct toepassen
    • → Schrijft functies en variabelen met correcte type hints; kan uitleggen waarom een bepaald type gekozen is.
  • Typefouten herkennen en oplossen
    • → Detecteert en corrigeert typefouten met behulp van een static type checker.

Dynamisch vs. Statisch Getypeerde Talen

Elke waarde in een programma heeft een type (int, string, list, enz.). Talen verschillen in wanneer ze controleren of types correct gebruikt worden.

Definities

Statisch Getypeerd

Statisch getypeerde talen (Java, C++, TypeScript) controleren types tijdens het compileren, voordat het programma draait. Je definieert meestal expliciet het type van een variabele.

Bvb. in Java:

int x = 5;
x = "hello"; // Compilatiefout voordat het programma draait

Dynamisch Getypeerd

Dynamisch getypeerde talen (Python, JavaScript, Ruby) controleren types tijdens de uitvoering van het programma. Geen declaraties nodig.

x = 42
print(f"x = {x} and has type = {type(x)}")
# x = 42 and has type = <class 'int'>

x = "hello"
print(f"x = {x} and has type = {type(x)}")
# x = hello and has type = <class 'str'>

x = [1, 2, 3]
print(f"x = {x} and has type = {type(x)}")
# x = [1, 2, 3] and has type = <class 'list'>

De variabele x heeft zelf geen vast type; het type zit in het object waarnaar x op dat moment wijst.

Voor- en nadelen

Er is geen 'beste' optie en beide systemen hebben voor- en nadelen!

Statisch getypeerd Dynamisch getypeerd
Voordelen Vangt typefouten vroeger op, vóór uitvoering Minder boilerplate, sneller schrijven en prototypen
Betere autocomplete en refactoring-tools Flexibeler (bvb. functies die meerdere types aankunnen)
Vaak sneller tijdens runtime dankzij optimalisatie Kan eenvoudiger zijn om te leren
Nadelen Uitgebreider; elke variabele heeft een type-declaratie Typefouten pas merkbaar tijdens uitvoering
Code moet gecompileerd worden voor uitvoering Moeilijker om fouten vooraf te detecteren
Kan star aanvoelen voor snelle scripts of prototyping Kan trager zijn tijdens uitvoering

'Typefouten pas merkbaar tijdens uitvoering'

We zoomen even in op dit erg belangrijke nadeel van dynamisch getypeerde talen.

FAST_SHIPPING_COST = "10.00"

def shipping_total(subtotal, fast):
    if fast:
        return subtotal + FAST_SHIPPING_COST
    return subtotal

print(shipping_total(50.0, False)) # (1)!
print(shipping_total(50.0, True))  # (2)!
  1. ✅ Dit werkt zonder problemen.
  2. ❌ TypeError! Programma crasht.

Een programma dat deze module gebruikt, werkt zonder problemen totdat de fast-optie gebruikt wordt!

Python's middenweg: type annotaties

Sinds Python 3.5 kun je optioneel types annoteren bij variabelen, functie-argumenten en functie-returnwaarden.

def greet(name: str, age: int) -> str:
    return f"{name} is {age} years old"

Let op!

Deze type annotaties hebben geen invloed op de uitvoering van het programma!

Je kan perfect greet(5, "old") aanroepen en dit zal zonder fouten uitgevoerd worden. In plaats van annotaties wordt dan ook meestal gesproken over type hints.

Waarom zou je dan de moeite doen om deze hints te gebruiken?! Er zijn meerdere goede redenen en deze bekijken we hierna in detail.

  • Betere ervaring in code editors / IDEs.
  • Interpreteerbaar door extra tools om op voorhand fouten op te sporen.
  • Code eenvoudiger te begrijpen (zowel voor mensen als voor AI!). Zelfdocumenterende code.
  • Geavanceerd gebruik in combinatie met bvb. Pydantic.

Voordelen van type hints

Betere editor-ervaring

Link met backend???

Wat heeft dit nu eigenlijk allemaal te maken met backend-applicaties?

In deze backend-module gaan we FastAPI gebruiken als framework om backend-services te bouwen in Python.

En FastAPI is sterk gebaseerd op het gebruik van type hints!

De kernfilosofie van FastAPI ligt in het verlengde van Pydantic: dat type hints niet alleen documentatie zijn, maar uitvoerbare specificaties: dezelfde modellen (types) die je definieert voor input en output zorgen automatisch voor validatie, foutmeldingen én interactieve API-documentatie.

Voordat we FastAPI bekijken, is het dus een must om het gebruik van die annotaties grondig te beheersen.

Type hints zijn zó belangrijk voor FastAPI dat de documentatie van het project met precies dat onderwerp start! Die documentatie is bijzonder goed gemaakt, dus maken we er hier gewoon gebruik van.

We nemen samen de FastAPI-introductie door tot aan het "Pydantic models" deel (daar komen we in een volgende les pas op terug).

Samengevat: type hints geven je editor de mogelijkheid om:

  • Autocomplete te geven die weet wat erin zit: first_name. toont meteen de juiste str-methodes.
  • Fouten te onderstrepen terwijl je typt - nog vóór je opslaat.
  • Refactoring te ondersteunen: met type hints vindt de editor referenties van attributen en methodes betrouwbaarder.
  • Informatie te tonen: je ziet het type en de docstring zonder naar de definitie te springen.

Fouten vangen vóór uitvoering

Een static type checker zoals mypy of ty scant je volledige codebase in seconden en rapporteert typefouten - zonder ook maar één regel uit te voeren.

We komen op het einde van dit hoofdstuk terug op deze tools, maar hieronder zie je alvast het resultaat voor het shipping_total-voorbeeld dat we eerder zagen (deze keer met type hints!):

shipping.py
FAST_SHIPPING_COST = "10.00"

def shipping_total(subtotal: float, fast: bool) -> float:
    if fast:
        return subtotal + FAST_SHIPPING_COST
    return subtotal

print(shipping_total(50.0, False))
print(shipping_total(50.0, True))
print(shipping_total("50.0", False))
uvx mypy shipping.pyshipping.py:5: error: Unsupported operand types for + ("float" and "str") [operator]
shipping.py:10: error: Argument 1 to "shipping_total" has incompatible type "str"; expected "float" [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Levende documentatie, betere samenwerking

Type hints zijn documentatie die niet kan verouderen. Een docstring kan achterblijven bij de code; een type hint wordt door de checker geverifieerd:

# Dit zegt wat de functie doet - maar kan liegen:
def get_users(active_only):
    """Return active users. active_only: bool, returns list"""
    ...

# Dit zegt wat de functie doet - en de checker garandeert het:
def get_users(active_only: bool) -> list[dict[str, str]]:
    ...

Een nieuwe collega hoeft de broncode niet in te duiken om te weten wat de functie verwacht en teruggeeft. De handtekening alleen vertelt het verhaal.

In teamcontext betekenen type hints:

  • Minder misverstanden. Het functievoorschrift is een contract: dit gaat erin, dat komt eruit.
  • Snellere code reviews. Reviewers hoeven niet mentaal uit te voeren wat er gebeurt - de types vertellen het verhaal.
  • Veiligere merges. CI draait de type checker; een typefout blokkeert de merge. Geen discussie nodig.
  • Makkelijkere onboarding. Nieuwe teamleden kunnen de codebase verkennen via types in plaats van via debuggen.

Hoe annoteren

In de FastAPI-introductie hebben we al de volledige basis gezien en herhalen we hier dus niet alles.

Wanneer wel en niet annoteren

De belangrijkste plaats voor type hints is in functievoorschriften. Immers, binnen een functie gebruik je meestal andere functies. Als die andere functies ook type hints hebben, dan heeft het weinig zin de resulterende variabele opnieuw te annoteren.

def greet_loudly(name: str) -> str:
    loud_name = name.upper()  # (1)!
    return f"HELLO, {loud_name}!"

print(greet_loudly("Alice"))
  1. loud_name: str = name.upper() kan ook, maar is overbodig

Start dus altijd in de eerste plaats met je functies te annoteren. Daarna kan je de volgende regel gebruiken:

Annoteer enkel wat je editor nog niet kent!

Wanneer je in VS Code met je muis over een functie-argument of variabele gaat staan, krijg je te zien wat VS Code daar al weet. Bijvoorbeeld bij name in het bovenstaande voorbeeld, maar dus ook bij loud_name: (variable) loud_name: str. Zo niet, dan krijg je bvb. (variable) foo: Unknown te zien.

Combinatie met een default-waarde

Heeft een functieargument een default-waarde, dan bepaalt deze meestal ook direct het type van dat argument. Toch is het gebruikelijk om ook een type hint te gebruiken. De type hint komt in dit geval vóór het =-teken.

def shipping_total(subtotal: float, fast: bool = False) -> float:
    if fast:
        return subtotal + FAST_SHIPPING_COST
    return subtotal

Collections (Generics)

Voor lijsten, dicts, sets en tuples specificeer je het type van de inhoud tussen vierkante haken:

names: list[str] = ["Alice", "Bob"]
scores: list[float] = [8.5, 9.0, 7.5]
config: dict[str, int] = {"timeout": 30, "retries": 3}
unique_ids: set[int] = {1, 2, 3}
coordinate: tuple[float, float] = (51.05, 3.72)

Union: |

Vaak kan een waarde meerdere types hebben - het meest voorkomende patroon is "iets of None":

def find_user(name: str) -> dict[str, str] | None:
    ...
    return None  # geen gebruiker gevonden

De |-operator lees je als "of": dict | None betekent "een dictionary of None".

# Een parameter die meerdere types aanvaardt:
def format_value(value: int | float | str) -> str:
    return str(value)

# Een gebruiker heeft een leeftijd of niet:
age: int | None = None
Optional[T] - de oude syntax

Vóór Python 3.10 schreef je Optional[int] in plaats van int | None. Je komt deze vorm nog vaak tegen in bestaande codebases:

from typing import Optional

def find_user(name: str) -> Optional[dict[str, str]]:
    ...

Vanaf Python 3.10 is int | None de aanbevolen syntax. Beide zijn correct; | is korter en vereist geen import.

Any - en waarom je het wil vermijden

Any is het joker-type: het betekent "eender wat". Een functie met Any-parameters is niet geholpen met type checking:

from typing import Any

def process(data: Any) -> Any:
    return data.do_something()

Wanneer Any wél oké is:

  • Bij geleidelijke migratie van oude code: je kan niet alles in één keer typeren.
  • Wanneer het type écht onbekend is. Bvb. data uit een extern systeem waarvan je het schema niet kent.

Vuistregel: als je het type kán specificeren, doe het dan ook. Elke Any is een gemiste kans om fouten te vangen.


Oefeningen

Oefening 1: Functies annoteren

Geef elke functie de juiste parameter- en return-types:

# A
def multiply(a, b):
    return a * b

# B
def create_greeting(name, age):
    return f"{name} is {age} jaar oud"

# C
def find_longest(words):
    if not words:
        return None
    return max(words, key=len)

# D
def group_by_first_letter(words):
    result = {}
    for word in words:
        first = word[0].lower()
        if first not in result:
            result[first] = []
        result[first].append(word)
    return result

Oefening 2: Typefouten opsporen

Elk van deze codeblokken bevat minstens één typefout. Vind ze zonder de code uit te voeren:

# A
def add_score(scores: list[int], new_score: int) -> list[int]:
    scores.append(new_score)
    return scores

result = add_score([1, 2, 3], "4")

# B
def get_user() -> dict[str, str]:
    return {"name": "Alice", "age": 30}  # age is een int, niet str

# C
def average(values: list[float]) -> float:
    return sum(values) / len(values)

print(average([]))  # hint: wat gebeurt er als values leeg is?

# D
from typing import Any

def process(item: Any) -> str:
    return item.strip().upper()

process(42)

Oplossingen

Oefening 1:

# A
def multiply(a: float, b: float) -> float:
    return a * b

# B
def create_greeting(name: str, age: int) -> str:
    return f"{name} is {age} jaar oud"

# C
def find_longest(words: list[str]) -> str | None:
    if not words:
        return None
    return max(words, key=len)

# D
def group_by_first_letter(words: list[str]) -> dict[str, list[str]]:
    result: dict[str, list[str]] = {}
    for word in words:
        first = word[0].lower()
        if first not in result:
            result[first] = []
        result[first].append(word)
    return result

Oefening 2:

  • A: "4" is een str, maar new_score verwacht int. Typefout op de aanroep.
  • B: "age": 30 - 30 is een int, maar de returnwaarde claimt dict[str, str]. Ofwel het type is fout (dict[str, str | int]), ofwel de waarde moet "30" zijn.
  • C: average([]) - deling door nul als values leeg is. Geen typefout in strikte zin, maar wél een runtime-fout. Het returntype float dekt deze fout niet af; type checking alleen is niet genoeg - je hebt ook tests nodig.
  • D: process(42) - 42 heeft geen .strip()-methode. Met type hints had je dit vermeden: def process(item: str) -> str: en de checker had process(42) geweigerd.

Geschiedenis

Type hints kwamen niet uit het niets. Ze zijn het resultaat van jarenlange discussie en experimenten in de Python-gemeenschap.

Jaar Gebeurtenis
1991 Python 0.9 - volledig dynamisch, geen type-systeem.
2006 Python 3.0 planning - Guido van Rossum stelt functie-annotaties voor als generiek metadata-mechanisme (`def foo(x: "beschrijving") -> "return"). Nog geen types.
2008 Python 3.0 - functie-annotaties bestaan, maar hebben geen vaste betekenis. Iedereen gebruikt ze anders (of niet).
2012 mypy-project start bij Dropbox - Jukka Lehtosalo bouwt een experimentele static type checker voor Python, geïnspireerd door TypeScript.
2014 Guido van Rossum raakt overtuigd. Samen met Jukka en anderen werkt hij PEP 484 uit.
2015 PEP 484 geaccepteerd - Python krijgt optionele type hints. typing-module toegevoegd aan de standaardbibliotheek.
2016 PEP 526 - syntax voor variabele-annotaties (x: int = 5).
2020 PEP 604 - union-syntax met | (str | None in plaats van Optional[str]).
2021 PEP 585 - generics zonder typing-import (list[int] i.p.v. List[int]).
2023 PEP 695 - type-statement voor type-aliassen (Python 3.12).
2024–26 ty (Astral) verschijnt als moderne, razendsnelle type checker. Typing is de norm in moderne, professionele Python-projecten.
Waarom duurde het tot 2015?

Python bestond 24 jaar zonder type hints. De weerstand was lange tijd cultureel: Python was trots op zijn eenvoud en dynamiek. Types voelden als een stap terug richting Java.

Wat veranderde? Drie dingen:

  1. Schaal. Python werd gebruikt in codebases van miljoenen regels bij Dropbox, Google, Instagram. Dynamisch typen schaalt slecht zonder tooling.
  2. Tooling. Mypy bewees dat type checking praktisch haalbaar was zonder Python zelf te veranderen.
  3. TypeScript. Het succes van TypeScript toonde dat optionele types wél werken. Je moet niet kiezen tussen dynamisch en statisch, je kan beide hebben.

Extra's

Sterk en zwak getypeerde talen

Los van statisch of dynamisch kan een programmeertaal ook nog sterk (strongly typed) of zwak (weakly typed) getypeerd zijn.

Python is een sterk getypeerde taal. Elke waarde heeft een specifiek type en verschillende types kan je niet zomaar met elkaar combineren. We hadden dit gedrag al gezien, maar nu kunnen we er een naam aan geven!

x = 5
print(type(x))
# <class 'int'>

y = "hello"
print(type(y))
# <class 'str'>

"hello" + 5 # TypeError!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    "hello" + 5
    ~~~~~~~~^~~
TypeError: can only concatenate str (not "int") to str

Zwak getypeerde talen (zoals JavaScript) zetten types stilzwijgend om wanneer dat nodig lijkt, zelfs als dat tot rare resultaten leidt:

> "hello" + 5
'hello5'

> "2" + 2
'22'

> "5" - 1
4

Duck Typing

Duck Typing

If it walks like a duck and quacks like a duck, it's a duck.

Een term die vaak opduikt i.v.m. dynamisch typeren is duck typing. Het idee: Python geeft niet om het exacte type van een object, zolang het maar de methodes of het gedrag heeft die nodig zijn.

def report_size(x: str):
    print(len(x))

report_size("Syntra")    # 6

De functie report_size is geschreven om de lengte van een string te loggen. Maar omdat Python dynamisch getypeerd is, controleert het nergens welk type x precies is. De functie, en dus ook de len-functie, worden gewoon uitgevoerd.

Bijgevolg werkt de functie evengoed voor een lijst of een dictionary:

report_size([1, 2, 3])    # 3
report_size({"a": 1})     # 1

Net als bij dynamisch typeren in het algemeen geeft duck typing veel flexibiliteit aan de ontwikkelaar. Maar net zoals bij de vorige voorbeelden betekent dit dat een object zonder de juiste ondersteuning pas een fout geeft op het moment dat het echt gebruikt wordt.

Zo beschikken int en float niet over de mogelijkheid hun 'lengte' terug te geven:

report_size(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    len(42)
    ~~~^^^^
TypeError: object of type 'int' has no len()

Samenvatting

Samenvatting

Type hints zijn de brug tussen Python's dynamische aard en de voordelen van statisch typen: je vertelt de lezer, de editor en de type checker wat een functie verwacht en teruggeeft, zonder iets te veranderen aan hoe het programma draait.

  • Python is een dynamisch getypeerde taal: het type zit in het object, niet in de variabele. Typefouten zijn daardoor pas merkbaar tijdens de uitvoering.
  • Type hints zijn optioneel en hebben geen invloed op de uitvoering. Vandaar de naam hints.
  • Toch zijn ze goud waard: betere autocomplete en editor-ondersteuning, vroege foutdetectie met een static type checker (mypy, ty) en zelfdocumenterende code die niet kan verouderen.
  • FastAPI en Pydantic draaien volledig op type hints: de types die je schrijft worden uitvoerbare specificaties voor validatie, foutmeldingen en API-documentatie.
  • Annoteer eerst je functievoorschriften (parameters én returnwaarde). Binnen de functie is annoteren meestal overbodig - de editor leidt het type zelf af.
  • Gebruik generics voor collections (list[str], dict[str, int]) en unions met | voor "of" - int | None sinds Python 3.10, daarvoor Optional[int]. Vermijd Any waar je het type kent.
  • Python is sterk getypeerd (geen stille typeconversie: "hello" + 5 faalt) én dynamisch. Duck typing: het gedrag telt, niet het label.

Wat moet je kunnen?

TODO