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écution

Après

text = "café"          # str : toujours du texte Unicode
data = text.encode()   # bytes : toujours des données binaires explicites

Rationalisation 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=5

Les 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 count

Python 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) # True

Enfin, 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 = 2

Après

from enum import Enum

class Status(Enum):
    PENDING = 1
    RUNNING = 2

Arrê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):
    pass

Après

from pathlib import Path
config_path = Path(__file__).parent.parent / 'config.json'
if not config_path.exists():
    pass

Arrê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)  # 4

Traquez 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."""
    pass

Après

from typing import Dict
def process_user(user_data: Dict[str, int]) -> None:
    pass

Arrê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_000

Les 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
    pass

Après

from pathlib import Path
path = Path('/tmp/file.txt')
with open(path) as f:  # Just works now
    pass

Python 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: int

De 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() # False

Arrê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) # works

Après

def func(a, b, /):
    # a and b CANNOT be passed as keywords.
    pass
func(a=2, b=3) # raises an error

Dé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._data

Aprè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]) # 24

De 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 | overrides

Arrê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)) # 60

Python 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 ValueError

Identifiez 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 standard

Python 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 e

Après

except ValueError as e:
    e.add_note("Vérifiez votre clé d'API dans le fichier .env")
    raise

Typez é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 verbeux

Aprè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 prudent

Aprè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.
>>> exit

Adoptez 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.py

Simplifiez 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ères

Gé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)

Official Release Notes

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_library

Indispensable 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).

Judicael Poumay (Ph.D.)
Index