Définition
Quand on tire les éléments d'une source un par un, avec next(), il faut un moyen de savoir qu'il n'y en a plus. Renvoyer None ne suffirait pas : rien n'empêche None d'être une valeur légitime de la source, et confondre une fin réelle avec une donnée ordinaire romprait le code au premier cas particulier venu. Il fallait donc un signal qui ne se confonde jamais avec ce qui est parcouru.
StopIteration est ce signal : l'exception qu'un itérateur lève lorsqu'il n'a plus rien à donner. Ce n'est pas une panne mais une convention, celle par laquelle une source annonce qu'elle est arrivée au bout. Chaque appel à next() consomme un élément et le retire au passage ; quand il n'en reste aucun, l'appel suivant lève StopIteration au lieu d'inventer une valeur.
Sur une liste de deux lettres, cela donne ceci :
lettres = iter(["a", "b"])
next(lettres) # 'a'
next(lettres) # 'b'
next(lettres)
# StopIterationLa source peut être une liste, un tuple, une chaîne, un fichier ouvert ou un générateur : le protocole ne change pas d'un cas à l'autre. Cette identité permet à une seule boucle de parcourir des objets de natures très différentes sans jamais adapter son écriture.
La boucle for l'attrape déjà
On rencontre rarement cette exception en pratique : la boucle for l'intercepte avant qu'elle n'atteigne le reste du programme. Écrire for element in collection: revient à demander un itérateur, à appeler next() en boucle, puis à sortir dès que le signal de fin se présente. Le lecteur n'a jamais besoin de gérer StopIteration lui-même, la boucle l'a déjà fait à sa place.
Voici ce que cache cette syntaxe :
# Ce qu'une boucle for fait réellement
iterateur = iter(collection)
while True:
try:
element = next(iterateur)
except StopIteration:
break
traiter(element)Cette équivalence explique un comportement qui surprend souvent : un itérateur déjà parcouru ne se reparcourt pas. Une fois que le while interne a atteint StopIteration, il n'y a plus rien à consommer, et une seconde boucle for sur le même objet ne produit rien du tout, sans la moindre erreur affichée.
Elle explique aussi pourquoi des outils bâtis sur ce protocole, comme enumerate ou zip, s'arrêtent au premier signal de fin reçu, sans attendre que les autres sources se vident. Dès que l'une des sources combinées lève StopIteration, ils s'arrêtent immédiatement, même si une autre en a encore.
Trois façons de traiter la fin
Une boucle for absorbe le signal à la place du lecteur. Mais que faire quand on appelle next() directement, hors de toute boucle ? C'est la seule situation où l'exception remonte jusqu'à l'écran, et trois écritures y répondent, selon ce que la fin de la source représente pour le programme.
Le tableau suivant les résume, de la plus stricte à la plus permissive :
| Écriture | Comportement sur une source épuisée |
|---|---|
next(iterateur) | Lève StopIteration et interrompt le programme |
next(iterateur, None) | Renvoie la valeur par défaut, sans rien lever |
try autour de l'appel | Permet de traiter la fin comme un cas métier à part |
Le deuxième argument de next est en général le plus pratique des trois : il transforme l'exception en valeur ordinaire, ce qui évite d'entourer trois lignes de code d'un bloc de rattrapage pour un cas pourtant parfaitement prévisible. Le bloc try garde son utilité quand la fin doit déclencher un traitement à part, comme journaliser l'événement plutôt que continuer avec une valeur par défaut.
Le piège du générateur
Un générateur, c'est-à-dire une fonction qui contient un yield, lève lui aussi StopIteration quand il arrive au bout de son travail. Cela soulève une question : que devient un return écrit dans son corps, puisqu'une fonction ordinaire renvoie une valeur avec ce mot clé ? Rien de tel ici : le return d'un générateur ne renvoie pas une valeur, il met fin à la production, et rien de plus.
Avant Python 3.7, une StopIteration levée par erreur à l'intérieur d'un générateur, un next() oublié dans son corps par exemple, mettait fin à la boucle appelante sans le moindre message, et les données manquantes passaient inaperçues pendant des mois. Depuis cette version, ce cas est converti en RuntimeError, pour que l'erreur ne puisse plus se cacher.
Retenez surtout ceci : ne levez jamais StopIteration à la main pour interrompre un traitement. break arrête une boucle, return arrête une fonction, et l'un des deux suffit toujours.
Questions fréquentes
Faut-il rattraper StopIteration dans son propre code ?
Seulement autour d'un appel direct à next(), et même là, la valeur par défaut fait le même travail en une ligne. Partout ailleurs, la boucle s'en charge déjà, et intercepter l'exception soi-même revient à réécrire ce que le langage fait très bien tout seul.
Pourquoi une seconde boucle sur le même objet ne donne-t-elle rien ?
Parce qu'un itérateur ne se rembobine pas. Une liste en fournit un nouveau à chaque parcours, ce qui la rend parcourable indéfiniment, alors qu'un générateur ou un fichier ouvert est lui-même l'itérateur : une fois épuisé, il le reste pour de bon.
Quelle différence avec une erreur classique du programme ?
Celle-ci décrit un déroulement normal, pas une panne. Elle appartient donc à une branche différente des erreurs de valeur ou de type, et un rattrapage trop large risque de la confondre avec un vrai défaut, masquant les deux à la fois.