itertools est un module de la bibliothèque standard qui expose des briques d’itération combinables. Son intérêt n’est pas de remplacer une boucle for par une fonction au nom obscur, mais de manipuler des flux de données sans jamais les charger entièrement en mémoire. Chaque fonction retourne un itérateur paresseux : rien n’est calculé tant qu’on ne consomme pas le résultat. C’est ce qui permet de chaîner des transformations sur des millions d’éléments avec une empreinte mémoire constante.

Voici les fonctions que j’utilise réellement, regroupées par usage, avec les pièges qui font perdre du temps.

Itérateurs infinis : count, cycle, repeat

Ces trois fonctions produisent des flux sans fin. Elles ne sont utiles qu’associées à une condition d’arrêt, sinon la boucle ne se termine jamais.

count(start, step) génère une suite arithmétique infinie. Pratique pour numéroter sans gérer un compteur manuel.

from itertools import count, islice

for i in count(10, 2):
    if i > 20:
        break
    print(i)  # 10, 12, 14, 16, 18, 20

cycle(iterable) répète un itérable indéfiniment. Typique pour alterner entre des ressources (round-robin sur des serveurs, des couleurs, des workers).

from itertools import cycle

couleurs = cycle(['rouge', 'vert', 'bleu'])
for _, couleur in zip(range(5), couleurs):
    print(couleur)  # rouge, vert, bleu, rouge, vert

repeat(elem, times) répète une valeur. Sans times, c’est infini. Sa principale utilité est de fournir un argument constant à map ou starmap.

from itertools import repeat

list(repeat(7, 3))  # [7, 7, 7]
list(map(pow, range(5), repeat(2)))  # [0, 1, 4, 9, 16] — chaque base au carré

Le piège. count(), cycle() et repeat() sans times ne s’arrêtent jamais. Il faut toujours les borner avec islice, takewhile, un zip sur une séquence finie, ou un break. Un list(count()) fige le processus.

Découper et filtrer un flux

islice(iterable, start, stop, step) applique un découpage de type slice sur n’importe quel itérateur, y compris infini, sans le matérialiser en liste. C’est l’outil de référence pour borner un flux.

from itertools import islice, count

list(islice(count(), 5))        # [0, 1, 2, 3, 4]
list(islice(count(), 2, 8, 2))  # [2, 4, 6]

takewhile(pred, iterable) retourne les éléments tant que le prédicat est vrai, puis s’arrête au premier échec. dropwhile fait l’inverse : il saute les éléments tant que le prédicat est vrai, puis retourne tout le reste sans réévaluer.

from itertools import takewhile, dropwhile

data = [2, 3, 8, 1, 9, 4]
list(takewhile(lambda x: x < 5, data))  # [2, 3]       — s'arrête au 8
list(dropwhile(lambda x: x < 5, data))  # [8, 1, 9, 4] — saute 2 et 3, garde le reste

filterfalse(pred, iterable) est le complément de filter : il garde les éléments pour lesquels le prédicat est faux. Plus lisible que filter(lambda x: not pred(x), ...).

from itertools import filterfalse

list(filterfalse(lambda x: x % 2, range(10)))  # [0, 2, 4, 6, 8] — les pairs

compress(data, selectors) filtre data selon un second itérable de booléens. Utile quand le masque de sélection est calculé ailleurs, séparément des données.

from itertools import compress

noms = ['Alice', 'Bob', 'Carol', 'Dan']
actifs = [True, False, True, False]
list(compress(noms, actifs))  # ['Alice', 'Carol']

Combiner et transformer

chain(*iterables) enchaîne plusieurs itérables en une seule séquence, sans créer de liste intermédiaire. Sa variante chain.from_iterable aplatit un itérable d’itérables, idéale pour des flux paresseux.

from itertools import chain

list(chain([1, 2], [3, 4], [5]))               # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3, 4]]))    # [1, 2, 3, 4]

accumulate(iterable, func, initial) produit des résultats cumulés. Par défaut, c’est une somme cumulative, mais n’importe quelle fonction binaire convient (max, operator.mul, etc.). Le paramètre initial (Python 3.8+) fixe une valeur de départ.

from itertools import accumulate
import operator

list(accumulate([1, 2, 3, 4]))               # [1, 3, 6, 10] — somme cumulée
list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24] — produit cumulé
list(accumulate([3, 1, 4, 1, 5], max))       # [3, 3, 4, 4, 5] — maximum courant

starmap(func, iterable) applique une fonction à des arguments déjà groupés en tuples. C’est map quand les arguments sont pré-empaquetés : starmap(f, [(a, b)]) appelle f(a, b).

from itertools import starmap

points = [(3, 4), (6, 8), (5, 12)]
list(starmap(lambda x, y: (x**2 + y**2)**0.5, points))  # [5.0, 10.0, 13.0]

pairwise(iterable) (Python 3.10+) retourne les éléments par paires consécutives qui se chevauchent. Parfait pour calculer des différences ou comparer chaque élément au suivant.

from itertools import pairwise

list(pairwise([1, 2, 3, 4]))  # [(1, 2), (2, 3), (3, 4)]

temps = [10, 13, 12, 18]
[b - a for a, b in pairwise(temps)]  # [3, -1, 6] — variations successives

zip_longest(*iterables, fillvalue) fusionne plusieurs itérables comme zip, mais s’aligne sur le plus long en comblant les trous avec fillvalue au lieu de s’arrêter au plus court.

from itertools import zip_longest

list(zip_longest([1, 2, 3], ['a', 'b'], fillvalue='?'))
# [(1, 'a'), (2, 'b'), (3, '?')]

groupby : le piège du tri préalable

groupby(iterable, key) regroupe les éléments consécutifs partageant la même clé. Le mot important est consécutifs : groupby ne regroupe que des runs adjacents, il ne trie pas. Sur une entrée non triée par la clé, on obtient des groupes fragmentés.

from itertools import groupby

data = [('FR', 'Paris'), ('US', 'NYC'), ('FR', 'Lyon')]

# Mauvais : non trié par pays → 'FR' apparaît en deux groupes séparés
for pays, groupe in groupby(data, key=lambda x: x[0]):
    print(pays, [v for _, v in groupe])
# FR ['Paris']
# US ['NYC']
# FR ['Lyon']

# Bon : trier d'abord par la même clé
data.sort(key=lambda x: x[0])
for pays, groupe in groupby(data, key=lambda x: x[0]):
    print(pays, [v for _, v in groupe])
# FR ['Paris', 'Lyon']
# US ['NYC']

C’est l’erreur la plus fréquente sur cette fonction : oublier le sort avec la même clé que le groupby. Autre subtilité, l’objet groupe est un itérateur partagé : si on passe au groupe suivant sans avoir consommé le précédent, son contenu est perdu. La fonction key se prête bien à operator.itemgetter, plus rapide et lisible qu’une lambda.

tee : dupliquer un itérateur, avec prudence

tee(iterable, n) retourne n itérateurs indépendants à partir d’un seul. Il ne copie pas les données : les itérateurs partagent un buffer interne qui mémorise tout ce que le plus lent n’a pas encore consommé.

from itertools import tee

it = iter([1, 2, 3, 4])
a, b = tee(it, 2)
list(a)  # [1, 2, 3, 4]
list(b)  # [1, 2, 3, 4]

Deux pièges réels. D’abord, ne plus toucher à l’itérable source après tee : continuer à le consommer désynchronise les copies. Ensuite, si l’un des itérateurs prend beaucoup d’avance sur l’autre, le buffer interne grossit pour retenir les éléments en attente. Dupliquer un flux puis consommer entièrement la première copie avant la seconde revient à tout garder en mémoire, ce qui annule l’avantage de la paresse. tee est efficace seulement si les copies avancent à peu près au même rythme.

Combinatoire : product, permutations, combinations

Ces fonctions génèrent des arrangements. Elles restent paresseuses, mais attention : le nombre de résultats explose vite (factoriel ou exponentiel).

product(*iterables, repeat) calcule le produit cartésien. Il remplace les boucles for imbriquées.

from itertools import product

list(product([1, 2], ['a', 'b']))   # [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
list(product([0, 1], repeat=3))     # toutes les combinaisons binaires sur 3 bits

permutations(iterable, r) génère tous les arrangements ordonnés de longueur r (l’ordre compte). combinations(iterable, r) génère les sous-ensembles non ordonnés de longueur r (l’ordre ne compte pas). combinations_with_replacement autorise en plus la répétition d’un même élément.

from itertools import permutations, combinations, combinations_with_replacement

list(permutations('ABC', 2))                  # AB AC BA BC CA CB — 6 arrangements
list(combinations('ABC', 2))                  # AB AC BC          — 3 combinaisons
list(combinations_with_replacement('ABC', 2)) # AA AB AC BB BC CC — 6 avec répétition

Le piège. permutations(range(10)) produit 3 628 800 tuples. Ne jamais envelopper ces fonctions dans un list() sans borner la taille de l’entrée, sous peine de saturer la mémoire. Itérer directement avec un break ou un islice quand on ne cherche qu’un échantillon.

Récapitulatif

FonctionRôleÀ retenir
count / cycle / repeatflux infinisborner avec islice ou takewhile
isliceslice paresseuxfonctionne sur un itérateur infini
takewhile / dropwhilecouper selon un prédicats’arrête / saute au premier basculement
filterfalse / compressfiltragecomplément de filter / masque externe
chainconcaténer des itérablesfrom_iterable pour aplatir
accumulaterésultats cumulésfunc et initial personnalisables
starmapmap sur des tuples d’argumentsf((a, b))f(a, b)
pairwisepaires consécutivesPython 3.10+, idéal pour les écarts
zip_longestzip sans troncaturecomble avec fillvalue
groupbyregrouper des runstrier d’abord par la clé
teedupliquer un itérateurcoûteux mémoire si désynchronisé
product / permutations / combinationscombinatoirela sortie explose, borner l’entrée

itertools n’apporte pas de fonctionnalité impossible à écrire à la main. Il apporte des primitives en C, testées, qui composent entre elles et qui préservent la paresse de bout en bout. Là où une compréhension de liste matérialise tout, un pipeline itertools traite un élément à la fois. Sur de gros volumes, c’est la différence entre un script qui tient en mémoire et un qui sature. Pour les structures de données complémentaires, voir le module collections.