3 Pieges Python a Eviter : Arguments Mutables, Iteration et Identite

Evitez les erreurs courantes en Python : valeurs par defaut mutables, modification pendant iteration et comparaison d'identite. Solutions et bonnes pratiques incluses.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 8 min read
3 Pieges Python a Eviter : Arguments Mutables, Iteration et Identite

Introduction

Python est un langage elegant et puissant, mais il cache quelques pieges subtils qui peuvent surprendre meme les developpeurs experimentes. Ces pieges ne sont pas des bugs du langage, mais plutot des comportements qui decoulent de choix de conception specifiques a Python.

En tant que developpeur, vous avez probablement deja rencontre des bugs mysterieux ou des comportements inattendus dans votre code Python. Souvent, ces problemes proviennent de trois sources principales que nous allons explorer en detail dans cet article.

Pourquoi ces pieges sont-ils si courants ?

  • Les arguments mutables par defaut semblent intuitifs mais cachent un comportement contre-intuitif
  • La modification d’une collection pendant son iteration est une erreur naturelle a commettre
  • La difference entre is et == n’est pas toujours evidente pour les nouveaux developpeurs

Les sujets couverts seront :

  1. Les valeurs par defaut mutables - Pourquoi elles sont evaluees une seule fois
  2. La modification pendant l’iteration - Comment eviter les resultats inattendus
  3. L’identite vs l’egalite - Quand utiliser is et quand utiliser ==

Nous allons fournir des exemples de code detailles, des explications approfondies, et surtout des solutions pratiques que vous pourrez appliquer immediatement dans vos projets.

1. Les valeurs par defaut de fonctions et methodes

Comprendre le probleme fondamental

En Python, les arguments par defaut sont evalues une seule fois, au moment de la definition de la fonction, et non a chaque appel. Cette caracteristique est souvent source de bugs difficiles a detecter.

La valeur par defaut est partagee entre instances

Imaginez une classe Car avec une propriete wheels definie comme attribut de classe :

class Wheel:
    def __init__(self, size=17):
        self.size = size

# PROBLEME : attribut de classe mutable
class Car:
    wheels = []  # Partage entre TOUTES les instances !

    def add_wheel(self, wheel):
        self.wheels.append(wheel)

# Demonstration du probleme
car1 = Car()
car1.add_wheel(Wheel())
print(f"Car1 wheels: {len(car1.wheels)}")  # 1

car2 = Car()
car2.add_wheel(Wheel())
print(f"Car2 wheels: {len(car2.wheels)}")  # 2 - Surprise !
print(f"Car1 wheels: {len(car1.wheels)}")  # 2 - Car1 aussi affecte !

Dans cet exemple, toutes les instances de Car partagent la meme liste de roues car wheels est un attribut de classe.

La bonne facon d’initialiser les valeurs par defaut

Pour eviter ce probleme, initialisez toujours les attributs mutables dans le constructeur (__init__) :

class Car:
    def __init__(self):
        self.wheels = []  # Nouvelle liste pour CHAQUE instance

    def add_wheel(self, wheel):
        self.wheels.append(wheel)

# Verification
car1 = Car()
car1.add_wheel(Wheel())
print(f"Car1 wheels: {len(car1.wheels)}")  # 1

car2 = Car()
car2.add_wheel(Wheel())
print(f"Car2 wheels: {len(car2.wheels)}")  # 1 - Correct !
print(f"Car1 wheels: {len(car1.wheels)}")  # 1 - Toujours correct !

Les valeurs par defaut de fonctions

Le meme probleme existe avec les fonctions. Les arguments par defaut sont evalues a la definition, pas a chaque appel :

def foo(li=[]):
    li.append(1)
    print(li)

# Avec argument explicite - fonctionne comme attendu
foo([2])  # [2, 1]
foo([3])  # [3, 1]

# Sans argument - comportement inattendu
foo()  # [1]
foo()  # [1, 1] - La liste s'accumule !
foo()  # [1, 1, 1] - Continue a s'accumuler !

La solution idiomatique Python

Utilisez None comme valeur par defaut et creez l’objet mutable a l’interieur de la fonction :

def foo(li=None):
    if li is None:
        li = []
    li.append(1)
    print(li)

foo()  # [1]
foo()  # [1] - Correct !
foo()  # [1] - Toujours correct !

Cas d’utilisation avance : exploiter ce comportement

Dans certains cas rares, ce comportement peut etre utile, par exemple pour implementer un cache simple :

def fibonacci(n, cache={}):
    if n in cache:
        return cache[n]
    if n <= 1:
        return n
    result = fibonacci(n-1) + fibonacci(n-2)
    cache[n] = result
    return result

# Le cache persiste entre les appels
print(fibonacci(10))  # 55
print(fibonacci(100))  # Tres rapide grace au cache

2. La modification d’une sequence pendant une iteration

Comprendre pourquoi c’est dangereux

Modifier une liste pendant qu’on l’itere est l’une des erreurs les plus courantes en Python. Le probleme vient du fait que l’iterateur utilise un index interne qui devient desynchronise lorsque la taille de la liste change.

Probleme lors de la suppression d’elements

Imaginons que vous voulez supprimer tous les elements d’une liste :

alist = [0, 1, 2]
for index, value in enumerate(alist):
    print(f"Index: {index}, Value: {value}, List: {alist}")
    alist.pop(index)

print(f"Resultat final: {alist}")  # [1] - Pas vide !

Que se passe-t-il ?

  1. Iteration 0 : on supprime l’element a l’index 0 (valeur 0), la liste devient [1, 2]
  2. Iteration 1 : l’index est maintenant 1, on supprime l’element a l’index 1 (valeur 2), la liste devient [1]
  3. Iteration 2 : l’index 2 n’existe plus, la boucle se termine

L’element 1 n’a jamais ete visite car il est “passe” a l’index 0 pendant que l’iterateur avancait a l’index 1.

Solutions recommandees

Solution 1 : Iterer en sens inverse

En iterant de la fin vers le debut, les suppressions n’affectent pas les indices des elements restants :

alist = [1, 2, 3, 4, 5]
for index, item in reversed(list(enumerate(alist))):
    if item % 2 == 0:
        alist.pop(index)
print(alist)  # [1, 3, 5]

Solution 2 : List comprehension (recommande)

La facon la plus pythonique et la plus performante :

alist = [1, 2, 3, 4, 5]
alist = [item for item in alist if item % 2 != 0]
print(alist)  # [1, 3, 5]

Solution 3 : Utiliser filter()

Pour les cas plus complexes ou avec des fonctions existantes :

alist = [1, 2, 3, 4, 5]
alist = list(filter(lambda x: x % 2 != 0, alist))
print(alist)  # [1, 3, 5]

Solution 4 : Creer une nouvelle liste

Si vous devez effectuer des operations plus complexes :

zlist = [1, 2, 3, 4, 5]
z_temp = []
for item in zlist:
    if item % 2 != 0:
        z_temp.append(item)
zlist = z_temp
print(zlist)  # [1, 3, 5]

Attention aux dictionnaires aussi

Le meme probleme existe avec les dictionnaires :

# ERREUR : RuntimeError: dictionary changed size during iteration
d = {'a': 1, 'b': 2, 'c': 3}
# for key in d:
#     if d[key] == 2:
#         del d[key]

# SOLUTION : copier les cles
d = {'a': 1, 'b': 2, 'c': 3}
for key in list(d.keys()):  # list() cree une copie
    if d[key] == 2:
        del d[key]
print(d)  # {'a': 1, 'c': 3}

3. L’identite des entiers et des chaines de caracteres

Comprendre is vs ==

Avant d’aborder le caching, clarifions la difference fondamentale :

  • == compare les valeurs (egalite)
  • is compare les identites (meme objet en memoire)
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True - memes valeurs
print(a is b)  # False - objets differents en memoire
print(a is c)  # True - c pointe vers le meme objet que a

Les entiers internes sont stockes en cache

Pour des raisons de performance, Python pre-cree et met en cache les entiers de -5 a 256 (plage frequemment utilisee). Cela peut conduire a des comportements surprenants :

# Dans la plage du cache (-5 a 256)
a = 100
b = 100
print(a is b)  # True - meme objet en cache

# Hors de la plage du cache
x = 1000
y = 1000
print(x is y)  # False - objets differents

# Exemples avec calculs
-8 is (-7 - 1)      # False (hors cache)
-3 is (-2 - 1)      # True (dans cache)
(255 + 1) is 256    # True (dans cache)
(256 + 1) is 257    # False (hors cache)

Regle d’or : Utilisez toujours == pour comparer des valeurs :

-8 == (-7 - 1)       # True
-3 == (-2 - 1)       # True
(255 + 1) == 256     # True
(256 + 1) == 257     # True

Les chaines de caracteres et l’interning

Python utilise egalement un mecanisme appele string interning pour les chaines qui ressemblent a des identifiants (lettres, chiffres, underscores) :

# Chaines simples - internees
a = 'hello'
b = 'hello'
print(a is b)  # True

# Concatenation a la compilation
'python' is 'py' + 'thon'  # True (optimise par le compilateur)

# Chaines avec espaces - pas internees
x = 'hello world'
y = 'hello world'
print(x is y)  # Resultat imprevisible !

# Concatenation a l'execution
s1 = 'py'
s2 = 'thon'
print('python' is s1 + s2)  # False (pas optimise)

Regle d’or : Utilisez toujours == pour comparer des chaines :

'python' == 'py' + 'thon'  # True - toujours fiable

Cas particulier : None

None est un singleton en Python. Il n’existe qu’une seule instance de None, donc utiliser is est non seulement correct mais recommande :

x = None

# Recommande
if x is None:
    print("x est None")

# Fonctionne mais moins idiomatique
if x == None:
    print("x est None")

Bonnes Pratiques

Resume des patterns a adopter

  1. Arguments par defaut mutables

    # MAL
    def func(items=[]):
        items.append(1)
        return items
    
    # BIEN
    def func(items=None):
        if items is None:
            items = []
        items.append(1)
        return items
  2. Modification pendant iteration

    # MAL
    for item in items:
        if condition(item):
            items.remove(item)
    
    # BIEN
    items = [item for item in items if not condition(item)]
  3. Comparaison de valeurs

    # MAL (pour les valeurs)
    if x is 100:
        pass
    
    # BIEN
    if x == 100:
        pass
    
    # Exception : None
    if x is None:  # Correct !
        pass

Pieges Courants

Les erreurs les plus frequentes

PiegeSymptomeSolution
Liste par defautDonnees partagees entre appelsUtiliser None
Dict par defautIdemUtiliser None
Supprimer en iterantElements manquesList comprehension ou reversed
is avec entiersResultats incoherentsUtiliser ==
is avec chainesResultats imprevisiblesUtiliser ==

Comment detecter ces problemes

# Verifier les arguments par defaut d'une fonction
def func(items=[]):
    pass

print(func.__defaults__)  # ([],) - meme objet a chaque appel !

# Verifier l'identite des objets
print(id(257))  # Adresse memoire
print(id(257))  # Adresse differente !

Conclusion

Ces trois pieges Python sont parmi les plus courants et les plus frustrants pour les developpeurs. Ils decoulent de decisions de conception legitimes du langage (performance, simplicite), mais peuvent causer des bugs subtils si on ne les comprend pas.

Points cles a retenir :

  1. Arguments mutables par defaut : Toujours utiliser None comme valeur par defaut pour les listes, dictionnaires et autres objets mutables. Creez l’objet dans le corps de la fonction.

  2. Modification pendant l’iteration : Ne jamais modifier une collection pendant qu’on l’itere. Preferez les list comprehensions ou iterez en sens inverse.

  3. Identite vs Egalite : Utilisez == pour comparer des valeurs, reservez is uniquement pour None, True, False et les comparaisons de types.

En appliquant ces bonnes pratiques, vous eviterez des heures de debogage et ecrirez du code Python plus robuste et plus maintenable. Ces concepts sont egalement des questions classiques en entretien technique, donc les maitriser vous sera doublement utile !

Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles