Een gerichte verzameling Python-interviewvragen voor backend-developers. Gebruik elk antwoord als een gesproken structuur: begin met het concept, voeg een concreet voorbeeld toe en benoem vervolgens de afwegingen.
1Welke ingebouwde datatypen heeft Python en welke daarvan zijn veranderlijk?
Python heeft scalaire typen zoals NoneType, bool, int, float en complex; tekst- en binaire typen zoals str, bytes en bytearray; sequentietypen zoals list, tuple en range; en collectietypen zoals dict, set en frozenset. Het belangrijke onderscheid voor sollicitatiegesprekken is veranderlijkheid: list, dict, set en bytearray kunnen ter plekke worden gewijzigd, terwijl getallen, bool, str, bytes, tuple, range, frozenset en None onveranderlijk zijn. Een nuttige nuance is dat de elementverwijzingen van een tuple zelf vastliggen, maar de interne staat van een gerefereerd veranderlijk object nog steeds kan wijzigen.
2Hoe verschillen `list`, `tuple`, `set` en `dict` van elkaar in de praktijk?
Een `list` is een geordende, veranderlijke reeks en is daarom geschikt voor wachtrijen met taken, verzamelde resultaten en geordende gegevens die veranderen. Een `tuple` is geordend maar onveranderlijk, wat het nuttig maakt voor vaste records, retourwaarden van functies en waarden die niet opnieuw mogen worden toegewezen. Een `set` slaat unieke, hashbare objecten op en is ideaal voor het controleren van lidmaatschap of het verwijderen van duplicaten. Een `dict` koppelt hashbare sleutels aan waarden en is de standaard datastructuur voor indexen, zoekacties en JSON-achtige gegevens. In moderne Python-versies behoudt een `dict` de invoegvolgorde, maar het belangrijkste doel blijft toegang op basis van sleutels.
items = ["a", "b", "a"]
unique = set(items)
index = {"user_id": 42}
point = (10, 20)
`==` controleert of twee objecten gelijk zijn qua waarde, meestal via `__eq__`. `is` controleert of twee referenties naar exact hetzelfde object in het geheugen wijzen. In productiecode moet `is` over het algemeen gereserveerd worden voor controles op singletons, zoals `value is None`, `value is True` alleen wanneer identiteit echt van belang is, of voor sentinel-objecten. Vertrouw niet op implementatiedetails zoals caching van kleine gehele getallen of string-interning; deze kunnen ervoor zorgen dat `is` in voorbeelden lijkt te werken, terwijl het de verkeerde semantische keuze is.
4Wat is een hashbaar object en waarom is dit belangrijk?
Een hashbaar object heeft een hashwaarde die gedurende zijn levensduur stabiel blijft, en een gelijkheidsgedrag (equality) dat consistent is met die hash. Hashbaarheid is belangrijk omdat `dict`-sleutels en `set`-elementen hiervan afhankelijk zijn. De meeste onveranderlijke ingebouwde typen zijn hashbaar, maar niet alle objecten in een onveranderlijke container worden automatisch hashbaar: een `tuple` die een `list` bevat, is niet hashbaar. Koppel dit tijdens sollicitatiegesprekken aan het ontwerp: als een object als sleutel wordt gebruikt, mogen velden die meedoen in `__eq__` of `__hash__` niet veranderen na toevoeging. Gebeurt dit wel, dan kan het object onbereikbaar worden in een `dict` of `set`.
5Hoe werkt slicing, inclusief negatieve indexen en stappen?
Een slice `sequence[start:stop:step]` is inclusief `start`, exclusief `stop` en verplaatst zich met `step`. Ontbrekende grenzen worden afgeleid uit de richting van de stap. Negatieve indexen tellen vanaf het einde, dus `s[-1]` is het laatste element. Een negatieve stap loopt van rechts naar links; `s[::-1]` creëert een omgekeerde reeks voor veel reekstypen. De afweging is dat slicing vaak een nieuw object creëert, waardoor het expressief is, maar niet altijd zonder prestatiekosten bij grote hoeveelheden gegevens.
text = "interview"
print(text[1:5])
print(text[::-1])
Python geeft objectreferenties door via toewijzing, wat vaak 'call by sharing' wordt genoemd. De functie ontvangt lokale parameternamen die gebonden zijn aan dezelfde objecten die door de aanroeper zijn verstrekt. Als het object veranderlijk is, kan de functie het wijzigen en zal de aanroeper deze verandering waarnemen. Als de functie een nieuwe waarde toewijst aan de parameternaam, verandert alleen de lokale binding. Een sterk antwoord in een interview maakt onderscheid tussen ter plekke muteren en lokaal opnieuw toewijzen: `items.append(...)` of `items.clear()` wijzigt de lijst van de aanroeper, terwijl `items = []` binnen de functie de lokale naam simpelweg naar een nieuwe lijst laat wijzen.
7Waarom zijn veranderlijke standaardargumenten gevaarlijk?
Standaardwaarden voor argumenten worden eenmalig geëvalueerd wanneer de functie wordt gedefinieerd, en niet telkens wanneer de functie wordt aangeroepen. Als een standaardwaarde een list of dict is, kunnen alle aanroepen hetzelfde object delen en per ongeluk status lekken. Het veilige patroon is def f(items=None): en vervolgens items = [] aanmaken in de functie als items de waarde None heeft. Er zijn zeldzame gevallen waarin een gedeelde status de bedoeling is, zoals bij een kleine cache, maar dan moet dit expliciet en gedocumenteerd zijn omdat het de lezers van de code kan verrassen.
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("python"))
print(add_tag("backend")) # Same list reused
def safe_add_tag(tag, tags=None):
tags = [] if tags is None else tags
tags.append(tag)
return tags
*args verzamelt extra positionele argumenten in een tuple, terwijl **kwargs extra keyword-argumenten in een dict verzamelt. Ze zijn nuttig voor wrappers, decorators, flexibele API's en het doorsturen van functieaanroepen. Bij de aanroep breidt *iterable positionele waarden uit en **mapping keyword-waarden. De afweging is duidelijkheid: expliciete parameters zijn beter wanneer de functie een stabiel openbaar contract heeft, terwijl args en kwargs beter zijn bij grensvlakken waar het doorsturen of compatibiliteit van belang is.
Python zoekt naar namen in de `Local`, `Enclosing`, `Global` en `Builtins` scopes. Het lezen van een naam volgt deze volgorde. Het toewijzen van een naam binnen een functie creëert normaal gesproken een lokale binding, wat de reden is dat code een `UnboundLocalError` kan veroorzaken als het een naam leest vóór de lokale toewijzing. Met `global` leidt men een toewijzing om naar de module-scope, en `nonlocal` leidt een toewijzing om naar een omsluitende functie-scope. Gebruik beide met mate in goede productiecode, omdat ze de datastroom minder duidelijk maken.
total = 0
def add(value):
global total
total += value
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
Een closure is een functie die toegang behoudt tot variabelen uit een omsluitend bereik nadat de buitenste functie is geretourneerd. Dit is nuttig voor decorators, callback-factories en geconfigureerde functies. Late binding betekent dat de vrije variabele wordt opgezocht wanneer de binnenste functie wordt aangeroepen, en niet wanneer deze wordt aangemaakt. Lambda's die in een lus zijn gemaakt, kunnen daardoor allemaal de uiteindelijke luswaarde zien. Leg de huidige waarde vast met een standaardargument, bijvoorbeeld lambda i=i: i, of gebruik een kleine factoryfunctie.
callbacks = []
for i in range(3):
callbacks.append(lambda: i)
print([callback() for callback in callbacks]) # [2, 2, 2]
callbacks = []
for i in range(3):
callbacks.append(lambda i=i: i)
print([callback() for callback in callbacks]) # [0, 1, 2]
11Hoe verschillen klassevariabelen van instantievariabelen?
Een klassevariabele wordt opgeslagen op het klasse-object en gedeeld door instanties, tenzij een instantie deze overschaduwt met een eigen attribuut. Een instantievariabele wordt opgeslagen op een specifiek object en meestal toegewezen als self.name in __init__. Onveranderlijke klasseconstanten zijn doorgaans geen probleem, maar veranderlijke klasse-attributen veroorzaken vaak bugs wanneer ze per ongeluk worden gebruikt als toestand per instantie. Benoem in een sollicitatiegesprek de zoekvolgorde: Python controleert eerst de instantie, en vervolgens de klasse en zijn voorouders.
class Cart:
items = []
first = Cart()
second = Cart()
first.items.append("book")
print(second.items) # ["book"]
class SafeCart:
def __init__(self):
self.items = []
12Hoe werken overerving, MRO (Method Resolution Order) en super()?
Python lost attributen op via de MRO, wat zichtbaar is met ClassName.__mro__. Bij enkelvoudige overerving is dit eenvoudig, maar bij meervoudige overerving gebruikt Python C3-linearisatie om een consistente volgorde te creëren. super() betekent niet simpelweg 'roep mijn bovenliggende klasse aan'; het betekent 'roep de volgende implementatie in de MRO aan'. Dit maakt coöperatieve meervoudige overerving mogelijk wanneer methoden compatibele argumenten accepteren en elke klasse super() correct aanroept.
class A:
def save(self):
print("A")
class B(A):
def save(self):
print("B")
super().save()
class C(A):
def save(self):
print("C")
super().save()
class D(B, C):
pass
D().save() # B, C, A
13Wat is het verschil tussen een iterable, een iterator en een generator?
Een iterable is elk object dat een iterator kan retourneren via `iter()`, zoals een lijst of een bestandsobject. Een iterator is het object met een interne status dat `__next__` implementeert en een `StopIteration` genereert wanneer het is uitgeput. Een generator is een handige manier om een iterator te maken met `yield` of een generatorexpressie. Generators zijn geheugenefficiënt omdat ze waarden lazy produceren, maar ze zijn meestal slechts één keer te doorlopen en kunnen niet worden geïndexeerd zoals een lijst.
def read_lines(path):
with open(path, encoding="utf-8") as file:
for line in file:
yield line.strip()
14Wat is een contextmanager en waarom is with nuttig?
Een contextmanager definieert gedrag voor binnenkomst en vertrek met __enter__ en __exit__, of kan worden aangemaakt met contextlib.contextmanager. Het with-statement maakt resourcebeheer expliciet en betrouwbaar: bestanden worden gesloten, locks worden vrijgegeven en transacties kunnen worden gecommit of teruggedraaid, zelfs wanneer er uitzonderingen optreden. __exit__ ontvangt informatie over uitzonderingen en kan de uitzondering onderdrukken door true te retourneren, hoewel het onderdrukken van fouten met voorzichtigheid moet worden toegepast.
from contextlib import contextmanager
with open("data.txt", encoding="utf-8") as file:
data = file.read()
@contextmanager
def temporary_setting():
print("enter")
try:
yield
finally:
print("exit")
15Wat is het verschil tussen een ondiepe kopie (shallow copy) en een diepe kopie (deep copy)?
Een ondiepe kopie creëert een nieuwe buitenste container, maar behoudt verwijzingen naar dezelfde geneste objecten. Een diepe kopie kopieert geneste objecten recursief waar mogelijk. Voor eenvoudige platte lijsten is een ondiepe kopie vaak voldoende. Bij geneste veranderlijke structuren kan een ondiepe kopie nog steeds de status delen, wat voor onverwachte resultaten kan zorgen. Een diepe kopie is rekenintensiever en kan falen met een `TypeError` of `PicklingError` bij bronnen zoals geopende bestanden, sockets, databasesessies of objecten met identiteitsgevoelig gedrag.
from copy import copy, deepcopy
original = [[1], [2]]
shallow = copy(original)
deep = deepcopy(original)
original[0].append(99)
print(shallow) # [[1, 99], [2]]
print(deep) # [[1], [2]]
16Wat is de GIL (Global Interpreter Lock) en hoe beïnvloedt deze backend-code?
In standaard CPython staat de GIL toe dat slechts één thread tegelijk Python-bytecode uitvoert. Dit betekent dat threads CPU-gebonden Python-code meestal niet versnellen. Threads zijn nog steeds nuttig voor I/O-gebonden werk, omdat de GIL wordt vrijgegeven tijdens het wachten op veel blokkerende bewerkingen. Gebruik voor rekenintensief werk `multiprocessing`, native extensies, gevectoriseerde native bibliotheken of afzonderlijke services. Voor sollicitatiegesprekken vanaf 2026 is het ook nuttig om de free-threaded CPython-builds in Python 3.13+ te benoemen: deze maken de GIL optioneel voor compatibele code, maar vereisen nog steeds afhankelijkheids- en prestatietests.
17Wanneer moet je kiezen voor threads, processen of asyncio?
Gebruik threads wanneer je te maken hebt met blokkerende I/O of bibliotheken die alleen synchrone API's aanbieden. Gebruik processen voor CPU-intensief werk, isolatie of het inperken van crashes, waarbij je de overhead van geheugen en serialisatie accepteert. Gebruik asyncio wanneer je veel gelijktijdige I/O-operaties hebt en bibliotheken die compatibel zijn met async, zoals async-databasedrivers of HTTP-clients. De praktische keuze hangt ook af van je framework en afhankelijkheden: het mixen van async-code met blokkerende aanroepen kan de prestaties verslechteren.
18Waarvoor is `async`/`await` nuttig in Python-webservices?
`async` en `await` maken coöperatieve gelijktijdigheid mogelijk. Een coroutine geeft de controle terug aan de eventloop wanneer deze wacht op I/O, waardoor andere taken kunnen worden uitgevoerd. Dit is waardevol voor API's die wachten op databases, HTTP-aanroepen, wachtrijen of sockets. Het maakt CPU-gebonden code niet op magische wijze sneller; zwaar CPU-werk blokkeert de eventloop tenzij het wordt verplaatst naar een worker, proces of native code. In FastAPI wordt een normaal `def`-eindpunt uitgevoerd in een threadpool, terwijl blokkerende aanroepen binnen een `async def` de eventloop kunnen blokkeren; `async` helpt alleen wanneer het aanroeppad gebruikmaakt van niet-blokkerende asynchrone I/O.
19Hoe moeten exceptions worden afgehandeld in Python-productiecode?
Vang de meest specifieke exception op die je kunt afhandelen, en doe dit op een laag die kan herstellen, context kan toevoegen of de fout kan vertalen naar een domeinfout. Vermijd een kale `except`-clausule en het stilzwijgend negeren van fouten, omdat dit gegevensverlies en operationele problemen verbergt. Gebruik `raise` om dezelfde exception opnieuw op te werpen en `raise NewError(...) from exc` om de oorspronkelijke oorzaak (causale context) te behouden. Goede productiecode logt ook op applicatiegrenzen, retourneert gepaste HTTP-fouten en houdt foutmeldingen nuttig zonder geheimen te lekken.
try:
result = parse_payload(payload)
except ValueError as exc:
raise InvalidRequest("Invalid payload") from exc
20Hoe zou je een Python-service voor productie structureren en testen?
Scheid domeinlogica van framework- en infrastructuurcode, zodat het kerngedrag getest kan worden zonder server of database. Gebruik configuratie uit omgevingsvariabelen of getypeerde instellingen, logging in plaats van `print`, dependency injection bij de grenzen (boundaries), en expliciete time-outs voor netwerkaanroepen. Combineer voor tests unit-tests voor pure logica, integratietests voor database- of externe grenzen en enkele end-to-end controles voor kritieke stromen. Mock externe systemen bij duidelijke grenzen, maar test waarneembaar gedrag in plaats van private implementatiedetails.