Vue d'ensemble

Python 3.11, publié le 24 octobre 2022, est la version la plus rapide de l'histoire de CPython. Grâce à l'interpréteur adaptatif spécialisant (PEP 659), de nombreux programmes courants s'exécutent jusqu'à 60 % plus vite que sous Python 3.10, sans aucune modification de code.

Au-delà des performances, cette version introduit les groupes d'exceptions (ExceptionGroup et except*), le module tomllib pour la lecture native de fichiers TOML, des messages d'erreur encore plus précis pointant vers l'expression exacte, et asyncio.TaskGroup pour la concurrence structurée.

Nouveautés majeures

Interpréteur adaptatif spécialisant (PEP 659)

Le cœur de l'accélération de Python 3.11 repose sur un interpréteur adaptatif qui spécialise le bytecode à la volée. Au lieu d'exécuter des instructions génériques, l'interpréteur détecte les types récurrents et remplace les opcodes par des versions optimisées. Par exemple, une addition entre entiers est remplacée par une instruction machine rapide qui évite la résolution de type à chaque appel.

Les benchmarks officiels montrent une amélioration moyenne de 25 % sur la suite pyperformance, avec des pics à 60 % sur certains cas d'usage. Les applications web, les scripts de traitement de données et les outils CLI en profitent directement.

python
import time

def calculer_collatz(n):
    """Calcule la longueur de la suite de Collatz pour n."""
    etapes = 0
    while n != 1:
        if n % 2 == 0:
            n //= 2
        else:
            n = 3 * n + 1
        etapes += 1
    return etapes

# Ce type de boucle numérique bénéficie fortement
# de la spécialisation adaptative
debut = time.perf_counter()
resultats = {i: calculer_collatz(i) for i in range(1, 500_000)}
duree = time.perf_counter() - debut

plus_long = max(resultats, key=resultats.get)
print(f"Plus longue suite : n={plus_long}, {resultats[plus_long]} étapes")
print(f"Durée : {duree:.2f}s")
# Sur Python 3.10 : ~1.80s
# Sur Python 3.11 : ~1.10s (environ 40 % plus rapide)

Groupes d'exceptions (PEP 654)

Les groupes d'exceptions (ExceptionGroup) et la syntaxe except* permettent de lever et de capturer plusieurs exceptions simultanément. C'est essentiel pour la programmation parallèle où plusieurs tâches peuvent échouer en même temps, typiquement avec asyncio.TaskGroup.

La syntaxe except* filtre les exceptions par type au sein d'un groupe, et plusieurs clauses except* peuvent capturer différents types issus du même groupe.

python
# Création d'un groupe d'exceptions
def valider_formulaire(donnees):
    """Valide plusieurs champs et regroupe toutes les erreurs."""
    erreurs = []

    if not donnees.get('nom'):
        erreurs.append(ValueError("Le nom est obligatoire"))
    if not donnees.get('email') or '@' not in donnees.get('email', ''):
        erreurs.append(ValueError("Email invalide"))
    if donnees.get('age') is not None and donnees['age'] < 0:
        erreurs.append(TypeError("L'âge doit être positif"))

    if erreurs:
        raise ExceptionGroup("Erreurs de validation", erreurs)

# Capture sélective avec except*
try:
    valider_formulaire({'nom': '', 'email': 'invalide', 'age': -5})
except* ValueError as groupe_val:
    print(f"{len(groupe_val.exceptions)} erreur(s) de valeur :")
    for e in groupe_val.exceptions:
        print(f"  - {e}")
except* TypeError as groupe_type:
    print(f"{len(groupe_type.exceptions)} erreur(s) de type :")
    for e in groupe_type.exceptions:
        print(f"  - {e}")

# Résultat :
# 2 erreur(s) de valeur :
#   - Le nom est obligatoire
#   - Email invalide
# 1 erreur(s) de type :
#   - L'âge doit être positif

tomllib : lecture native de TOML (PEP 680)

Le module tomllib permet de lire des fichiers TOML directement depuis la bibliothèque standard, sans dépendance externe. TOML est le format de configuration utilisé par pyproject.toml, devenu le standard pour la configuration des projets Python. Le module ne supporte que la lecture ; l'écriture nécessite toujours une bibliothèque tierce comme tomli-w.

python
import tomllib
from pathlib import Path

# Lecture d'un fichier pyproject.toml
contenu_toml = Path('pyproject.toml').read_bytes()
config = tomllib.loads(contenu_toml.decode())

nom_projet = config['project']['name']
version = config['project']['version']
print(f"Projet : {nom_projet} v{version}")

# Lecture directe depuis un fichier ouvert en mode binaire
with open('pyproject.toml', 'rb') as f:
    config = tomllib.load(f)

# Accès aux sections imbriquées
dependances = config.get('project', {}).get('dependencies', [])
for dep in dependances:
    print(f"  Dépendance : {dep}")

# Lecture d'une chaîne TOML
extrait = """
[serveur]
hote = "0.0.0.0"
port = 8000
debug = false

[base_de_donnees]
url = "postgresql://localhost/mabase"
pool_size = 5
"""

params = tomllib.loads(extrait)
print(f"Serveur : {params['serveur']['hote']}:{params['serveur']['port']}")
print(f"Base : {params['base_de_donnees']['url']}")
# Serveur : 0.0.0.0:8000
# Base : postgresql://localhost/mabase

Localisation fine des erreurs dans les tracebacks

Python 3.11 améliore considérablement les tracebacks en pointant vers l'expression exacte qui a provoqué l'erreur, et non plus seulement la ligne. Des indicateurs visuels (tildes et caréts) soulignent précisément l'opération fautive. C'est particulièrement utile lorsqu'une ligne contient plusieurs appels ou opérations.

python
# Exemple avec des appels chaînés
# resultat = donnees['utilisateurs'][0]['adresse']['ville'].upper()
#
# Traceback (most recent call last):
#   File "app.py", line 12, in traiter
#     resultat = donnees['utilisateurs'][0]['adresse']['ville'].upper()
#                ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
# KeyError: 'adresse'

# Exemple avec des opérations arithmétiques
# total = prix_ht * quantite + frais_livraison / nb_articles
#
# Traceback (most recent call last):
#   File "calcul.py", line 8, in calculer_total
#     total = prix_ht * quantite + frais_livraison / nb_articles
#                                  ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
# ZeroDivisionError: division by zero

# Exemple avec un appel de méthode
# rapport = generateur.creer().sauvegarder().envoyer()
#
# Traceback (most recent call last):
#   File "rapport.py", line 5, in generer_rapport
#     rapport = generateur.creer().sauvegarder().envoyer()
#               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
# AttributeError: 'NoneType' object has no attribute 'envoyer'

asyncio.TaskGroup : concurrence structurée

asyncio.TaskGroup offre une approche de concurrence structurée pour les tâches asynchrones. Contrairement à asyncio.gather(), un TaskGroup garantit que toutes les tâches sont terminées ou annulées à la sortie du bloc async with, même en cas d'erreur. Les exceptions sont regroupées dans un ExceptionGroup.

python
import asyncio

async def recuperer_donnees(source, delai):
    """Simule la récupération de données depuis une source."""
    await asyncio.sleep(delai)
    return f"Données de {source}"

async def traiter_en_parallele():
    """Récupère des données de plusieurs sources en parallèle."""
    resultats = {}

    async with asyncio.TaskGroup() as groupe:
        tache_api = groupe.create_task(
            recuperer_donnees('API externe', 0.5)
        )
        tache_bdd = groupe.create_task(
            recuperer_donnees('Base de données', 0.3)
        )
        tache_cache = groupe.create_task(
            recuperer_donnees('Cache Redis', 0.1)
        )

    # Toutes les tâches sont terminées ici
    resultats['api'] = tache_api.result()
    resultats['bdd'] = tache_bdd.result()
    resultats['cache'] = tache_cache.result()

    for cle, valeur in resultats.items():
        print(f"{cle} : {valeur}")

asyncio.run(traiter_en_parallele())
# cache : Données de Cache Redis
# bdd : Données de Base de données
# api : Données de API externe

Améliorations mineures

  • Le module re supporte les groupes atomiques ((?>...)) et les quantificateurs possessifs (*+, ++).
  • Les StrEnum, IntEnum et ReprEnum sont enrichis dans le module enum.
  • Le module hashlib supporte les algorithmes BLAKE2 au niveau fichier.
  • Le module math gagne math.cbrt() pour la racine cubique et math.exp2().
  • Le démarrage de l'interpréteur est 10 à 15 % plus rapide.

Dépréciations et suppressions

Les changements notables dans cette catégorie :

  • Le module asynchat et asyncore sont supprimés après avoir été dépréciés depuis Python 3.6.
  • Les champs st_ctime sur Windows changent de sémantique (date de création au lieu de date de modification des métadonnées).
  • Plusieurs fonctions dépréciées du module unittest sont supprimées.
  • Le module aifc est déprécié et prévu pour suppression dans Python 3.13.

Sources