1Quels sont les types de données intégrés de Python et lesquels sont mutables ?
Python possède des types scalaires tels que NoneType, bool, int, float et complex ; des types texte et binaires tels que str, bytes et bytearray ; des types séquence tels que list, tuple et range ; et des types collection tels que dict, set et frozenset. La distinction importante en entretien est la mutabilité : list, dict, set et bytearray peuvent changer sur place, tandis que les nombres, bool, str, bytes, tuple, range, frozenset et None sont immuables. Une nuance utile est qu'une référence d'élément de tuple est fixe, mais l'état interne d'un objet mutable référencé peut toujours changer.
2Comment list, tuple, set et dict diffèrent-ils dans un code réel ?
Une liste est une séquence ordonnée et mutable, elle est donc utile pour les files de travail, les résultats collectés et les données ordonnées qui changent. Un tuple est ordonné mais immuable, il est donc utile pour les enregistrements fixes, les retours de fonction et les valeurs qui ne doivent pas être réassignées. Un set stocke des objets uniques et hachables et est idéal pour les vérifications d'appartenance ou la déduplication. Un dict mappe des clés hachables à des valeurs et est la structure normale pour les index, les recherches et les données de type JSON. Dans le Python moderne, dict préserve l'ordre d'insertion, mais son objectif principal reste l'accès basé sur les clés.
items = ["a", "b", "a"]
unique = set(items)
index = {"user_id": 42}
point = (10, 20)
== demande si deux objets sont égaux en valeur, généralement via __eq__. is demande si deux références pointent vers exactement le même objet en mémoire. Dans un code de production, is devrait généralement être réservé aux vérifications de singleton telles que value is None, value is True uniquement lorsque l'identité est vraiment importante, ou les objets sentinelles. Ne comptez pas sur des détails d'implémentation tels que la mise en cache des petits entiers ou l'interning de chaînes ; ceux-ci peuvent faire apparaître is comme fonctionnel dans des exemples tout en étant le mauvais choix sémantique.
4Qu'est-ce qu'un objet hachable et pourquoi est-ce important ?
Un objet hachable possède une valeur de hachage qui reste stable pendant sa durée de vie et un comportement d'égalité cohérent avec ce hachage. L'hachabilité est importante car les clés dict et les éléments set en dépendent. La plupart des intégrés immuables sont hachables, mais tous les objets à l'intérieur d'un conteneur immuable ne deviennent pas automatiquement hachables : un tuple contenant une liste n'est pas hachable. En entretien, reliez cela au design : si un objet est utilisé comme clé, les champs qui participent à __eq__ ou __hash__ ne doivent pas changer après l'insertion, sinon l'objet peut devenir inaccessible dans un dict ou un set.
5Comment fonctionne le slicing, y compris les index négatifs et les étapes ?
Une tranche sequence[start:stop:step] inclut start, exclut stop et avance par step. Les limites manquantes sont inférées à partir de la direction du step. Les index négatifs comptent à partir de la fin, donc s[-1] est le dernier élément. Un pas négatif parcourt de droite à gauche ; s[::-1] crée une séquence inversée pour de nombreux types de séquences. Le compromis est que le slicing crée souvent un nouvel objet, il est donc expressif mais pas toujours gratuit pour les grandes données.
text = "interview"
print(text[1:5])
print(text[::-1])
6Comment Python passe-t-il les arguments aux fonctions ?
Python passe des références d'objets par affectation, souvent appelé appel par partage (call by sharing). La fonction reçoit des noms de paramètres locaux liés aux mêmes objets fournis par l'appelant. Si l'objet est mutable, la fonction peut le muter et l'appelant observera le changement. Si la fonction réaffecte le nom du paramètre, seul le lien local change. Une bonne réponse d'entretien distingue la mutation sur place (in-place) de la réattribution locale : items.append(...) ou items.clear() modifie la liste de l'appelant, tandis que items = [] à l'intérieur de la fonction ne fait pointer le nom local qu'une nouvelle liste.
7Pourquoi les arguments par défaut mutables sont-ils dangereux ?
Les valeurs des arguments par défaut sont évaluées une seule fois lors de la définition de la fonction, et non à chaque appel. Si un argument par défaut est une liste ou un dictionnaire, tous les appels peuvent partager le même objet et fuiter accidentellement l'état. Le modèle sûr consiste à utiliser `def f(items=None):` puis créer `items = []` à l'intérieur si `items is None`. Il existe des cas rares où l'état partagé est intentionnel, comme un petit cache, mais dans ce cas, il doit être explicite et documenté car cela surprend les lecteurs.
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 collecte les arguments positionnels supplémentaires dans un tuple, tandis que **kwargs collecte les arguments par mot-clé supplémentaires dans un dictionnaire. Ils sont utiles pour les wrappers, les décorateurs, les API flexibles et le transfert d'appels. Au site d'appel, *iterable étend les valeurs positionnelles et **mapping étend les valeurs de mots-clés. Le compromis est la clarté : des paramètres explicites sont meilleurs lorsque la fonction a un contrat public stable, tandis que args et kwargs sont meilleurs aux frontières où le transfert ou la compatibilité est importante.
9Comment Python résout les noms de variables avec LEGB ?
Python recherche les noms dans les portées Local, Enclosing, Global et Builtins. La lecture d'un nom suit cet ordre. L'assignation d'un nom à l'intérieur d'une fonction crée normalement un lien local, raison pour laquelle le code peut lever UnboundLocalError s'il lit un nom avant une assignation locale. global redirige l'assignation vers la portée du module, et nonlocal redirige l'assignation vers une portée de fonction englobante. Dans un bon code de production, utilisez les deux avec parcimonie car ils rendent le flux d'état moins évident.
total = 0
def add(value):
global total
total += value
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
10Qu'est-ce qu'une closure et qu'est-ce que le late binding ?
Une closure est une fonction qui conserve l'accès aux variables d'une portée englobante après que cette fonction externe ait retourné. Ceci est utile pour les décorateurs, les usines de rappels (callback factories) et les fonctions configurées. Le late binding signifie que la variable libre est recherchée lorsque la fonction interne est appelée, et non lorsqu'elle est créée. Les lambdas créés dans une boucle peuvent tous voir la valeur finale de la boucle. Capturez la valeur actuelle avec un argument par défaut, par exemple lambda i=i: i, ou utilisez une petite fonction usine.
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]
11Quelle est la différence entre les variables de classe et les variables d'instance ?
Une variable de classe est stockée sur l'objet de classe et est partagée par les instances, à moins qu'une instance ne l'éclipse avec son propre attribut. Une variable d'instance est stockée sur un objet particulier, généralement assignée comme self.name dans __init__. Les constantes de classe immuables sont généralement acceptables, mais les attributs de classe mutables constituent un bug courant lorsqu'ils sont accidentellement utilisés comme état par instance. Lors d'un entretien, mentionnez l'ordre de recherche : Python vérifie d'abord l'instance, puis la classe et ses ancêtres.
class Cart:
items = []
first = Cart()
second = Cart()
first.items.append("book")
print(second.items) # ["book"]
class SafeCart:
def __init__(self):
self.items = []
12Comment fonctionnent l'héritage, le MRO et super() ?
Python résout les attributs via l'Ordre de Résolution des Méthodes (Method Resolution Order), visible avec ClassName.__mro__. Dans l'héritage simple, c'est direct, mais en héritage multiple, Python utilise la linéarisation C3 pour créer un ordre cohérent. super() ne signifie pas exactement 'appeler mon parent' ; cela signifie 'appeler la prochaine implémentation dans le MRO'. Cela rend possible l'héritage multiple coopératif lorsque les méthodes acceptent des arguments compatibles et que chaque classe appelle correctement super().
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
13Quelle est la différence entre un itérable, un itérateur et un générateur ?
Un itérable est tout objet qui peut retourner un itérateur via iter(), comme une liste ou un objet fichier. Un itérateur est l'objet avec état qui implémente __next__ et lève StopIteration lorsqu'il est épuisé. Un générateur est une manière pratique de créer un itérateur avec yield ou une expression génératrice. Les générateurs sont efficaces en mémoire car ils produisent des valeurs paresseusement, mais ils sont généralement à usage unique et ne peuvent pas être indexés comme une liste.
def read_lines(path):
with open(path, encoding="utf-8") as file:
for line in file:
yield line.strip()
14Qu'est-ce qu'un gestionnaire de contexte et pourquoi est-ce que with est utile ?
Un gestionnaire de contexte définit un comportement d'entrée (enter) et de sortie (exit) avec __enter__ et __exit__, ou peut être créé avec contextlib.contextmanager. L'instruction with rend la gestion des ressources explicite et fiable : les fichiers sont fermés, les verrous sont relâchés et les transactions peuvent valider ou annuler même en cas d'exceptions. __exit__ reçoit les informations sur l'exception et peut supprimer l'exception en retournant true, bien que la suppression des erreurs doive être utilisée avec prudence.
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")
15Quelle est la différence entre une copie superficielle (shallow copy) et une copie profonde (deep copy) ?
Une copie superficielle crée un nouveau conteneur externe mais conserve les références aux mêmes objets imbriqués. Une copie profonde copie récursivement les objets imbriqués lorsque cela est possible. Pour des listes plates simples, une copie superficielle est souvent suffisante. Pour des structures mutables imbriquées, la copie superficielle peut toujours partager l'état et vous surprendre. La copie profonde est plus coûteuse et peut échouer avec TypeError ou PicklingError pour des ressources telles que les fichiers ouverts, les sockets, les sessions de base de données ou les objets ayant un comportement sensible à l'identité.
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]]
Exécution en temps réel, asynchrone et code de production
16Qu'est-ce que le GIL et comment affecte-t-il le code backend ?
Dans CPython standard, le Global Interpreter Lock (GIL) ne permet qu'à un seul thread d'exécuter du bytecode Python à la fois. Cela signifie que les threads n'accélèrent généralement pas le code Python lié au CPU. Les threads restent utiles pour le travail lié aux E/S car le GIL est libéré pendant l'attente de nombreuses opérations bloquantes. Pour un travail intensif en CPU, utilisez multiprocessing, des extensions natives, des bibliothèques natives vectorisées ou des services séparés. Pour les entretiens de 2026, mentionnez également les builds CPython sans GIL pour Python 3.13+ : ils rendent le GIL optionnel pour le code compatible, mais ils nécessitent toujours des tests de dépendance et de performance.
17Quand devriez-vous choisir les threads, les processus ou asyncio ?
Utilisez des threads lorsque vous avez des opérations I/O bloquantes ou des bibliothèques qui n'exposent que des API synchrones. Utilisez des processus pour le travail lié au CPU, l'isolation ou la contenance en cas de crash, acceptant ainsi les frais généraux de sérialisation et de mémoire. Utilisez asyncio lorsque vous avez de nombreuses opérations I/O concurrentes et des bibliothèques compatibles avec async, telles que les pilotes de base de données asynchrones ou les clients HTTP. Le choix pratique dépend également de votre framework et de vos dépendances : mélanger du code async avec des appels bloquants peut dégrader les performances.
18À quoi servent les fonctions async/await dans les services web Python ?
async et await permettent une concurrence coopérative. Une coroutine rend le contrôle à la boucle d'événements lorsqu'elle attend des opérations I/O, permettant ainsi à d'autres tâches de s'exécuter. Ceci est précieux pour les API qui attendent des bases de données, des appels HTTP, des files d'attente ou des sockets. Cela ne rend pas magiquement le code lié au CPU plus rapide ; un travail intensif en CPU bloque la boucle d'événements à moins qu'il ne soit déplacé vers un worker, un processus ou du code natif. Dans FastAPI, un endpoint normal def est exécuté dans un pool de threads, tandis que des appels bloquants à l'intérieur d'async def peuvent bloquer la boucle d'événements ; async n'aide que lorsque le chemin d'appel utilise des I/O asynchrones non bloquantes.
19Comment les exceptions doivent-elles être gérées dans un environnement Python de production ?
Attrapez l'exception la plus spécifique que vous pouvez gérer, et attrapez-la à un niveau qui peut récupérer, ajouter du contexte ou le traduire en une erreur métier. Évitez `bare except` et évitez d'engloutir silencieusement les erreurs car cela masque la perte de données et les problèmes opérationnels. Utilisez `raise` pour relancer la même exception et utilisez `raise NewError(...) from exc` pour préserver le contexte causal. Un bon code de production enregistre également les journaux aux limites, renvoie des erreurs HTTP appropriées et maintient les messages d'exception utiles sans divulguer de secrets.
try:
result = parse_payload(payload)
except ValueError as exc:
raise InvalidRequest("Invalid payload") from exc
20Comment structurer et tester un service Python de production ?
Séparer la logique métier du code de framework et d'infrastructure afin que le comportement principal puisse être testé sans serveur ni base de données. Utiliser la configuration à partir de l'environnement ou des paramètres typés, le logging au lieu de print, l'injection de dépendances aux frontières, et des délais d'attente explicites pour les appels réseau. Pour les tests, combiner les tests unitaires pour la logique pure, les tests d'intégration pour les bases de données ou les limites externes, et quelques vérifications de bout en bout pour les flux critiques. Mocker les systèmes externes aux frontières claires, mais tester le comportement observable plutôt que les détails d'implémentation privés.