Python a parcouru un long chemin depuis sa première sortie officielle en 1991. Aujourd’hui, en 2026, Python 3 est devenu un outil omniprésent et l’un des langages de programmation les plus utilisés. Au fil des ans, de nombreuses fonctionnalités que nous considérons aujourd’hui comme acquises ont été introduites, ainsi que bien d’autres dont vous ignorez peut-être l’existence.
Dans ce parcours historique complet de l’évolution de Python 3, nous passerons en revue chaque version de la 3.0 jusqu’à la très attendue Python 3.15 de 2026, en mettant en avant les fonctionnalités majeures et en découvrant de nombreux changements méconnus.
Python 3.0 — Table rase pour le futur
Notes de version officielles
À la fin des années 2000, Python 2 connaissait un immense succès, mais certains choix de conception initiaux n’avaient plus de sens. Arrivé le 3 décembre 2008, Python 3 a courageusement brisé la compatibilité ascendante pour débarrasser Python de ses bizarreries historiques et établir un nouveau standard pour le langage.
Corriger les plus grandes curiosités du langage
Croyez-le ou non, à l’époque de Python 2, print était une instruction, pas une fonction. Cela rendait impossible son utilisation dans des lambdas ou son passage en argument. En en faisant une fonction, Python 3 a rendu l’opération la plus courante du langage cohérente avec tout le reste.
Ensuite, il y avait le problème des chaînes de caractères. Dans Python 2, il existait une séparation confuse entre str (qui n’était que des octets) et un type unicode distinct. L’encodage était une partie de roulette russe.
Une chaîne pouvait être en UTF-8, ou en Latin-1. Vous ne le découvriez que lorsque votre programme plantait, et le débogage n’était pas toujours une mince affaire.
Réalisant que ce n’était pas très pratique, Python 3 a rendu le texte strictement str (Unicode) et les données binaires strictement bytes.
Avant
data = "café"
# est-ce de l'utf-8 ? de l'ISO-2022-JP ? Seul Dieu le sait avant l'exécutionAprès
text = "café" # str : toujours du texte Unicode
data = text.encode() # bytes : toujours des données binaires explicitesRationalisation des séquences
Il fut un temps où range(1000000) créait littéralement une liste d’un million d’éléments.
Il fallait utiliser xrange() pour être efficace, car xrange produisait les éléments au fur et à mesure de leur consommation.
Dans Python 3, range() agit désormais comme xrange(), rendant ce dernier obsolète et Python plus économe en mémoire.
Le déballage (unpacking) de séquences est également devenu plus facile avec l’opérateur étoile.
Récupérer le premier élément et le reste d’une liste nécessitait auparavant un découpage manuel.
Python 3.0 a introduit l’opérateur * dans les affectations, permettant de déballer les séquences naturellement.
Avant
seq = [1, 2, 3, 4, 5]
first = seq[0]
rest = seq[1:-1]
last = seq[-1]Après
first, *rest, last = [1, 2, 3, 4, 5]
# first=1, rest=[2, 3, 4], last=5Les compréhensions de dict et de set simplifiées
Les compréhensions de liste existaient déjà dans Python 2, mais si vous vouliez construire un ensemble (set) ou un dictionnaire en une seule expression, vous deviez passer une compréhension de liste au constructeur. Python 3.0 a doté les sets et les dicts de leur propre syntaxe de compréhension native.
Avant
squares = dict([(x, x**2) for x in range(5)])
unique = set([x for x in data if x > 0])Après
squares = {x: x**2 for x in range(5)}
unique = {x for x in data if x > 0}Le chaînage d’exceptions pour mieux comprendre les erreurs
Lorsqu’une exception survenait à l’intérieur d’un bloc except, la trace d’appels (traceback) d’origine était silencieusement perdue. Python 3.0 a introduit raise ... from ... pour lier explicitement les erreurs. Ainsi, lors du débogage, vous voyez toute la chaîne de causalité au lieu de deviner ce qui a été occulté.
Avant
try:
do_database_thing()
except DBError as e:
raise AppError("App crashed")
# Le traceback original de DBError est perdu à jamais.Après
try:
do_database_thing()
except DBError as e:
raise AppError("App crashed") from e
# Traceback complet préservé.Accéder aux scopes parents avec le mot-clé nonlocal
Les fermetures (closures) dans Python 2 pouvaient lire les variables d’un scope englobant, mais ne pouvaient pas les modifier. L’astuce courante consistait à envelopper la valeur dans un conteneur mutable comme une liste. nonlocal a rendu cela propre.
Avant
def outer():
count = [0] # Astuce mutable pour modifier depuis le scope interne
def inner():
count[0] += 1
return count[0]Après
def outer():
count = 0
def inner():
nonlocal count
count += 1
return countPython 3.1 — Maturité du nouveau standard
Notes de version officielles
Sortie le 27 juin 2009, cette mise à jour a prouvé que Python 3 était prêt pour l’ingénierie sérieuse en introduisant des structures de données hautement pratiques et des améliorations de la gestion de contexte.
Imposer l’ordre explicitement avec OrderedDict
Autrefois, les dictionnaires Python ne se souciaient pas de l’ordre. Les afficher pouvait vous donner {'b': 2, 'a': 1} ou {'a': 1, 'b': 2} de manière aléatoire.
Python 3.1 a lancé OrderedDict, permettant de garantir que l’ordre est respecté chaque fois que nécessaire.
Dans CPython 3.6, l’ordre d’insertion a été préservé comme un détail d’implémentation ; c’est devenu une garantie officielle du langage dans Python 3.7.
Un objet Counter intégré
Compter la fréquence d’apparition de chaque élément dans une liste est l’une des tâches de données les plus courantes.
Avant Counter, vous deviez écrire une boucle de comptage manuelle à chaque fois.
Avec Counter, non seulement c’est intégré à Python, mais cela inclut également des méthodes utiles :
from collections import Counter
counts = Counter(['apple', 'apple', 'pear'])
# Counter({'apple': 2, 'pear': 1})
counts.most_common(1) # [('apple', 2)]Un code plus plat avec les gestionnaires de contexte multiples
Si vous deviez ouvrir deux fichiers à la fois, vous deviez imbriquer des instructions with, créant une pyramide d’indentation sans fin. Python 3.1 a autorisé plusieurs gestionnaires de contexte sur une seule ligne. Un changement simple et très attendu.
Avant
with open('source.txt') as src:
with open('dest.txt', 'w') as dst:
dst.write(src.read())Après
with open('source.txt') as src, open('dest.txt', 'w') as dst:
dst.write(src.read())Python 3.2 — Équiper la bibliothèque standard
Notes de version officielles
Lancé le 20 février 2011, Python 3.2 a armé les développeurs de modules prêts pour la production pour la création de CLI, la mise en cache avancée et la concurrence fluide.
De meilleures interfaces en ligne de commande avec argparse
Si vous avez construit une CLI Python, vous avez probablement utilisé argparse. Il a été ajouté à la bibliothèque standard dans Python 3.2. Alors qu’optparse gérait déjà l’analyse d’options traditionnelle, argparse a été ajouté pour supporter des modèles de CLI plus complexes tels que les arguments positionnels, les sous-commandes, les options requises et la validation intégrée.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("name")
parser.add_argument("--shout", action="store_true")Concurrence simplifiée avec les futures
Le multi-threading a également été simplifié avec Python 3.2 grâce à la sortie de concurrent.futures. Une bibliothèque intégrée bien pratique pour bon nombre de vos besoins en concurrence.
Avant
import threading
threads = []
for i in range(4):
t = threading.Thread(target=work, args=(i,))
threads.append(t)
t.start()Comme vous pouvez le voir, la concurrence était autrefois verbeuse et donc plus sujette aux erreurs. concurrent.futures a introduit une API de plus haut niveau qui traite les « tâches » comme des choses qui retourneront une valeur dans le « futur », vous protégeant de la gestion des threads à bas niveau.
Après
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
results = list(executor.map(work, [1, 2, 3, 4]))Cache intégré avec lru_cache
Mettre en cache le résultat d’une fonction coûteuse nécessitait auparavant d’écrire votre propre wrapper de dictionnaire. lru_cache a transformé cela en un simple décorateur. C’est peut-être l’un des décorateurs les plus puissants de la bibliothèque standard. En ajoutant une seule ligne, vous obtenez un cache LRU (Least Recently Used) avec éviction automatique, sécurité des threads (thread-safety) et même des statistiques sur les succès de cache via fetch_data.cache_info().
from functools import lru_cache
@lru_cache(maxsize=32)
def fetch_data(url):
return http_get(url)Arrêter d’exécuter des tests inutiles avec les sauts conditionnels
Les tests sont devenus plus intelligents avec des décorateurs pour sauter des tests et marquer les échecs attendus. Auparavant, vous effectuiez un return précoce à l’intérieur d’un test et l’exécuteur le marquait comme « Réussi » alors qu’il n’avait jamais réellement tourné.
Avant
def test_windows_registry(self):
if not sys.platform.startswith("win"):
return # L'exécuteur dit "Passé". Trompeur !Après
@unittest.skipUnless(sys.platform.startswith("win"), "Nécessite Windows")
def test_windows_registry(self):
...Python 3.3 — Une version mineure mais indispensable
Notes de version officielles
Arrivée sur la scène le 29 septembre 2012, cette version a ajouté de nombreux outils intégrés indispensables et a ouvert la voie à l’async moderne avec la délégation de générateur.
Encore plus d’outils intégrés
Les tests unitaires ne sont pas la partie préférée de tout le monde dans le développement logiciel, donc tout outil qui les facilite est apprécié.
La bibliothèque mock était déjà massivement populaire en tant que package tiers.
Python 3.3 l’a standardisée au sein de la bibliothèque standard, donnant à chaque projet un accès instantané aux objets simulés sans dépendance supplémentaire.
from unittest.mock import Mock
service = Mock()
service.hello.return_value = "Hello, world!"
print(service.hello()) # Hello, world!Les environnements virtuels ont changé le développement logiciel à jamais. Fini le temps du « mais ça marche sur ma machine » (à quelques exceptions près). Pendant longtemps, les gens ont utilisé des tiers comme virtualenv pour y parvenir. Dans Python 3.3, venv a été ajouté à la bibliothèque standard ; l’isolation des dépendances fait désormais partie intégrante du workflow du langage.
Enfin, Python a ajouté un outil pour valider et manipuler les adresses IP. Le faire soi-même avec des regex était notoirement risqué, une regex naïve comme \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} acceptant volontiers 999.999.999.999. Le module ipaddress a rendu l’analyse réseau sécurisée et orientée objet.
import ipaddress
ip = ipaddress.ip_address("192.168.1.10")
print(ip.is_private) # TrueEnfin, lorsqu’une extension C plantait, Python se contentait jusqu’ici d’afficher Segmentation fault (core dumped) et de s’arrêter. Pas de traceback, aucun indice sur l’endroit où cela s’était produit. Désormais, grâce à faulthandler, Python affiche un traceback au moment exact du crash, faisant du débogage un cauchemar en moins.
Déléguer le travail de vos générateurs avec yield from
Ce fut une victoire massive pour la lisibilité. yield from permet à un générateur de déléguer son travail à un autre, ce qui est devenu un modèle vital pour les premières implémentations async.
Avant
def countdown(n):
for i in range(n, 0, -1):
yield i
def blastoff():
for i in countdown(3):
yield i
yield "🚀"Après
def countdown(n):
yield from range(n, 0, -1)
def blastoff():
yield from countdown(3) # Délégation élégante
yield "🚀"Python 3.4 — Poser l’architecture Async
Notes de version officielles
Sortie le 16 mars 2014, cette mise à jour charnière a formellement introduit la boucle d’événements asyncio, faisant passer la programmation asynchrone d’un ajout de niche à une philosophie centrale du langage.
Entrer dans l’ère de la boucle d’événements avec asyncio
Avant asyncio, le Python asynchrone signifiait souvent s’appuyer sur des frameworks tiers comme Twisted ou Gevent.
Ces outils étaient puissants, mais le modèle de programmation pouvait sembler fragmenté et lourd en callbacks.
Avec Python 3.4, asyncio a introduit une boucle d’événements standard dans la bibliothèque standard.
Ce fut un changement majeur : la programmation asynchrone n’était plus seulement un modèle d’écosystème de niche, mais quelque chose que Python lui-même supportait officiellement.
À l’époque, cependant, Python n’avait pas encore la syntaxe moderne async / await (elle viendra dans la prochaine version). Le code asyncio précoce utilisait des décorateurs et des coroutines basées sur des générateurs avec yield from.
Après
import asyncio
@asyncio.coroutine
def greet():
yield from asyncio.sleep(1)
print("Hello after one second")Remplacer vos nombres magiques par des enums
Les enums ont apporté la sécurité de type et la lisibilité aux constantes. Au lieu de faire circuler des entiers ou des chaînes de caractères magiques, vous utilisez un ensemble de valeurs nommé et structuré qui rend le débogage bien plus agréable.
Avant
STATUS_PENDING = 1
STATUS_RUNNING = 2Après
from enum import Enum
class Status(Enum):
PENDING = 1
RUNNING = 2Arrêter de manipuler des chaînes et adopter pathlib
os.path traitait les chemins de fichiers comme de simples chaînes de caractères. On les joignait avec os.path.join, on vérifiait leur existence avec os.path.exists, et le code paraissait toujours maladroit. pathlib traite les chemins comme des objets intelligents dotés de méthodes, et utilise l’opérateur / pour les joindre.
Avant
import os
config_path = os.path.join(os.path.dirname(__file__), '..', 'config.json')
if not os.path.exists(config_path):
passAprès
from pathlib import Path
config_path = Path(__file__).parent.parent / 'config.json'
if not config_path.exists():
passArrêtez de réimplémenter les maths et utilisez le module statistics
Les opérations statistiques de base comme la moyenne ou la médiane nécessitaient autrefois soit d’importer numpy, soit de se taper les calculs à la main. Python 3.4 nous a offert un module standard léger pour l’essentiel.
Avant
data = [1, 2, 4, 4, 5]
mean = sum(data) / len(data)
# Median? Sort the list, find the midpoint, handle even/odd lengths...Après
import statistics
statistics.mean(data) # 3.2
statistics.median(data) # 4Traquez les fuites de mémoire avec tracemalloc
Les fuites de mémoire en Python sont rares mais brutales. Quand votre processus gonfle jusqu’à 4 Go, vous n’aviez auparavant aucune idée de quelle ligne de code était responsable. tracemalloc fait le lien entre les blocs de mémoire et la ligne exacte de Python qui les a créés.
import tracemalloc
tracemalloc.start()
# ... run code ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
# Shows exactly which line allocated the most memory.Python 3.5 — L’aube de l’async natif et des indices de type
Official Release Notes
Dévoilée le 13 septembre 2015, cette version a modernisé la syntaxe de Python en introduisant les mots-clés dédiés async/await et en posant les bases essentielles du typage statique.
Écrivez du code asynchrone qui ressemble enfin à du Python
C’est à ce moment-là que l’asynchrone en Python a commencé à ressembler à du code normal. Les nouveaux mots-clés ont rendu la logique asynchrone aussi lisible que du code synchrone standard.
Avant
import asyncio
@asyncio.coroutine
def fetch():
yield from asyncio.sleep(1)Après
async def fetch():
await asyncio.sleep(1)Nettoyez votre algèbre linéaire avec l’opérateur @
Pour la communauté scientifique, ce fut une victoire majeure. Cela a transformé des appels de fonctions imbriqués en équations d’algèbre linéaire enfin lisibles.
Avant
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
result = np.dot(A, B)Après
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = A @ B Note : Les listes Python standard n’implémentent pas l’opérateur @. Il nécessite des types comme les tableaux NumPy qui définissent la méthode __matmul__.
Fusionnez vos collections avec un soupçon d’étoiles
Que signifie réellement être « Pythonique » ? Cette mise à jour syntaxique en est sans doute l’un des meilleurs exemples. C’est le moyen le plus concis de fusionner des listes et des dictionnaires sans muter les objets originaux.
Avant
a = [1, 2]
b = [3, 4]
combined = a + b + [5]
d1 = {"x": 1}
d2 = {"y": 2}
merged = d1.copy()
merged.update(d2)Après
combined = [*a, *b, 5]
merged = {**d1, **d2}Mettez de l’ordre dans le chaos avec les indices de type
La PEP 484 a changé Python à jamais. Bien que Python reste typé dynamiquement à l’exécution, le nouveau module typing vous permet d’ajouter des indices de type statiques que les IDE et des outils comme mypy utilisent pour débusquer les bugs avant même que le code ne soit lancé.
Avant
def process_user(user_data):
"""user_data must be a dict of string to integers."""
passAprès
from typing import Dict
def process_user(user_data: Dict[str, int]) -> None:
passArrêtez de lutter avec Popen et utilisez subprocess.run
Popen est incroyablement puissant, mais généralement disproportionné pour lancer une simple commande. subprocess.run() a introduit une API unique, propre et bloquante pour exécuter des commandes externes et capturer leur sortie.
Avant
import subprocess
p = subprocess.Popen(["ls", "-l"], stdout=subprocess.PIPE)
out, err = p.communicate()
if p.returncode != 0:
raise Exception()Après
import subprocess
result = subprocess.run(["ls", "-l"], capture_output=True, check=True)
print(result.stdout)Cessez de vous battre contre les erreurs de virgule flottante avec math.isclose
L’arithmétique en virgule flottante est notoirement imprécise (0.1 + 0.2 donne 0.30000000000000004). Les développeurs ne cessaient de réinventer des comparaisons basées sur la tolérance avec des epsilons arbitraires. isclose gère proprement les tolérances absolues et relatives.
Avant
if abs(0.1 + 0.2 - 0.3) < 1e-9:
print("Close enough")Après
import math
if math.isclose(0.1 + 0.2, 0.3):
print("Mathematically close")Python 3.6 — Python devient plus beau et un peu plus sûr
Official Release Notes
Sortie le 23 décembre 2016, cette mise à jour adorée des fans a fondamentalement changé notre façon d’écrire du code avec l’introduction des élégantes f-strings et des annotations de type pour les variables.
Un code plus lisible
Suite à la mise à jour de la PEP 484 de la version 3.5, Python 3.6 permet désormais d’annoter le type des variables. Cela ne change pas l’exécution du code, mais cela change notre manière de l’écrire.
from typing import List
prices: List[float] = []Vous pouvez aussi rendre vos grands nombres plus lisibles grâce aux underscores (tirets bas). Une petite fonctionnalité avec un impact énorme sur la lisibilité. Il est désormais impossible de confondre un million avec dix millions au premier coup d’œil.
big_number = 1_000_000_000Les sacrées f-strings
Franchement, n’étions-nous pas tous agacés par la syntaxe %s pour le formatage de chaînes ? C’était moche, déroutant et fastidieux.
print("Hello, %s. Age: %d" % (name, age))
print("Hello, {}. Age: {}".format(name, age))Avec Python 3.6 sont arrivées les f-strings. Une solution plus rapide et nettement plus lisible. Je n’ai jamais adopté une fonctionnalité aussi vite. Elles permettent de placer des expressions directement dans la chaîne, ce qui en fait le choix par défaut pour presque tous les développeurs.
Après
print(f"Hello, {name}. Age: {age}")Secrets : une alternative plus sûre à random
Les développeurs utilisaient random pour des mots de passe et des jetons sans réaliser que ce n’était pas sécurisé d’un point de vue cryptographique. secrets offre une alternative rapide et sûre qui s’appuie directement sur le générateur aléatoire cryptographique du système d’exploitation.
import secrets
token = secrets.token_urlsafe(32)En pratique, secrets est désormais le standard pour générer des valeurs imprévisibles comme les jetons de réinitialisation de mot de passe, les jetons CSRF et les identifiants de session. Il ne remplace pas le hachage de mot de passe ou une architecture de sécurité globale, mais pour le hasard sécurisé pur, il est bien plus approprié que random.
Streamez vos données de manière asynchrone avec aisance
Python 3.5 nous a apporté async/await, mais on ne pouvait pas utiliser yield à l’intérieur d’un async def ni écrire des compréhensions asynchrones. Python 3.6 a étendu la puissance des générateurs au monde de l’asynchrone, permettant ainsi de streamer des données de manière asynchrone.
async def fetch_all():
for url in urls:
yield await fetch(url) # Streams one at a time
data = [item async for item in fetch_all()]Laissez les objets Path s’épanouir dans la bibliothèque standard
Quand pathlib a été introduit en 3.4, les fonctions de la bibliothèque standard comme open() n’acceptaient pas réellement les objets Path. Il fallait les convertir en chaînes. Python 3.6 a créé le protocole os.PathLike, de sorte que pathlib fonctionne enfin partout nativement.
Avant
from pathlib import Path
path = Path('/tmp/file.txt')
with open(str(path)) as f: # Had to convert to string manually
passAprès
from pathlib import Path
path = Path('/tmp/file.txt')
with open(path) as f: # Just works now
passPython 3.7 — Simplification des données et débogage
Official Release Notes
Arrivée le 27 juin 2018, cette version a sabré le code répétitif (boilerplate) grâce aux dataclasses et a standardisé l’expérience de débogage dans tout l’écosystème.
Quelques simplifications bienvenues
Les dataclasses ont terrassé le « monstre du boilerplate ».
Python génère désormais automatiquement les méthodes __init__, __repr__ et __eq__ pour vous, en se basant sur les indices de type.
from dataclasses import dataclass
@dataclass
class User:
name: str
age: intDe plus, les dictionnaires garantissent désormais l’ordre d’insertion, rendant OrderedDict obsolète.
C’est un nouvel exemple de Python rendant une solution de fortune inutile, comme il l’avait fait avec xrange et maintenant avec OrderedDict.
Cela garantit que le langage évolue sans rester prisonnier de ses décisions passées.
Enfin, une nouvelle vérification ultra-rapide au niveau C permet de s’assurer que tous les caractères d’une chaîne sont dans la plage ASCII (0-127). Crucial pour la journalisation sécurisée et les contraintes de base de données où l’on veut garantir l’absence de caractères non-ASCII.
"café".isascii() # FalseArrêtez de taper pdb.set_trace et utilisez breakpoint
Ah, le débogueur… je ne l’ai jamais utilisé et je ne le ferai probablement jamais. Pourtant, avec Python 3.7, il est plus simple à utiliser que jamais.
breakpoint() nous offre un moyen standard et unique d’entrer dans le débogueur. Cela permet aussi de changer de débogueur (par exemple pour pudb ou ipdb) via des variables d’environnement sans modifier votre code.
Avant
import pdb; pdb.set_trace()Après
breakpoint()Soyons honnêtes, on va tous continuer à utiliser print() pour déboguer.
Mesurez vos performances à la nanoseconde près
Pour le profiling haute performance et les benchmarks précis, les flottants de Python causaient des pertes de précision à cause des erreurs d’arrondi sur les machines rapides. Le suffixe _ns a été ajouté à plusieurs fonctions temporelles pour offrir une précision sous forme d’entiers.
import time
# Returns the time as a precise integer representing nanoseconds.
start = time.time_ns()Protégez votre état à travers les frontières asynchrones
Le stockage local au thread (thread-local storage) s’effondre en Python asynchrone car de nombreuses coroutines peuvent s’exécuter sur le même thread. Cela signifie que les données de la « requête actuelle » ou de l’« utilisateur actuel » ne peuvent plus résider en toute sécurité dans des variables locales au thread.
contextvars résout ce problème en donnant à chaque tâche asynchrone son propre contexte logique. En pratique, cela permet aux frameworks web et aux systèmes de log de conserver des données liées à la requête — comme un ID utilisateur, un ID de requête ou un ID de trace — sans avoir à les passer en argument à chaque appel de fonction.
import contextvars
# Context-local storage correctly handles async task boundaries.
user_id = contextvars.ContextVar('user_id')
user_id.set(123)Python 3.8 — Une version controversée
Official Release Notes
Lancée le 14 octobre 2019, Python 3.8 a suscité débats et innovations en introduisant l’opérateur Walrus (le morse) pour les assignations en ligne et en offrant aux auteurs de bibliothèques un contrôle plus strict sur les paramètres.
Assignez et vérifiez d’un seul trait avec le walrus
Bien que ce soit très spécifique et controversé, il permet d’assigner une variable et de vérifier sa valeur sur la même ligne. Personnellement, je n’aime pas trop, je trouve que ça nuit à la lisibilité et je n’ai pas encore vu grand monde l’utiliser, mais ça existe.
Dans la communauté Python, le débat autour du walrus (PEP 572) a été si intense que Guido van Rossum a quitté la direction de Python, déclarant qu’il ne voulait plus se battre aussi durement pour une PEP.
Avant
match = re.search(pattern, text)
if match:
data = match.group(1)Après
if match := re.search(pattern, text): #assigns and checks
data = match.group(1)Protégez votre API contre les arguments nommés
C’est vital pour les auteurs de bibliothèques.
Cela leur permet de changer le nom des paramètres à l’avenir sans casser le code des utilisateurs de leur bibliothèque.
Auparavant, si vous vouliez empêcher les utilisateurs de taper explicitement les paramètres comme func(a=1), vous deviez effectuer une vérification manuelle dans le corps de la fonction.
Désormais, l’utilisation de l’opérateur slash (/) garantit que les utilisateurs doivent passer ces arguments par position.
Personnellement, je pense que la lisibilité du code est primordiale, donc cette fonctionnalité devrait être utilisée avec parcimonie.
Avant
def func(a, b, **kwargs):
pass
func(a=2, b=3) # worksAprès
def func(a, b, /):
# a and b CANNOT be passed as keywords.
pass
func(a=2, b=3) # raises an errorDéboguez plus vite avec les f-strings auto-documentées
Un raccourci bien pratique qui évite de taper deux fois le nom de la variable quand on logue l’état du programme. C’est une fonctionnalité sympa, même si je ne peux m’empêcher de remarquer que cette version semble pour l’instant composée de mises à jour syntaxiques assez bizarres et de niche.
Avant
print(f"user={user} score={score}")Après
print(f"{user=} {score=}") # Affiche 'user=Guido score=99'Mettez vos propriétés en cache et ménagez votre CPU
Un cache basique pour vos propriétés. Désormais, les calculs lourds ne s’exécutent qu’au premier accès, et les suivants sont aussi rapides qu’un simple accès à un attribut.
Avant
class Dataset:
@property
def data(self):
if not hasattr(self, '_data'):
self._data = load_heavy_file() # Prend 5 secondes
return self._dataAprès
from functools import cached_property
class Dataset:
@cached_property
def data(self):
return load_heavy_file() # Évalué une fois, puis mis en cache pour toujours !Structurez vos données avec TypedDict et Literal
Le système de types de Python a gagné en maturité ici. TypedDict nous permet de définir la structure stricte des dictionnaires (comme des réponses JSON), et Literal restreint les valeurs à des chaînes ou des nombres précis.
from typing import TypedDict, Literal
class Config(TypedDict):
id: int
mode: Literal["r", "w"] # Définit 'mode' comme étant exactement "r" ou "w"Diverses autres fonctionnalités ont été ajoutées
On a toujours eu sum(). Il était logique d’ajouter enfin un équivalent natif pour le produit qui gère correctement les calculs et affiche des performances dignes du C :
import math
result = math.prod([1, 2, 3, 4]) # 24De même, on connaissait shlex.split() pour découper des commandes en listes. shlex.join() fait exactement l’inverse, en gérant proprement les guillemets pour les espaces et caractères spéciaux.
import shlex
cmd_str = shlex.join(["ls", "-l", "my dir"]) # 'ls -l "my dir"'Python 3.9 — Polissage des types et des dictionnaires
Notes de version officielles
Sortie le 5 octobre 2020, cette mise à jour a affiné le quotidien des développeurs avec des opérateurs de fusion de dictionnaires intuitifs et des indices de types natifs pour les collections.
Fusionnez vos dicts avec un simple pipe
Une manière plus intuitive et lisible de fusionner des dictionnaires, calquée sur le style des ensembles (sets).
Avant
merged = {**defaults, **overrides}Après
merged = defaults | overridesArrêtez d’importer List et adoptez les génériques natifs
Plus besoin d’importer List, Dict ou Tuple du module typing. Vous pouvez utiliser les types de collections natifs directement comme indices de types.
Avant
from typing import List
def process(items: List[int]): ...Après
def process(items: list[int]): ...Gérez les fuseaux horaires nativement avec zoneinfo
Python dispose enfin d’un moyen intégré de gérer les fuseaux horaires IANA sans bibliothèques tierces, rendant les calculs de dates bien plus fiables dès la sortie de boîte.
from zoneinfo import ZoneInfo
eastern = ZoneInfo("US/Eastern")Nettoyez les extrémités de vos chaînes avec une précision chirurgicale
Retirer des préfixes ou suffixes sans expressions régulières imposait des découpes fastidieuses. Ces méthodes de chaînes offrent un moyen rapide, sûr et intuitif de nettoyer les bords.
url = "https://example.com"
clean = url.removeprefix("https://")Résolvez vos graphes de dépendances avec graphlib
Résoudre des dépendances (comme l’ordre de compilation de paquets) est un problème d’informatique théorique complexe. L’avoir dans la bibliothèque standard évite des heures de débogage sur des algorithmes de graphes foireux.
from graphlib import TopologicalSorter
graph = {"task_B": {"task_A"}, "task_C": {"task_B"}}
ts = TopologicalSorter(graph)
print(tuple(ts.static_order())) # ('task_A', 'task_B', 'task_C')Laissez math gérer vos plus petits communs multiples (PPCM)
Extension de la bibliothèque math pour supporter nativement le PPCM (LCM) sur plusieurs arguments. math.gcd existait, mais pas math.lcm, ce qui nous forçait à coder nos propres fonctions.
import math
print(math.lcm(4, 5, 6)) # 60Python 3.10 — La révolution du Pattern Matching
Notes de version officielles
Lancée le 4 octobre 2021, cette mise à jour syntaxique massive a apporté un air de programmation fonctionnelle à Python avec le tant attendu pattern matching structurel.
Laissez tomber les if imbriqués pour le pattern matching structurel
En passant du C au Python, ce qui m’a le plus manqué, ce sont les instructions switch-case. Ça fait si longtemps que j’avais oublié à quel point je les aimais. Avec Python 3.10, nous avons enfin un équivalent. match/case permet de déconstruire des structures de données complexes de manière déclarative. C’est nettement plus propre que des if imbriqués pour traiter des réponses d’API ou des AST.
Avant
if isinstance(data, dict) and "status" in data:
if data["status"] == 200:
if data["body"] == "Success":
# ... traitement ...
if data["body"] == "Partial":
# ... traitement ...
if data["status"] == 429:
if data["body"] == "Retry":
# ... traitement ...Après
match data:
case {"status": 200, "body": "Success"}:
# ... traitement ...
case {"status": 200, "body": "Partial"}:
# ... traitement ...
case {"status": 429, "body": "Retry"}:
# ... traitement ...
Nettoyez vos indices de types avec le pipe d’union
Simple et Pythonique, cela donne aux indices de types l’allure d’une logique Python standard. C’est plus propre, plus rapide à taper et plus facile à lire.
Avant
from typing import Union
def parse(val: Union[int, str]): ...Après
def parse(val: int | str): ...Comptez vos bits à la vitesse du C avec bit_count
Aussi connu sous le nom de « population count » ou popcount. Le faire via des manipulations de chaînes était incroyablement lent ; c’est maintenant une fonction C native ultra-rapide.
Avant
count = bin(42).count('1') # On crée des chaînes pour faire des maths !Après
count = (42).bit_count() # 3Échouez rapidement sur les itérables de tailles différentes avec zip(strict=True)
Une victoire majeure pour l’intégrité des données. Le paramètre strict=True garantit que vos boucles parallèles plantent bruyamment au lieu d’ignorer silencieusement les données manquantes.
Avant
# Perte de données silencieuse si les listes sont inégales !
list(zip([1, 2, 3], ['A', 'B'])) # [(1, 'A'), (2, 'B')] - le 3 est ignoré en silence !Après
list(zip([1, 2, 3], ['A', 'B'], strict=True)) # Lève une ValueErrorIdentifiez la bibliothèque standard sans deviner
Indispensable pour les linters, formatters et autres outils qui doivent différencier les paquets installés via pip des modules Python intégrés sans s’appuyer sur des listes codées en dur.
import sys
"json" in sys.stdlib_module_names # True car json est une lib standardPython 3.11 — La mise à jour vers la vitesse et la sécurité
Notes de version officielles
Sortie le 24 octobre 2022, cette version n’a pas seulement apporté des gains de performance sans précédent, elle a aussi révolutionné la gestion des erreurs concurrentes avec les groupes d’exceptions.
Gérez une nuée d’erreurs avec les groupes d’exceptions
Avant, si 10 tâches asynchrones échouaient, vous ne voyiez généralement que l’erreur de la première.
Nous avons maintenant except* et ExceptionGroup. Cela permet au code concurrent de rapporter plusieurs échecs simultanément, facilitant grandement le débogage d’applications asynchrones ou multi-threadées complexes.
async def task1():
raise ValueError("ID utilisateur invalide")
async def task2():
raise ValueError("Type de données erroné")
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(task1())
tg.create_task(task2())
except* ValueError as eg: # eg est un ExceptionGroup, qui est itérable
print("ValueError(s) gérée(s)")
for e in eg.exceptions:
print(" -", e)Analysez vos pyproject.toml nativement avec tomllib
TOML est devenu le langage de configuration par défaut pour les outils Python. L’inclusion d’un parseur natif ultra-rapide garantit que l’écosystème Python n’a pas besoin de dépendances externes juste pour s’auto-initialiser.
import tomllib
with open("pyproject.toml", "rb") as f:
config = tomllib.load(f)Orchestrez vos tâches avec TaskGroup
TaskGroup a révolutionné la sécurité de l’asynchrone. Il apporte une concurrence structurée, garantissant que les tâches en arrière-plan sont strictement gérées, attendues, ou proprement arrêtées en cas d’erreur.
Avant
# Avec gather(), si une tâche échoue, les autres continuent de s'exécuter jusqu'à
# la fin ou jusqu'à leur annulation explicite, gaspillant potentiellement des ressources.
results = await asyncio.gather(task1(), task2())Après
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(do_work())
task2 = tg.create_task(do_work())
# TaskGroup garantit que si une tâche échoue, toutes les autres tâches restantes
# du groupe sont automatiquement annulées.Enrichissez vos erreurs avec add_note
Vous pouvez désormais ajouter du contexte utile à une erreur sans modifier le type d’exception original ni perdre la trace de pile (stack trace).
Avant
try:
raise ValueError("Bad")
except ValueError as e:
# Il fallait l'envelopper dans une nouvelle exception pour ajouter des infos
raise ValueError(f"Contexte : {e}") from eAprès
except ValueError as e:
e.add_note("Vérifiez votre clé d'API dans le fichier .env")
raiseTypez élégamment vos API fluentes avec Self
Self signifie « une instance de cette classe ». C’est particulièrement utile pour des méthodes comme copy(), des builders ou des API fluentes qui renvoient self, car les outils de vérification de types comprennent que le type de retour reste lié à la classe réelle.
C’est d’autant plus utile avec l’héritage : si une sous-classe appelle copy(), le résultat est déduit comme étant la sous-classe, et non juste la classe parente. Avant Self, conserver ce comportement nécessitait un pattern TypeVar beaucoup plus verbeux.
Avant
from typing import TypeVar
T = TypeVar("T", bound="MyClass")
class MyClass:
def copy(self: T) -> T: ... # Très verbeuxAprès
from typing import Self
class MyClass:
def copy(self) -> Self: ...Python 3.12 — Libération des génériques et des f-strings
Notes de version officielles
Sortie le 2 octobre 2023, cette mise à jour a transformé l’indiçage de types en une véritable fonctionnalité native du langage et a levé les limitations historiques du formatage des f-strings.
Écrivez des génériques qui ressemblent à du vrai code
Les génériques ressemblent désormais à une fonctionnalité native plutôt qu’à un bricolage importé. C’est plus propre et plus intuitif pour quiconque vient de langages comme Java ou TypeScript.
Avant
from typing import TypeVar
T = TypeVar("T")
def first(l: list[T]) -> T: ...Après
def first[T](l: list[T]) -> T: ...Libérez-vous des restrictions de guillemets dans les f-strings
Le « cauchemar des guillemets » : vous ne pouviez pas utiliser les mêmes guillemets à l’intérieur qu’à l’extérieur. De plus, aucun commentaire n’était autorisé à l’intérieur des accolades. Les f-strings n’ont plus de restrictions arbitraires. Elles sont désormais analysées comme de véritables expressions Python, permettant un code beaucoup plus naturel.
Avant
print(f"Songs: {', '.join(songs)}") # Il fallait être prudentAprès
# Utilisez n'importe quel guillemet, ajoutez des commentaires, écrivez une logique sur plusieurs lignes.
print(f"Songs: {
', '.join(songs) # Les commentaires sont maintenant autorisés !
}")Regroupez vos itérables par lots sans calculs manuels
Le regroupement d’itérables par lots (batching) est extrêmement courant (par exemple, appeler une API avec 50 identifiants à la fois). batched fournit un outil intégré efficace au niveau C qui fonctionne nativement sur n’importe quel itérable, pas seulement les listes.
Avant
# L'ère du découpage manuel.
chunk_size = 3
for i in range(0, len(data), chunk_size):
chunk = data[i:i + chunk_size]Après
from itertools import batched
for chunk in batched(data, 3):
pass # Traitez 3 éléments à la fois proprement.Protégez vos surcharges de méthodes avec @override
Provenant de langages disposant de mots-clés override natifs, ce décorateur garantit que les hiérarchies orientées objet ne se brisent pas silencieusement lorsque vous renommez une méthode parente.
Avant
class Parent:
def process(self): pass
class Child(Parent):
def proces(self): pass # Faute de frappe ! Mais cela échoue silencieusement, et la logique parente s'exécute à la place.Après
from typing import override
class Child(Parent):
@override
def proces(self): pass # Le vérificateur de type vous crie immédiatement dessus !Parcourez vos répertoires de manière orientée objet
Le dernier clou dans le cercueil de os.walk. Le parcours de répertoires entièrement orienté objet est enfin là.
Avant
import os
from pathlib import Path
# os.walk renvoie des chaînes de caractères, vous devez donc les transformer manuellement en objets Path.
for root, dirs, files in os.walk(directory):
path = Path(root) / files[0]Après
from pathlib import Path
# Tout ce qui est renvoyé est nativement un objet Path !
for root, dirs, files in Path(directory).walk():
path = root / files[0]Python 3.13 — Déverrouiller le véritable parallélisme
Official Release Notes
Publié le 7 octobre 2024, ce jalon historique a enfin amorcé la suppression progressive du Global Interpreter Lock (GIL) et a complètement modernisé le shell interactif par défaut.
Profitez d’un shell qui vous apprécie vraiment
Le nouveau shell donne à Python interactif l’allure d’un outil moderne, avec de meilleures invites d’aide et une expérience de développement bien plus fluide.
Avant
# Basique, pas de couleurs, indentation agaçante, exit() requis.
>>> exit()Après
# Coloré, édition multi-ligne, historique intelligent, exit fonctionne tout simplement.
>>> exitAdoptez le futur multi-cœur sans le GIL
Ah, le GIL, quelle histoire… Quand Python a été créé, les processeurs mono-cœur étaient la norme et le multi-threading n’était encore qu’un concept académique. Ainsi, la manière dont Python a été initialement conçu ne permettait pas un véritable traitement parallèle.
Au lieu de cela, Python simulait le multi-threading. Cela fonctionnait pour les tâches liées aux entrées/sorties (I/O bound), mais les véritables tâches liées au processeur (CPU bound) ne pouvaient pas être parallélisées.
À mesure que le langage évoluait, cette limitation est devenue de plus en plus difficile à supprimer, mais aussi de plus en plus difficile à justifier. Après beaucoup de travail et de délibérations, Python 3.13 a ajouté un support expérimental pour une version « free-threaded ».
Bien que le GIL subsiste dans la version standard, la version expérimentale permet d’exécuter des threads en parallèle sur plusieurs cœurs. Vous pouvez désactiver le GIL au moment de l’exécution avec -X gil=0 ou en définissant la variable d’environnement PYTHON_GIL=0.
# Sur une version free-threaded :
python3.13 -X gil=0 script.pySimplifiez vos génériques avec des types par défaut
Vous deviez définir plusieurs surcharges si vous vouliez un type par défaut. Désormais, les « Type Parameter Defaults » simplifient la conception de bibliothèques en permettant aux classes génériques d’avoir un type par défaut raisonnable si aucun n’est fourni.
Après
T = TypeVar("T", default=str)Mettez à jour vos objets immuables avec une API standard
Réconcilie les API fragmentées entre dataclasses, namedtuple et objets personnalisés en une interface standard unique pour la modification d’objets immuables.
Avant
from dataclasses import replace
# Pour les dataclasses, vous utilisiez `replace`. Pour les namedtuples, c'était `_replace`.
new_obj = replace(obj, status="done")Après
import copy
# Une API standard unique pour copier et remplacer des champs.
new_obj = copy.replace(obj, status="done")Python 3.14 — Une mémoire plus intelligente et des chaînes plus sûres
Official Release Notes
Lancée le 7 octobre 2025, cette avancée architecturale introduit les chaînes modèles (template strings) pour gérer en toute sécurité les injections de données brutes et améliore les performances des applications grâce au ramasse-miettes incrémental (Incremental Garbage Collection).
Arrêtez de mettre vos classes entre guillemets et utilisez les annotations différées
Python diffère désormais l’évaluation des indices de type (type hints) par défaut. Cela résout les problèmes de « référence circulaire » et accélère l’importation des modules. Avant
# Vous deviez utiliser des chaînes si une classe se référençait elle-même dans ses propres méthodes.
class Node:
def __init__(self, next: "Node"): ...Après
class Node:
def __init__(self, next: Node): ... # Plus besoin de chaînes de caractèresGérez les données brutes en toute sécurité avec les t-strings
Les f-strings transforment immédiatement tout en chaîne de caractères. Cela peut être dangereux pour le SQL ou l’HTML si ce n’est pas géré avec précaution.
Avant
query = f"SELECT * FROM users WHERE id = {user_id}" Les t-strings (Template strings) renvoient des objets Template qui permettent aux auteurs de bibliothèques (comme SQLAlchemy ou Jinja) de recevoir le modèle brut et les variables séparément. Cela permet aux bibliothèques en aval de traiter les interpolations en toute sécurité pour prévenir les risques d’injection.
Après
query = t"SELECT * FROM users WHERE id = {user_id}"
# query est un objet Template, pas une chaîne de caractères.Compressez à une vitesse digne de Meta avec Zstandard
Python 3.14 a ajouté un nouveau package compression unifié. Bien que les anciens modules comme gzip existent toujours et ne soient pas dépréciés pour au moins cinq ans, le nouveau package offre une API plus cohérente. Plus important encore, il a ajouté le support natif de Zstandard, l’algorithme de compression moderne ultra-rapide créé par Meta.
from compression import zstd
compressed = zstd.compress(b"Hello World" * 100)Plus besoin de dépendre de liaisons tierces pour l’un des formats de compression les plus importants du web.
Nettoyez vos blocs de capture d’exceptions multiples
Si vous vouliez capturer plusieurs exceptions, vous deviez les envelopper dans un tuple. Dans Python 3.14, vous pouvez enfin abandonner les parenthèses si vous n’utilisez pas le mot-clé as.
Avant
try:
connect()
except (TimeoutError, ConnectionRefusedError):
print("Network is down!")After
try:
connect()
except TimeoutError, ConnectionRefusedError:
print("Network is down!")Attendez, on dirait du Python 2 ? Oui ! Python 2 utilisait des virgules pour lier les variables (except Exception, e), ce qui était déroutant. Python 3 a corrigé cela avec as. Maintenant que as est strictement imposé pour la liaison de variables, la virgule revient en toute sécurité à sa fonction légitime : séparer une liste de types.
Lissez vos saccades avec le GC incrémental
Le ramasse-miettes (GC) de Python fonctionnait auparavant en mode « stop-the-world ». Lorsqu’il collectait la mémoire cyclique, toute votre application s’arrêtait. Pour les serveurs web ou les jeux vidéo, cela provoquait des micro-saccades perceptibles. Python 3.14 introduit le GC incrémental, qui divise le processus de collecte en petites étapes, réduisant considérablement les temps de pause et garantissant la fluidité des applications haute performance.
Python 3.15 — L’optimisation ultime de l’efficacité (Pré-version/Brouillon)
Note : Cette section est basée sur les brouillons actuels et les propositions de pré-version. La version finale est prévue pour le 1er octobre 2026, ce n’est donc pas encore de l’histoire ancienne et les détails peuvent changer.
Prévue pour fin 2026, cette version tournée vers l’avenir promet d’accélérer considérablement les temps de démarrage des applications grâce aux importations paresseuses (lazy imports) et de refondre le suivi des performances avec le nouveau profileur Tachyon.
Accélérez votre démarrage avec les imports paresseux
Avant Chaque import en haut du fichier s’exécute immédiatement, ralentissant le démarrage.
import heavy_library Après Le module n’est chargé que lorsque vous l’utilisez réellement.
lazy import heavy_libraryIndispensable pour les outils CLI et les frameworks où la flexibilité est cruciale. Nous n’aurons plus à importer à l’intérieur de conditions. Notez que les imports paresseux explicites ont des restrictions d’utilisation spécifiques pour garantir la compatibilité.
Verrouillez vos dictionnaires avec frozendict
Python 3.15 introduit un nouveau type intégré frozendict. Il fournit un type de mappage standard, hachable et immuable, parfait pour la configuration et comme clés dans d’autres dictionnaires. Auparavant, vous deviez utiliser MappingProxyType ou des bibliothèques tierces.
Après
settings = frozendict({"id": "123"})Aplatissez vos listes en une seule compréhension
Aplatir des listes imbriquées ou combiner plusieurs générateurs a toujours nécessité soit itertools.chain(), soit l’écriture d’une compréhension de liste à double boucle déroutante ([x for sublist in mainlist for x in sublist]). Python 3.15 introduit les opérateurs de déballage * et ** directement à l’intérieur des compréhensions.
Avant
lists = [[1, 2], [3, 4], [5]]
# Le "for x in L for L in lists" (ou l'inverse) ?
flattened = [x for L in lists for x in L]
# Ou en important un outil :
import itertools
flattened = list(itertools.chain.from_iterable(lists))Après
lists = [[1, 2], [3, 4], [5]]
flattened = [*L for L in lists] # [1, 2, 3, 4, 5]Cette seule fonctionnalité épargne aux développeurs la recherche Stack Overflow la plus courante de l’histoire de Python : « comment aplatir une liste de listes ? ». Cela fonctionne même avec les dictionnaires : {**d for d in dicts} !
Profilez votre code de production avec Tachyon
Les profileurs standard de Python (cProfile et profile) utilisent le « traçage déterministe », ce qui signifie qu’ils enregistrent chaque appel de fonction. C’est précis, mais cela ajoute une charge massive à votre code, le ralentissant souvent au point que le profil devient inexact pour le débogage en production. Python 3.15 introduit un package profiling dédié ainsi qu’un nouveau profileur par échantillonnage statistique nommé Tachyon.
Avant
python -m cProfile script.py
# Ralentit considérablement le script, faussant les métriques de performance en temps réel.Après
python -m profiling.sampling run script.py
# Échantillonne la pile d'appels à haute fréquence, fournissant des métriques précises avec une charge extrêmement faible.Gardez vos maths pures avec le module integer
Comme les mathématiques sur les entiers deviennent plus importantes pour la cryptographie et les données à grande échelle, le module math générique (qui se concentre sur les nombres à virgule flottante) avait besoin d’un frère. Python 3.15 introduit math.integer pour les opérations mathématiques sur les entiers purs.
Regarder en arrière pour mieux voir l’avenir
Python a vraiment parcouru un long chemin et ceci n’était qu’une liste non exhaustive des fonctionnalités majeures de Python 3. J’espère que vous avez découvert des fonctionnalités intéressantes que vous ne connaissiez pas, car c’est mon cas !
Si vous utilisez encore une ancienne version, je n’ai qu’un conseil : mettez à jour. L’eau est bonne, le code est plus joli, et ça ne fait que s’améliorer (si l’on ignore le Morse).