Une collection éditoriale pour le développeur qui ne veut pas confondre simple et facile. Un numéro = un mécanisme-clé, disséqué jusqu'à ce que la tension soit nue : quel bug il tient à distance, ce que le dynamisme te laisse porter, et quand ne pas l'employer. On arrête de réciter la syntaxe — on écoute le modèle. Un nom n'est pas une boîte ; c'est une étiquette collée sur un objet. An editorial collection for the developer who refuses to confuse simple with easy. One issue = one key mechanism, dissected until the tension is bare: which bug it keeps at arm's length, what dynamism leaves on your shoulders, and when not to use it. You stop reciting syntax — you listen to the model. Names are not boxes; they are labels stuck onto objects.
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.
Un seul opérateur, deux gestes opposés. x += 1 fabrique un nouvel objet et recolle l'étiquette ; lst += [1] mute la liste sur place. Le signe ne décide de rien — c'est le type de l'objet qui tranche. Et quand un tuple « immuable » contient une liste, la même ligne peut muter l'objet ET lever une exception dans la foulée. One operator, two opposite moves. x += 1 builds a new object and re-sticks the label; lst += [1] mutates the list in place. The sign decides nothing — the object's type does. And when an 'immutable' tuple holds a list, the same line can mutate the object AND raise an exception in one breath.
Le défaut d'un paramètre n'est pas recalculé à chaque appel : l'expression tourne une seule fois, quand def s'exécute, et le résultat se range sur la fonction. Un défaut mutable devient alors un état partagé qui survit entre les appels — et un def f(t=time.time()) fige l'instant de sa propre naissance. L'antidote tient en un mot immuable : None. A parameter's default isn't recomputed on each call: the expression runs once, when def executes, and the result is stored on the function. A mutable default then becomes shared state surviving across calls — and def f(t=time.time()) freezes the instant of its own birth. The antidote fits in one immutable word: None.
Passer un objet à une fonction ne le copie pas : le paramètre devient un second nom sur le même objet. Muter cet objet en place se voit chez l'appelant ; réassigner le nom local, non. Toute la différence entre une fonction qui rend un service et une qui saccage tes données tient dans cette ligne de partage — muter, ou rebind. Passing an object to a function doesn't copy it: the parameter becomes a second name on the same object. Mutating that object in place shows in the caller; rebinding the local name doesn't. The whole difference between a function that helps and one that wrecks your data sits on that dividing line — mutate, or rebind.
« Copie d'abord » est la parade à l'aliasing — encore faut-il savoir ce qu'on duplique. list(x), x[:], dict(x) clonent le contenant mais partagent encore les éléments : la copie de surface. copy.deepcopy descend récursivement et isole tout, à un prix — et souvent pour rien. Entre les deux, la juste dose se choisit, elle ne se subit pas. 'Copy first' is the counter to aliasing — provided you know what you're duplicating. list(x), x[:], dict(x) clone the container but still share the elements: the shallow copy. copy.deepcopy descends recursively and isolates everything, at a price — often for nothing. Between the two, the right dose is chosen, not suffered.
sous = grille[:] a tout d'une copie franche : le crochet vide dit « tout, en neuf ». C'en est une — d'un seul niveau. Sur une liste de listes, les lignes restent partagées, et un sous[0].append() rejaillit sur l'original. Pire, [[0]*3]*3 fabrique trois noms sur une même ligne. Et l'affectation de tranche a[:] = X ne copie rien : elle mute l'objet en place, sous tous ses alias. sub = grid[:] looks every bit an honest copy: the empty bracket says 'all of it, fresh'. It is one — of a single level. On a list of lists, the rows stay shared, and a sub[0].append() splashes back onto the original. Worse, [[0]*3]*3 makes three names for one row. And slice assignment a[:] = X copies nothing: it mutates the object in place, under all its aliases.
« If it walks like a duck and quacks like a duck, it's a duck. » En Python, une fonction ne devrait pas demander de quelle classe tu hérites, mais si tu réponds à l'appel qu'elle va te passer. L'interface appartient au consommateur : implémente les bonnes méthodes, et tu satisfais le protocole sans rien déclarer. Le prix : aucun compilateur ne vérifie ce contrat — il te reste l'AttributeError, au runtime, loin de la faute. 'If it walks like a duck and quacks like a duck, it's a duck.' In Python, a function shouldn't ask which class you inherit from, but whether you answer the call it's about to make. The interface belongs to the consumer: implement the right methods, and you satisfy the protocol without declaring anything. The price: no compiler checks that contract — you're left with AttributeError, at runtime, far from the fault.
Le vrai pouvoir de Python n'est pas dans les méthodes que tu nommes, mais dans celles qu'il appelle pour toi. len(x) exécute x.__len__(), x[k] exécute x.__getitem__(k), a + b exécute a.__add__(b). Implémente les bonnes méthodes spéciales, et ton objet se fond dans la syntaxe du langage — pas par héritage, par le modèle de données. Le prix : chaque dunder t'engage à un contrat que le langage suppose tenu. Python's real power isn't in the methods you name, but in the ones it calls for you. len(x) runs x.__len__(), x[k] runs x.__getitem__(k), a + b runs a.__add__(b). Implement the right special methods, and your object melts into the language's syntax — not by inheritance, by the data model. The price: each dunder commits you to a contract the language assumes is upheld.
Un dict cherche une clé en deux temps : hash() choisit le compartiment, == confirme l'identité dedans. Casse l'accord entre les deux — deux objets égaux avec des hash différents — et le dict cherche au bon endroit un objet qui n'y est jamais rangé. C'est pour ça qu'une liste est unhashable : elle mute, et un objet qui mute après avoir été haché se perd dans son propre compartiment. A dict finds a key in two steps: hash() picks the bucket, == confirms identity inside it. Break the deal between the two — two equal objects with different hashes — and the dict searches the right spot for an object that was never filed there. That's why a list is unhashable: it mutates, and an object that mutates after being hashed loses itself in its own bucket.
for x in y n'est pas une primitive du langage : c'est iter(y) puis next() en boucle jusqu'à StopIteration. Cette désucre cache une distinction que la plupart des devs ne font jamais — un itérable et un itérateur ne sont pas la même chose. L'un se reparcourt à volonté ; l'autre s'épuise, une fois, et ne le dit à personne. for x in y isn't a language primitive: it's iter(y) then next() in a loop until StopIteration. That desugaring hides a distinction most devs never make — an iterable and an iterator aren't the same thing. One can be walked again and again; the other gets exhausted, once, and tells no one.
Une fonction qui contient yield ne s'exécute pas quand tu l'appelles : elle retourne un générateur, un itérateur qui n'a encore rien fait. Chaque next() reprend l'exécution exactement où elle s'était arrêtée — variables locales, position de boucle, tout gelé, sans une seule ligne d'attribut d'instance. Le prix : le corps de la fonction ne s'exécute jamais avant qu'on la tire, pas même la validation que tu croyais immédiate. A function containing yield doesn't run when you call it: it returns a generator, an iterator that has done nothing yet. Each next() resumes execution exactly where it stopped — local variables, loop position, everything frozen, without a single instance-attribute line. The price: the function body never runs before you pull it, not even the validation you assumed was immediate.
map(), zip(), filter() ne sont pas des listes déguisées : ce sont des vues paresseuses, des itérateurs au sens strict du №10, épuisés dès qu'on les a parcourus une fois. Réutiliser l'un d'eux ne lève aucune erreur — il rend juste « vide », en silence, comme s'il n'avait jamais rien contenu. Le seul antidote : list(), et son prix en mémoire. map(), zip(), filter() aren't disguised lists: they're lazy views, iterators in №10's strict sense, exhausted the moment you've walked them once. Reusing one raises no error — it just goes 'empty', silently, as if it never held anything. The one antidote: list(), and its price in memory.
Lire un nom dans une fonction cherche dans quatre portées, dans un ordre fixe : Local, Enclosing, Global, Built-in. Mais Python décide si un nom est local avant même d'exécuter la moindre ligne — en scannant tout le corps de la fonction à la recherche d'une assignation. Le résultat : lire une variable avant de l'assigner, plus bas dans la même fonction, ne va pas chercher la variable globale du même nom. Ça lève une UnboundLocalError. Reading a name inside a function searches four scopes, in a fixed order: Local, Enclosing, Global, Built-in. But Python decides whether a name is local before running a single line — by scanning the whole function body for an assignment. The result: reading a variable before assigning it, further down in the same function, doesn't fall back to the global variable of the same name. It raises UnboundLocalError.
[lambda: i for i in range(3)] fabrique trois fonctions différentes, mais elles impriment toutes 2 quand on les appelle — jamais 0, 1, 2. Une closure ne capture pas un instantané de la valeur au moment où elle est définie : elle capture la variable elle-même, lue au moment où on l'appelle. La boucle a terminé sa course avant le premier appel ; les trois fonctions lisent la même case, à son état final. [lambda: i for i in range(3)] builds three different functions, but they all print 2 when called — never 0, 1, 2. A closure doesn't capture a snapshot of the value at definition time: it captures the variable itself, read at call time. The loop finishes its run before the first call; all three functions read the same slot, at its final state.
Lire un nom englobant ne demande aucune déclaration — LEGB s'en charge (numéro 13). Le réassigner en demande une : sans mot-clé, Python déclare un nom local tout neuf, et l'original englobant reste intouché. global et nonlocal lèvent cette ambiguïté, chacun pointant vers un scope différent — mais l'un crée silencieusement, l'autre exige que la case existe déjà. Reading an enclosing name needs no declaration — LEGB handles it (issue 13). Reassigning it needs one: without a keyword, Python declares a brand-new local name, and the original enclosing one stays untouched. global and nonlocal lift that ambiguity, each pointing to a different scope — but one creates silently, the other demands the slot already exists.
obj.x n'est jamais une simple lecture de case mémoire : c'est une recherche, dans un ordre fixe. Python regarde d'abord le __dict__ de l'instance ; s'il n'y trouve rien, il remonte le __dict__ de la classe, puis celui de chaque classe parente, dans l'ordre du MRO. Une liste posée comme attribut de classe est donc partagée par toutes les instances — jusqu'à ce que l'une d'elles écrive enfin la sienne. obj.x is never a plain memory-slot read: it's a search, in a fixed order. Python first looks at the instance's __dict__; finding nothing there, it climbs the class's __dict__, then each parent class's, in MRO order. A list set as a class attribute is therefore shared by every instance — until one of them finally writes its own.
Le numéro précédent s'arrêtait à un __dict__ qui répond. Mais certains objets trouvés dans un __dict__ de classe ne sont pas des valeurs à retourner : ce sont des descripteurs, qui définissent __get__ et __set__, et transforment un accès en apparence anodin — self.x — en appel de méthode. property en est l'exemple le plus courant ; et un descripteur de donnée passe même avant le __dict__ de l'instance. The previous issue stopped once a __dict__ answered. But some objects found in a class __dict__ aren't values to return: they're descriptors, defining __get__ and __set__, turning an access that looks utterly plain — self.x — into a method call. property is the most common example; and a data descriptor even outranks the instance's own __dict__.
Chaque instance porte, par défaut, son propre __dict__ — une table de hachage complète, allouée dynamiquement, prête à accueillir n'importe quel attribut à n'importe quel moment. Ce confort a un prix en mémoire, multiplié par chaque objet créé. __slots__ le supprime : les attributs deviennent des descripteurs à taille fixe. La mémoire économisée se paie en flexibilité perdue. Every instance carries, by default, its own __dict__ — a full hash table, dynamically allocated, ready to welcome any attribute at any moment. That comfort has a memory price, multiplied by every object created. __slots__ removes it: attributes become fixed-size descriptors. The memory saved is paid for in lost flexibility.
f.close() écrit en dernière ligne d'une fonction a l'air de garantir la fermeture du fichier. Il ne le fait pas : une exception levée plus haut saute directement par-dessus cette ligne, sans jamais l'exécuter. with appelle __exit__ quoi qu'il arrive à l'intérieur du bloc — return anticipé, exception, ou sortie normale — le même contrat que finally, mais porté par l'objet lui-même. f.close() written on a function's last line looks like it guarantees the file gets closed. It doesn't: an exception raised further up jumps straight over that line, never running it. with calls __exit__ no matter what happens inside the block — early return, exception, or normal exit — the same contract as finally, but carried by the object itself.
Vérifier qu'une clé existe avant de la lire semble plus prudent que d'essayer et de rattraper l'échec. Mais entre la vérification et la lecture s'ouvre un intervalle — le temps qu'un autre thread, un autre processus, ou simplement le disque, change l'état vérifié. Python préfère EAFP : tenter l'opération directement, attraper l'exception si elle survient — sans jamais laisser cet intervalle s'ouvrir. Checking that a key exists before reading it looks more careful than trying and catching the failure. But between the check and the read opens a gap — time enough for another thread, another process, or simply the disk, to change the state just checked. Python prefers EAFP: attempting the operation directly, catching the exception if it happens — never leaving that gap open.
with garantissait le nettoyage ; l'except précis évitait d'avaler un vrai bug. Reste une dernière subtilité : que se passe-t-il quand finally et return se disputent la dernière parole dans une même fonction ? Et quand un générateur, jamais épuisé jusqu'au bout, tient encore un fichier ouvert — qui referme sa ressource, et comment ? with guaranteed cleanup; a precise except avoided swallowing a real bug. One last subtlety remains: what happens when finally and return both compete for the last word in the same function? And when a generator, never fully exhausted, is still holding a file open — who closes its resource, and how?
Lancer deux threads pour accélérer un calcul semble une évidence — sur un CPU à plusieurs cœurs, ils devraient se partager le travail. Pourtant, en Python, un seul thread exécute du bytecode à la fois. Le GIL sérialise le calcul ; seule l'attente — un fichier, un réseau, un timer — libère vraiment un cœur pour un autre thread. Spinning up two threads to speed up a computation looks obvious — on a multi-core CPU, they should split the work. Yet in Python, only one thread executes bytecode at a time. The GIL serializes computation; only waiting — a file, a network call, a timer — actually frees a core for another thread.
Le GIL libère un thread pendant l'attente réseau — mais un seul thread suffit, du moment qu'il sait rendre la main de lui-même. asyncio construit exactement ça : une boucle d'événements qui exécute des milliers de tâches en attente sans jamais ouvrir un seul thread supplémentaire. Le prix : un seul appel bloquant, oublié dans une coroutine, gèle toute la boucle d'un coup. The GIL frees a thread during a network wait — but a single thread is enough, as long as it knows how to hand back control on its own. asyncio builds exactly that: an event loop running thousands of waiting tasks without ever opening a single extra thread. The price: one blocking call, forgotten inside a coroutine, freezes the entire loop at once.
Une coroutine est un objet en attente, pas un calcul en cours — le n°23 l'a montré. La conséquence la plus silencieuse de cette mécanique : coro() sans await ne s'exécute jamais, sans la moindre erreur bruyante. Et une tâche créée avec create_task, sans référence gardée quelque part, peut disparaître avant même d'avoir eu la chance de tourner. A coroutine is a waiting object, not a computation in progress — issue №23 showed that. The most silent consequence of that mechanic: coro() without await never runs, with no loud error at all. And a task created with create_task, with no reference kept anywhere, can vanish before it ever gets the chance to run.
Le n°24 a mentionné une référence faible, sans expliquer ce qui distingue une référence forte. La réponse ouvre le dernier volume de la série : chaque objet Python porte un compteur de références, et meurt à l'instant précis où ce compteur atteint zéro — sauf quand deux objets se référencent mutuellement, et qu'aucun compteur ne redescend jamais à zéro tout seul. Issue №24 mentioned a weak reference, without explaining what sets a strong reference apart. The answer opens the series' final volume: every Python object carries a reference count, and dies at the exact instant that count hits zero — except when two objects reference each other, and no count ever drops to zero on its own.
Le Vol. I a posé is contre == sans jamais expliquer pourquoi a is b « marche » parfois. La réponse tenait dans ce numéro : CPython pré-alloue un cache de petits entiers et interne certaines chaînes au démarrage. a is b n'est vrai par coïncidence que pour les valeurs qui tombent dans ce cache — et se brise, sans prévenir, dès qu'on en sort. Vol. I posed is versus == without ever explaining why a is b sometimes 'works'. The answer was waiting in this issue: CPython preallocates a small-int cache and interns certain strings at startup. a is b is only true by coincidence for values that fall inside that cache — and breaks, without warning, the moment you step outside it.
Le Vol. VI a montré comment obj.attr cherche — instance, classe, MRO. Ce dernier numéro chiffre ce que cette recherche coûte réellement, dans la boucle la plus chaude d'un programme, et montre où la mettre en cache. La boucle de la série se referme ici : le dynamisme n'a jamais été gratuit, seulement invisible. Vol. VI showed how obj.attr searches — instance, class, MRO. This final issue puts a number on what that search actually costs, in a program's hottest loop, and shows where to cache it. The series' loop closes here: dynamism was never free, only invisible.
Le piège, en apprenant Python, c'est de prendre la simplicité pour de la facilité — d'écrire b = a, def f(x=[]), une liste qu'on tranche, sans voir la tension que chaque facilité te laisse porter au runtime. Chaque numéro dissèque une seule tension : noms et références, mutabilité, protocoles implicites, itération paresseuse, coût du dynamisme. Le bug qu'on tient à distance d'abord, le prix ensuite, et le contre-exemple — quand ne pas l'employer. The trap, learning Python, is mistaking simple for easy — writing b = a, def f(x=[]), a list you slice, without seeing the tension each convenience leaves on your shoulders at runtime. Each issue dissects a single tension: names and references, mutability, implicit protocols, lazy iteration, the cost of dynamism. The bug you keep at bay first, the price next, and the counter-example — when not to use it.
La règle tacite : un nom n'est pas une boîte qui contient une valeur ; c'est une étiquette collée sur un objet. Python ne te refuse presque rien — il exécute, et te fait confiance pour tenir l'invariant qu'aucun compilateur ne vérifie. Comprends ce que le dynamisme te coûte, et tu cesses de lutter contre le runtime : tu l'écoutes. The unspoken rule: a variable is not a box that holds a value; it is a label stuck onto an object. Python refuses you almost nothing — it runs, and trusts you to hold the invariant no compiler will check. Understand what dynamism costs you, and you stop fighting the runtime: you listen to it.