Vol. 1 — № 01
The Python Loop · KiosqueNewsstand
Blog  
Un atelier Python · Édition d'ApprentissageA Python Workshop · Learning Edition

b = a ne copie rien, il colle une seconde étiquette b = a copies nothing, it sticks a second label

Trois lignes que tout le monde écrit le premier jour. On assigne b = a en croyant copier la liste, on modifie « seulement » b — et a a changé aussi. Ce n'est pas un bug : c'est tout le modèle objet de Python qui tient dans un signe égal. Un nom n'est pas une boîte qui contient une valeur ; c'est une étiquette collée sur un objet. Three lines everyone writes on day one. You assign b = a thinking you copied the list, you change 'only' b — and a changed too. It's no bug: it's all of Python's object model fitting inside an equals sign. A name isn't a box that holds a value; it's a label stuck onto an object.

AudienceAudience
Dev venant d'un langage typé, prêt à raisonner références Dev from a typed language, ready to reason about references
Format
Self-paced
ChapitresChapters
5
Date
Juil 2026 Jul 2026
≈ 17 min ●○○○ Noms & objetsRéférencesMutabilité
SommaireContents ·
01CadrageFraming3 min

Un signe égal — = — et deux noms sur un objet. Qui le mute ?One equals sign — = — and two names on one object. Who mutates it?

Quatre lignes que tout le monde écrit le premier jour. Le programme tourne, n'affiche pas ce qu'on attend, et pourtant rien n'a planté. Ce n'est pas un bug de append : c'est le modèle objet entier de Python qui tient dans un seul =. Assigner un nom est trivial ; comprendre ce que le nom désigne est tout le sujet.Four lines everyone writes on day one. The program runs, prints not what you expected, and yet nothing crashed. It's no append bug: it's all of Python's object model fitting inside a single =. Binding a name is trivial; understanding what the name points to is the whole point.

Le code qu'on croit anodinThe code we think is harmless
a = [1, 2, 3]
b = a                 # on croit copier la liste
b.append(4)           # on ne touche « que » b
print(a)              # [1, 2, 3, 4] — a a changé aussi
Deux noms, un objetTwo names, one object
a
b
pointent versboth point to
[1, 2, 3, 4]
list · 1 objet

Le = n'a pas dupliqué la liste : il a collé un second nom sur le même objet. b.append(4) mute cet objet — et a, qui le désigne aussi, « voit » la mutation.The = didn't duplicate the list: it stuck a second name onto the same object. b.append(4) mutates that object — and a, which points to it too, "sees" the mutation.

Le réflexe du volumeThe volume's reflex

« a = b copie-t-il, ou lie-t-il un second nom au même objet ? » En Python, l'assignation ne copie jamais l'objet. Elle lie un nom. Tant que tu vois une variable comme une boîte qui contient une valeur, la mutation partagée restera une magie noire."Does a = b copy, or bind a second name to the same object?" In Python, assignment never copies the object. It binds a name. As long as you see a variable as a box that holds a value, shared mutation will stay black magic.

Pourquoi ce numéro ouvre la série.Why this issue opens the series.

Tout le reste du modèle objet de Python — aliasing, copies, mutabilité, arguments par défaut, closures — est une conséquence de cette phrase. On ne commence pas par la syntaxe des classes : on commence par un = qui ne copie rien. Comprends ce qu'un nom désigne, et Python cesse d'être une loterie d'effets de bord.Everything else in Python's object model — aliasing, copies, mutability, default arguments, closures — follows from this one sentence. We don't start with class syntax: we start with an = that copies nothing. Understand what a name points to, and Python stops being a lottery of side effects.

02La surpriseThe surprise4 min

L'assignation ne copie jamais. C'est muter ou rebind.Assignment never copies. It's mutate or rebind.

« Mais avec un nombre, ça marche ! » — et c'est là que le modèle se dérobe. On croit que l'assignation copie parfois (les nombres) et parfois non (les listes). Faux : elle ne copie jamais. La vraie différence est ailleurs — entre un objet qu'on mute en place et un nom qu'on rebind sur un objet neuf. L'opérateur +=, le même signe, fait les deux selon le type.'But with a number, it works!' — and that's where the model slips away. You think assignment sometimes copies (numbers) and sometimes doesn't (lists). Wrong: it never copies. The real difference is elsewhere — between mutating an object in place and rebinding a name to a fresh object. The += operator, one sign, does both depending on the type.

int — += rebind sur un nouvel objetint — += rebinds to a new object
a = 1
b = a          # b pointe le même int 1
b += 1         # b += 1 NE mute pas 1 : il rebind b sur un NOUVEL objet 2
print(a, b)    # 1 2 — a n'a pas bougé

Un int est immuable : on ne peut pas changer la valeur 1. Donc b += 1 ne touche pas l'objet — il crée l'objet 2 et recolle l'étiquette b dessus. a tient toujours le 1 d'origine. Rien n'a été copié : un nom a simplement changé de cible.An int is immutable: you can't change the value 1. So b += 1 doesn't touch the object — it creates the object 2 and re-sticks the label b onto it. a still holds the original 1. Nothing was copied: a name simply changed target.

L'assignation ne copie jamais l'objet. Elle lie un nom. Ce qui change, c'est si l'objet visé se laisse muter.Assignment never copies the object. It binds a name. What changes is whether the target object lets itself be mutated.
03L'identitéIdentity5 min

Deux questions, deux opérateurs : même valeur, ou même objet ?Two questions, two operators: same value, or same object?

Pour raisonner sur les alias, il faut distinguer deux questions que le langage garde séparées. == demande « ces deux objets ont-ils la même valeur ? ». is demande « ces deux noms désignent-ils le même objet ? ». Confondre les deux, c'est le bug qui attend au tournant — surtout quand on écrit is par habitude d'un autre langage.To reason about aliases, you must split two questions the language keeps apart. == asks 'do these two objects have the same value?'. is asks 'do these two names point to the same object?'. Confusing them is the bug waiting around the corner — especially when you write is out of another language's habit.

== compare le contenu — is compare l'étiquette== compares content — is compares the label
x = [1, 2]
y = [1, 2]      # même contenu, construit séparément
print(x == y)   # True  — égalité : mêmes valeurs
print(x is y)   # False — identité : deux objets distincts

x et y ont le même contenu — == répond True. Mais ce sont deux listes construites séparément : deux objets, deux adresses. is répond False. == appelle __eq__ et peut être coûteux ; is ne compare que l'identité (id()), instantané. Presque toujours, ce que tu veux, c'est ==.x and y have the same content — == answers True. But they're two lists built separately: two objects, two addresses. is answers False. == calls __eq__ and can be costly; is only compares identity (id()), instant. Almost always, what you want is ==.

Le réflexe en une ligneThe reflex in one line

Devant deux noms, demande-toi laquelle des deux questions tu poses : « même valeur ? » → ==. « même objet, donc mutation partagée ? » → is. Le bug naît quand on pose l'une en croyant poser l'autre.Facing two names, ask which of the two questions you're posing: "same value?" → ==. "same object, hence shared mutation?" → is. The bug is born when you ask one thinking you asked the other.

04Le piègeThe trap4 min

Le défaut mutable : un objet évalué une fois, partagé à jamais.The mutable default: an object evaluated once, shared forever.

On tient maintenant le réflexe « un nom, un objet ». Voici où il se paie le plus cher, dans le piège le plus célèbre de Python. La valeur par défaut d'un paramètre est un objet comme un autre — et il est créé une seule fois, quand def s'exécute, pas à chaque appel. Un défaut mutable devient un état caché qui survit entre les appels.You now hold the 'one name, one object' reflex. Here's where it costs the most, in Python's most famous trap. A parameter's default value is an object like any other — and it's created once, when def runs, not on each call. A mutable default becomes hidden state that survives across calls.

panier=[] — un seul objet pour tous les appelspanier=[] — one object for every call
def ajouter(item, panier=[]):   # le [] est créé UNE fois, à la définition
    panier.append(item)
    return panier

print(ajouter("a"))   # ['a']
print(ajouter("b"))   # ['a', 'b'] — le même panier, d'un appel à l'autre !
print(ajouter("c"))   # ['a', 'b', 'c'] — il ne se vide jamais

Le [] n'est pas ré-évalué à chaque appel : il est construit une fois, à la lecture du def, et rangé sur la fonction (ajouter.__defaults__). Chaque appel sans argument reçoit ce même objet, qu'il mute. Le résultat s'accumule, silencieusement, entre des appels qui se croient indépendants. Ce n'est pas un crash — c'est un état qui fuit.The [] isn't re-evaluated on each call: it's built once, when the def is read, and stored on the function (ajouter.__defaults__). Every call without an argument gets that same object, and mutates it. The result piles up, silently, across calls that believe they're independent. It's not a crash — it's leaking state.

Le contre-exempleThe counter-example

Et parfois, le défaut partagé est voulu. Un cache de mémoïsation caché dans un def compute(x, _cache=) exploite exactement cette persistance entre appels — l'astuce est réelle, connue, documentée. La frontière n'est pas « mutable = mal » : c'est « l'état partagé est-il une intention, ou un accident ? ».And sometimes the shared default is intended. A memoization cache hidden in def compute(x, _cache=) exploits exactly this cross-call persistence — the trick is real, known, documented. The line isn't "mutable = bad": it's "is the shared state intent, or accident?".

is None, pas == None.is None, not == None.

Le test du sentinelle s'écrit toujours panier is None. Un == appellerait __eq__, qu'un objet exotique peut redéfinir pour se dire « égal à None » — et ton garde-fou saute. is compare l'identité : il n'y a qu'un seul None dans tout l'interpréteur, et rien ne peut se faire passer pour lui.The sentinel test is always written panier is None. A == would call __eq__, which some exotic object can redefine to claim it's 'equal to None' — and your guard breaks. is compares identity: there's a single None in the whole interpreter, and nothing can impersonate it.

Le défaut d'un paramètre est évalué une fois, à la définition — pas à chaque appel. Un objet mutable en défaut est un état global déguisé.A parameter's default is evaluated once, at definition — not on each call. A mutable default is global state in disguise.
05Bilan & éditoWrap-up & editorial2 min

Quatre gestes pour ne plus confondre le nom et l'objet.Four moves to stop confusing the name with the object.

« Un nom est une étiquette, pas une boîte » n'est pas une image de plus : c'est l'unité qui fait tenir tout le reste du modèle objet de Python. Une fois le réflexe en place — que désigne ce nom, et qui d'autre le désigne ? — l'aliasing, les copies, les arguments par défaut et les closures se lisent comme ses conséquences directes.'A name is a label, not a box' isn't one more metaphor: it's the unit that holds all the rest of Python's object model together. Once the reflex is in place — what does this name point to, and who else points to it? — aliasing, copies, default arguments and closures read as its direct consequences.

Tu veux…You want to…Le gesteThe moveCe que ça coûteWhat it costs
Une vraie copieA real copyExplicite : list(x), x[:], dict(x) — ou copy.deepcopy si imbriqué. Vol 2 · №05.Explicit: list(x), x[:], dict(x) — or copy.deepcopy if nested. Vol 2 · №05.La copie de surface partage encore les éléments internes. deepcopy coûte, et copie aussi ce qu'on voulait partager.A shallow copy still shares the inner elements. deepcopy costs, and also copies what you meant to share.
Tester la valeurTest the value== : appelle __eq__, compare le contenu. Vol 3 · №09.==: calls __eq__, compares content. Vol 3 · №09.Peut être coûteux sur de grosses structures, et redéfinissable — donc parfois surprenant.Can be costly on large structures, and overridable — hence sometimes surprising.
Tester l'identitéTest identityis : même objet ? Réservé aux singletons — x is None.is: same object? Reserved for singletons — x is None.Sur des entiers ou des chaînes, is « marche » par hasard (cache) puis casse. Vol 9 · №26.On ints or strings, is 'works' by accident (caching) then breaks. Vol 9 · №26.
Un défaut mutableA mutable defaultDéfaut None, construction dans le corps : if x is None: x = [].Default None, build in the body: if x is None: x = [].Oublier l'idiome, et le défaut devient un état partagé qui survit entre les appels.Skip the idiom, and the default becomes shared state surviving across calls.
« = » est le signe le plus court et le plus trompeur de Python. Pas parce qu'il copie — parce qu'on croit qu'il copie."=" is the shortest and most deceptive sign in Python. Not because it copies — because we think it copies.
Mot de l'éditeurFrom the editor

« Simple » ne veut pas dire « facile »."Simple" does not mean "easy".

On vend souvent Python comme le langage où « tout est intuitif » : pas de types à déclarer, pas de pointeurs, on assigne et ça marche. C'est vrai en surface, et c'est exactement le piège. Tant qu'on voit une variable comme une boîte qui contient une valeur — on copie sans copier, on mute à distance, on sème des [] par défaut — on reconstruit au runtime, en effets de bord et en états partagés, les bugs qu'une seule image aurait évités. Le jour où chaque nom s'accompagne, dans la même pensée, de la question « quel objet, et qui d'autre le tient ? », Python cesse d'être une loterie.Python is often sold as the language where 'everything is intuitive': no types to declare, no pointers, you assign and it works. That's true on the surface, and that's exactly the trap. As long as you see a variable as a box that holds a value — copying without copying, mutating at a distance, scattering default []s — you rebuild at runtime, in side effects and shared state, the very bugs one image would have avoided. The day every name comes, in the same thought, with the question 'which object, and who else holds it?', Python stops being a lottery.

Prochain numéro : mutable contre immuable, en grand. Pourquoi x += 1 et lst += [1] font deux choses opposées sous le même signe, ce qu'un tuple protège vraiment (et ce qu'il ne protège pas quand il contient une liste), et l'int caché derrière chaque nom.Next issue: mutable versus immutable, at scale. Why x += 1 and lst += [1] do two opposite things under the same sign, what a tuple truly protects (and what it doesn't when it holds a list), and the int hidden behind every name.

Retour au kiosqueBack to newsstand