Introductie tot Python Types¶
Python biedt ondersteuning voor optionele "type hints" (ook wel "type annotaties" genoemd).
Deze "type hints" of annotaties zijn een speciale syntax waarmee het type van een variabele kan worden gedeclareerd.
Door types voor je variabelen te declareren, kunnen editors en hulpmiddelen je beter ondersteunen.
Dit is slechts een korte tutorial/opfrisser over Python type hints. Het behandelt enkel het minimum dat nodig is om ze te gebruiken met FastAPI... en dat is relatief weinig.
FastAPI is helemaal gebaseerd op deze type hints, ze geven veel voordelen.
Maar zelfs als je FastAPI nooit gebruikt, heb je er baat bij om er iets over te leren.
Note
Als je een Python expert bent en alles al weet over type hints, sla dan dit hoofdstuk over.
Motivatie¶
Laten we beginnen met een eenvoudig voorbeeld:
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Het aanroepen van dit programma leidt tot het volgende resultaat:
John Doe
De functie voert het volgende uit:
- Neem een
first_name
en eenlast_name
- Converteer de eerste letter van elk naar een hoofdletter met
title()
. `` - Voeg samen met een spatie in het midden.
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Bewerk het¶
Dit is een heel eenvoudig programma.
Maar stel je nu voor dat je het vanaf nul zou moeten maken.
Op een gegeven moment zou je aan de definitie van de functie zijn begonnen, je had de parameters klaar...
Maar dan moet je “die methode die de eerste letter naar hoofdletters converteert” aanroepen.
Was het upper
? Was het uppercase
? first_uppercase
? capitalize
?
Dan roep je de hulp in van je oude programmeursvriend, (automatische) code aanvulling in je editor.
Je typt de eerste parameter van de functie, first_name
, dan een punt (.
) en drukt dan op Ctrl+Spatie
om de aanvulling te activeren.
Maar helaas krijg je niets bruikbaars:
Types toevoegen¶
Laten we een enkele regel uit de vorige versie aanpassen.
We zullen precies dit fragment, de parameters van de functie, wijzigen van:
first_name, last_name
naar:
first_name: str, last_name: str
Dat is alles.
Dat zijn de "type hints":
def get_full_name(first_name: str, last_name: str):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Dit is niet hetzelfde als het declareren van standaardwaarden zoals bij:
first_name="john", last_name="doe"
Het is iets anders.
We gebruiken dubbele punten (:
), geen gelijkheidstekens (=
).
Het toevoegen van type hints verandert normaal gesproken niet wat er gebeurt in je programma t.o.v. wat er zonder type hints zou gebeuren.
Maar stel je voor dat je weer bezig bent met het maken van een functie, maar deze keer met type hints.
Op hetzelfde moment probeer je de automatische aanvulling te activeren met Ctrl+Spatie
en je ziet:
Nu kun je de opties bekijken en er doorheen scrollen totdat je de optie vindt die “een belletje doet rinkelen”:
Meer motivatie¶
Bekijk deze functie, deze heeft al type hints:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
Omdat de editor de types van de variabelen kent, krijgt u niet alleen aanvulling, maar ook controles op fouten:
Nu weet je hoe je het moet oplossen, converteer age
naar een string met str(age)
:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
Types declareren¶
Je hebt net de belangrijkste plek om type hints te declareren gezien. Namelijk als functieparameters.
Dit is ook de belangrijkste plek waar je ze gebruikt met FastAPI.
Eenvoudige types¶
Je kunt alle standaard Python types declareren, niet alleen str
.
Je kunt bijvoorbeeld het volgende gebruiken:
int
float
bool
bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_d, item_e
Generieke types met typeparameters¶
Er zijn enkele datastructuren die andere waarden kunnen bevatten, zoals dict
, list
, set
en tuple
en waar ook de interne waarden hun eigen type kunnen hebben.
Deze types die interne types hebben worden “generieke” types genoemd. Het is mogelijk om ze te declareren, zelfs met hun interne types.
Om deze types en de interne types te declareren, kun je de standaard Python module typing
gebruiken. Deze module is speciaal gemaakt om deze type hints te ondersteunen.
Nieuwere versies van Python¶
De syntax met typing
is verenigbaar met alle versies, van Python 3.6 tot aan de nieuwste, inclusief Python 3.9, Python 3.10, enz.
Naarmate Python zich ontwikkelt, worden nieuwere versies, met verbeterde ondersteuning voor deze type annotaties, beschikbaar. In veel gevallen hoef je niet eens de typing
module te importeren en te gebruiken om de type annotaties te declareren.
Als je een recentere versie van Python kunt kiezen voor je project, kun je profiteren van die extra eenvoud.
In alle documentatie staan voorbeelden die compatibel zijn met elke versie van Python (als er een verschil is).
Bijvoorbeeld “Python 3.6+” betekent dat het compatibel is met Python 3.6 of hoger (inclusief 3.7, 3.8, 3.9, 3.10, etc). En “Python 3.9+” betekent dat het compatibel is met Python 3.9 of hoger (inclusief 3.10, etc).
Als je de laatste versies van Python kunt gebruiken, gebruik dan de voorbeelden voor de laatste versie, die hebben de beste en eenvoudigste syntax, bijvoorbeeld “Python 3.10+”.
List¶
Laten we bijvoorbeeld een variabele definiëren als een list
van str
.
Declareer de variabele met dezelfde dubbele punt (:
) syntax.
Als type, vul list
in.
Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes:
def process_items(items: list[str]):
for item in items:
print(item)
Van typing
, importeer List
(met een hoofdletter L
):
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Declareer de variabele met dezelfde dubbele punt (:
) syntax.
Zet als type de List
die je hebt geïmporteerd uit typing
.
Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes:
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Info
De interne types tussen vierkante haakjes worden “typeparameters” genoemd.
In dit geval is str
de typeparameter die wordt doorgegeven aan List
(of list
in Python 3.9 en hoger).
Dat betekent: “de variabele items
is een list
, en elk van de items in deze list is een str
”.
Tip
Als je Python 3.9 of hoger gebruikt, hoef je List
niet te importeren uit typing
, je kunt in plaats daarvan hetzelfde reguliere list
type gebruiken.
Door dat te doen, kan je editor ondersteuning bieden, zelfs tijdens het verwerken van items uit de list:
Zonder types is dat bijna onmogelijk om te bereiken.
Merk op dat de variabele item
een van de elementen is in de lijst items
.
Toch weet de editor dat het een str
is, en biedt daar vervolgens ondersteuning voor aan.
Tuple en Set¶
Je kunt hetzelfde doen om tuple
s en set
s te declareren:
def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
return items_t, items_s
from typing import Set, Tuple
def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]):
return items_t, items_s
Dit betekent:
- De variabele
items_t
is eentuple
met 3 items, eenint
, nog eenint
, en eenstr
. - De variabele
items_s
is eenset
, en elk van de items is van het typebytes
.
Dict¶
Om een dict
te definiëren, geef je 2 typeparameters door, gescheiden door komma's.
De eerste typeparameter is voor de sleutels (keys) van de dict
.
De tweede typeparameter is voor de waarden (values) van het dict
:
def process_items(prices: dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
from typing import Dict
def process_items(prices: Dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
Dit betekent:
- De variabele
prices
is eendict
:- De sleutels van dit
dict
zijn van het typestr
(bijvoorbeeld de naam van elk item). - De waarden van dit
dict
zijn van het typefloat
(bijvoorbeeld de prijs van elk item).
- De sleutels van dit
Union¶
Je kunt een variable declareren die van verschillende types kan zijn, bijvoorbeeld een int
of een str
.
In Python 3.6 en hoger (inclusief Python 3.10) kun je het Union
-type van typing
gebruiken en de mogelijke types die je wilt accepteren, tussen de vierkante haakjes zetten.
In Python 3.10 is er ook een nieuwe syntax waarin je de mogelijke types kunt scheiden door een verticale balk (|
).
def process_item(item: int | str):
print(item)
from typing import Union
def process_item(item: Union[int, str]):
print(item)
In beide gevallen betekent dit dat item
een int
of een str
kan zijn.
Mogelijk None
¶
Je kunt declareren dat een waarde een type kan hebben, zoals str
, maar dat het ook None
kan zijn.
In Python 3.6 en hoger (inclusief Python 3.10) kun je het declareren door Optional
te importeren en te gebruiken vanuit de typing
-module.
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
Door Optional[str]
te gebruiken in plaats van alleen str
, kan de editor je helpen fouten te detecteren waarbij je ervan uit zou kunnen gaan dat een waarde altijd een str
is, terwijl het in werkelijkheid ook None
zou kunnen zijn.
Optional[EenType]
is eigenlijk een snelkoppeling voor Union[EenType, None]
, ze zijn equivalent.
Dit betekent ook dat je in Python 3.10 EenType | None
kunt gebruiken:
def say_hi(name: str | None = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
from typing import Union
def say_hi(name: Union[str, None] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
Gebruik van Union
of Optional
¶
Als je een Python versie lager dan 3.10 gebruikt, is dit een tip vanuit mijn subjectieve standpunt:
- 🚨 Vermijd het gebruik van
Optional[EenType]
. - Gebruik in plaats daarvan
Union[EenType, None]
✨.
Beide zijn gelijkwaardig en onderliggend zijn ze hetzelfde, maar ik zou Union
aanraden in plaats van Optional
omdat het woord “optional” lijkt te impliceren dat de waarde optioneel is, en het eigenlijk betekent “het kan None
zijn”, zelfs als het niet optioneel is en nog steeds vereist is.
Ik denk dat Union[SomeType, None]
explicieter is over wat het betekent.
Het gaat alleen om de woorden en naamgeving. Maar die naamgeving kan invloed hebben op hoe jij en je teamgenoten over de code denken.
Laten we als voorbeeld deze functie nemen:
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
🤓 Other versions and variants
def say_hi(name: str | None):
print(f"Hey {name}!")
De parameter name
is gedefinieerd als Optional[str]
, maar is niet optioneel, je kunt de functie niet aanroepen zonder de parameter:
say_hi() # Oh, nee, dit geeft een foutmelding! 😱
De name
parameter is nog steeds vereist (niet optioneel) omdat het geen standaardwaarde heeft. Toch accepteert name
None
als waarde:
say_hi(name=None) # Dit werkt, None is geldig 🎉
Het goede nieuws is dat als je eenmaal Python 3.10 gebruikt, je je daar geen zorgen meer over hoeft te maken, omdat je dan gewoon |
kunt gebruiken om unions van types te definiëren:
def say_hi(name: str | None):
print(f"Hey {name}!")
🤓 Other versions and variants
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
Dan hoef je je geen zorgen te maken over namen als Optional
en Union
. 😎
Generieke typen¶
De types die typeparameters in vierkante haakjes gebruiken, worden Generieke types of Generics genoemd, bijvoorbeeld:
Je kunt dezelfde ingebouwde types gebruiken als generics (met vierkante haakjes en types erin):
list
tuple
set
dict
Hetzelfde als bij Python 3.8, uit de typing
-module:
Union
Optional
(hetzelfde als bij Python 3.8)- ...en anderen.
In Python 3.10 kun je , als alternatief voor de generieke Union
en Optional
, de verticale lijn (|
) gebruiken om unions van typen te voorzien, dat is veel beter en eenvoudiger.
Je kunt dezelfde ingebouwde types gebruiken als generieke types (met vierkante haakjes en types erin):
list
tuple
set
dict
En hetzelfde als met Python 3.8, vanuit de typing
-module:
Union
Optional
- ...en anderen.
List
Tuple
Set
Dict
Union
Optional
- ...en anderen.
Klassen als types¶
Je kunt een klasse ook declareren als het type van een variabele.
Stel dat je een klasse Person
hebt, met een naam:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Vervolgens kun je een variabele van het type Persoon
declareren:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Dan krijg je ook nog eens volledige editorondersteuning:
Merk op dat dit betekent dat "one_person
een instantie is van de klasse Person
".
Dit betekent niet dat one_person
de klasse is met de naam Person
.
Pydantic modellen¶
Pydantic is een Python-pakket voor het uitvoeren van datavalidatie.
Je declareert de "vorm" van de data als klassen met attributen.
Elk attribuut heeft een type.
Vervolgens maak je een instantie van die klasse met een aantal waarden en het valideert de waarden, converteert ze naar het juiste type (als dat het geval is) en geeft je een object met alle data terug.
Daarnaast krijg je volledige editorondersteuning met dat resulterende object.
Een voorbeeld uit de officiële Pydantic-documentatie:
from datetime import datetime
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: datetime | None = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Union[datetime, None] = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import List, Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Union[datetime, None] = None
friends: List[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
Info
Om meer te leren over Pydantic, bekijk de documentatie.
FastAPI is volledig gebaseerd op Pydantic.
Je zult veel meer van dit alles in de praktijk zien in de Tutorial - Gebruikershandleiding.
Tip
Pydantic heeft een speciaal gedrag wanneer je Optional
of Union[EenType, None]
gebruikt zonder een standaardwaarde, je kunt er meer over lezen in de Pydantic-documentatie over Verplichte optionele velden.
Type Hints met Metadata Annotaties¶
Python heeft ook een functie waarmee je extra metadata in deze type hints kunt toevoegen met behulp van Annotated
.
In Python 3.9 is Annotated
onderdeel van de standaardpakket, dus je kunt het importeren vanuit typing
.
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
In versies lager dan Python 3.9 importeer je Annotated
vanuit typing_extensions
.
Het wordt al geïnstalleerd met FastAPI.
from typing_extensions import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
Python zelf doet niets met deze Annotated
en voor editors en andere hulpmiddelen is het type nog steeds een str
.
Maar je kunt deze ruimte in Annotated
gebruiken om FastAPI te voorzien van extra metadata over hoe je wilt dat je applicatie zich gedraagt.
Het belangrijkste om te onthouden is dat de eerste typeparameter die je doorgeeft aan Annotated
het werkelijke type is. De rest is gewoon metadata voor andere hulpmiddelen.
Voor nu hoef je alleen te weten dat Annotated
bestaat en dat het standaard Python is. 😎
Later zul je zien hoe krachtig het kan zijn.
Tip
Het feit dat dit standaard Python is, betekent dat je nog steeds de best mogelijke ontwikkelaarservaring krijgt in je editor, met de hulpmiddelen die je gebruikt om je code te analyseren en te refactoren, enz. ✨
Daarnaast betekent het ook dat je code zeer verenigbaar zal zijn met veel andere Python-hulpmiddelen en -pakketten. 🚀
Type hints in FastAPI¶
FastAPI maakt gebruik van type hints om verschillende dingen te doen.
Met FastAPI declareer je parameters met type hints en krijg je:
- Editor ondersteuning.
- Type checks.
...en FastAPI gebruikt dezelfde declaraties om:
- *Vereisten te definïeren *: van request pad parameters, query parameters, headers, bodies, dependencies, enz.
- Data te converteren: van de request naar het vereiste type.
- Data te valideren: afkomstig van elke request:
- Automatische foutmeldingen te genereren die naar de client worden geretourneerd wanneer de data ongeldig is.
- De API met OpenAPI te documenteren:
- die vervolgens wordt gebruikt door de automatische interactieve documentatie gebruikersinterfaces.
Dit klinkt misschien allemaal abstract. Maak je geen zorgen. Je ziet dit allemaal in actie in de Tutorial - Gebruikershandleiding.
Het belangrijkste is dat door standaard Python types te gebruiken, op één plek (in plaats van meer klassen, decorators, enz. toe te voegen), FastAPI een groot deel van het werk voor je doet.
Info
Als je de hele tutorial al hebt doorgenomen en terug bent gekomen om meer te weten te komen over types, is een goede bron het "cheat sheet" van mypy
.