StopIteration en Python : la fin d'un itérateur

StopIteration est le signal de fin d'un itérateur en Python. Ce que la boucle for en fait, et pourquoi il ne faut jamais la lever à la main.
5 min de lecture
Believemy logo

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 :

PYTHON
lettres = iter(["a", "b"])

next(lettres)  # 'a'
next(lettres)  # 'b'
next(lettres)

# StopIteration

La 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 :

PYTHON
# 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 :

ÉcritureComportement 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'appelPermet 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.

Attention

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

Question

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.

Question

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.

Question

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.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.