Les nouveautés de Python 3.11

Découvrez toutes les nouveautés de la version 3.11 de Python

Publié le par Thibault Houdon (mis à jour le )
paceTemps de lecture estimé : 32 minutes

Comme tous les ans désormais au mois d'octobre, une nouvelle version de Python est de sorti. Ce cycle de développement annuel avait en effet démarré avec la version 3.9 (voir la PEP 602 à ce sujet).

Cette version 3.11 de Python était très attendu de la communauté notamment car elle apporte de nombreuses améliorations de performances. Sujet important pour Python qui a la réputation d'être un langage lent.

Des améliorations sur la performance

C'est donc la nouveauté la moins visible mais qui peut avoir le plus d'impact selon votre domaine. CPython (l'implémentation principale de Python) est désormais en moyenne 25% plus rapide avec Python 3.11 qu'avec Python 3.10.

La rapidité a été améliorée jusqu'à 60% dans certains cas de figure ce qui est loin d'être négligeable !

Personnellement, la "lenteur" relative de Python ne m'a jamais posé de problèmes en près de 10 ans d'utilisation du langage. Les problèmes de lenteurs étant bien plus souvent causés par le développeur que le langage lui-même (je prends souvent l'exemple d'un développeur qui trouvait son site développé avec Django trop lent et dont toutes les images sur le site étaient des PNG de 10mb et avec des requêtes SQL très mal optimisées).

Mais du code peu optimisé qui va 60% plus vite, c'est toujours bon à prendre 😁

Plus rapide au démarrage

Python est aussi et surtout plus rapide au démarrage. Cela peut sembler anecdotique mais quand vous faites rouler des scripts des centaines de fois à la seconde ça peut avoir un gros impact.

Je n'ai personnellement vu que très peu de différence entre la version 3.10 et 3.11 de Python (la documentation parle d'une amélioration de l'ordre de 10%).

Mais si on fait la comparaison entre une version un peu plus ancienne de Python comme la 3.6 et la 3.11, là on commence à clairement voir l'amélioration au fil des versions.

Petit exemple avec 250 exécution d'un script Python qui contient une simple instruction pass :

Avec Python 3.6

#!/bin/bash
SECONDS=0
for i in {1..250}
do
   /usr/bin/time python3.6 -c "pass"
done
duration=$SECONDS
echo "$(($duration % 60)) seconds elapsed."


$ speed-test36.sh
0.03 real         0.02 user         0.00 sys
0.03 real         0.02 user         0.00 sys
0.03 real         0.02 user         0.00 sys
...
0.03 real         0.02 user         0.00 sys
0.02 real         0.02 user         0.00 sys
0.03 real         0.02 user         0.00 sys
8 seconds elapsed.

Avec Python 3.11

#!/bin/bash
SECONDS=0
for i in {1..250}
do
   /usr/bin/time python3.11 -c "pass"
done
duration=$SECONDS
echo "$(($duration % 60)) seconds elapsed."


$ speed-test311.sh
0.02 real         0.01 user         0.00 sys
0.01 real         0.01 user         0.00 sys
0.02 real         0.01 user         0.00 sys
...
0.02 real         0.01 user         0.00 sys
0.02 real         0.01 user         0.00 sys
0.02 real         0.01 user         0.00 sys
5 seconds elapsed.

On passe de 8 à 5 secondes sur 250 exécutions, ce qui est presque 2x plus rapide !

Imaginez sur des scripts qui tournent des centaines de fois toute la journée, le gain de temps peut vite devenir conséquent.

Exception notes

Il est désormais possible d'ajouter des notes aux exceptions avec la méthode add_note :

import requests


def exception_notes():
    try:
        r = requests.get('http://www.google.comx')
    except requests.exceptions.RequestException as e:
        e.add_note("Couldn't fetch Google...")
        raise


exception_notes()

Groupes d'exceptions

Python 3.11 apporte la possibilité de grouper des exceptions grâce à la classe ExceptionGroup et à la nouvelle notation except*.

Petit exemple simple pour montrer la syntaxe :

try:
    raise ExceptionGroup("Exception Group for multiple errors", (
        ValueError("This is a value error"),
        TypeError("This is a type error"),
        KeyError("This is a Key error"),
        AttributeError('This is an Attribute Error'),
        AttributeError('This is another Attribute Error')
    ))

except* AttributeError as err:
    raise err
except* (ValueError, TypeError) as err:
    raise err
except* KeyError as err:
    raise err

Et un exemple un peu plus complet pour montrer un cas d'usage avec également la nouvelle méthode add_note :

import requests


def test_links(urls: list) -> None:
    exceptions = []
    for url in urls:
        try:
            requests.get(url)
        except Exception as e:
            e.add_note(url)
            exceptions.append(e)

    if exceptions:
        raise ExceptionGroup("Couldn't fetch some URLs", exceptions)


def write_to_file(exceptions, file_name):
    with open(f"log/{file_name}.txt", "w") as f:
        for exception in exceptions:
            f.write(f"{exception.__notes__[0]}\n")


try:
    test_links(
            urls=[
                "ht://www.google.com",
                "http://www.google.comx",
                "http://www.google.coms",
                "http://www.google.coma",
                "www.google.com",
            ]
    )
except* requests.exceptions.InvalidSchema as e:
    write_to_file(e.exceptions, "invalid_schema")
except* requests.exceptions.MissingSchema as e:
    write_to_file(e.exceptions, "missing_schema")
except* requests.exceptions.ConnectionError as e:
    write_to_file(e.exceptions, "connection_error")
else:
    print("All links are valid")

Typing de self

Du côté des annotations de type, il est désormais possible d'indiquer qu'une méthode retourne une instance de la classe avec le mot-clé Self disponible dans le module typing :

from typing import Self


class CustomPath:
    def __init__(self, path: str):
        self.path = path

    # La méthode concat retourne une instance de la classe CustomPath
    def concat(self, other: str) -> Self:
        return CustomPath(f'{self.path}/{other}')

    def __str__(self):
        return self.path

Messages d'erreurs plus précis

Les messages d'erreurs sont désormais plus précis en indiquant spécifiquement où se situe une erreur dans le traceback.

Prenez les trois exemples ci-dessous :

def example1():
    d = {"uno": [1, [1, 2, 3], 3]}
    print(d["uno"][5][2])


def example2():
    a, b, c, d, e, f = 1, 2, 0, 4, 5, 6
    print(a / b / c / d / e / f)


def example3():
    a = None
    b = ""
    print(a.capitalize() + b.capitalize())


example1()
example2()
example3()

Pour l'exemple #2, impossible avec Python <3.10 de savoir où se situe le problème dans l'opération mathématique.

Avec Python 3.10

Traceback (most recent call last):
  File "/Users/thibh/python-311-new-features/errors_handling/errors_handling.py", line 18, in <module>
    example2()
  File "/Users/thibh/python-311-new-features/errors_handling/errors_handling.py", line 8, in example2
    print(a / b / c / d / e / f)
ZeroDivisionError: float division by zero

Avec Python 3.11

Traceback (most recent call last):
  File "/Users/thibh/python-311-new-features/errors_handling/errors_handling.py", line 18, in <module>
    example2()
  File "/Users/thibh/python-311-new-features/errors_handling/errors_handling.py", line 8, in example2
    print(a / b / c / d / e / f)
          ~~~~~~^~~
ZeroDivisionError: float division by zero

TOML Support

Python utilise le format TOML depuis de nombreuses années comme fichiers de configurations dans de nombreux cas de figure mais il n'était jusqu'à présent pas possible de lire ces fichiers nativement.

C'est désormais possible avec l'ajout de la librairie tomllib qui permet de lire des fichiers de configuration .toml !

À noter que pour l'instant, seule la lecture est possible, cette librairie ne permet pas (encore ?) de créer des fichiers .toml.

# config.toml
title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00

[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 }
import tomllib

with open("setup.toml", "rb") as f:
    data = tomllib.load(f)

print(data)
print(data['owner']['name'])
print(data['database']['ports'])

Python uses TOML, or Tom's Obvious Minimal Language, as a configuration format (as in pyproject.toml), but doesn't expose the ability to read TOML-format files as a standard library module. Python 3.11 adds tomllib to address that problem. Note that tomllib doesn't create or write TOML files; for that you need a third-party module like Tomli-W or TOML Kit.

AsyncIO Task Groups

L'ajout de la classe TaskGroup permet de créer des groupes de tâches asynchrones. Auparavant, il fallait passer par asyncio.gather() pour effectuer cette opération.

import asyncio
import math


async def t1():
    print(int("hello"))
    await asyncio.sleep(2)


async def t2():
    print(math.sqrt(-10))
    await asyncio.sleep(1)


async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(t1())
            tg.create_task(t2())
    except* ValueError as e:
        print(e.exceptions)


if __name__ == '__main__':
    asyncio.run(main())

Vous noterez également dans le code ci-dessus la possibilité d'utiliser les groupes d'exceptions pour récupérer les erreurs potentielles des tâches asynchrones.

C'est d'ailleurs l'ajout des TaskGroup qui a nécessité l'ajout des groupes d'exceptions. Comme quoi, une nouveauté bénéficie bien souvent à une autre 😉

Améliorations de la librairie standard

Le module math

Nouvelles fonctions ajoutées au module math :

>>> import math
>>> math.cbrt(27)
3.0000000000000004
>>> math.exp2(6)
64.0

Récupérer uniquement les dossiers avec pathlib

La méthode glob de la classe Path du module pathlib permet désormais d'indiquer directement si l'on souhaite récupérer uniquement les dossiers à l'intérieur d'un dossier.

Il suffit pour cela d'ajouter un slash à la fin du chemin :

from pathlib import Path

p = Path("/Users/thibh/python-311-new-features/standard_lib/paths_tests")
print("Fichiers et dossiers")
dirs = p.glob("*")
for d in dirs:
    print(d)

print("Dossiers seulement")
dirs = p.glob("*/")
for d in dirs:
    print(d)
Fichiers et dossiers
/Users/thibh/python-311-new-features/standard_lib/paths_tests/document.pdf
/Users/thibh/python-311-new-features/standard_lib/paths_tests/test1
/Users/thibh/python-311-new-features/standard_lib/paths_tests/test3
/Users/thibh/python-311-new-features/standard_lib/paths_tests/test2
/Users/thibh/python-311-new-features/standard_lib/paths_tests/image1.png
Dossiers seulement
/Users/thibh/python-311-new-features/standard_lib/paths_tests/test1
/Users/thibh/python-311-new-features/standard_lib/paths_tests/test3
/Users/thibh/python-311-new-features/standard_lib/paths_tests/test2

Les StrEnum

Il est désormais possible d'utiliser la fonction auto pour créer des énumérations à partir de chaîne de caractères automatiquement grâce à la classe StrEnum :

from enum import StrEnum, auto


class Color(StrEnum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

Si on fait un print de la valeur de BLUE, une chaîne de caractères en minuscule est automatiquement générée :

>>> print(Color.BLUE.value)
"blue"

Dépréciations

La PEP 594 annonce la dépréciation à venir de nombreux modules de la librairie standard :

  • aifc
  • chunk
  • msilib
  • pipes
  • Utilisez subprocess à la place
  • telnetlib
  • audioop
  • crypt
  • nis
  • sndhdr
  • uu
  • cgi
  • imghdr
  • filetype, puremagic, python-magic
  • nntplib
  • spwd
  • xdrlib
  • cgitb
  • mailcap
  • ossaudiodev
  • sunau

Ces modules seront définitivement supprimés de Python dans la version 3.13, à l'exception de asynchat, asyncore et smtpd qui seront dépréciés dès la version 3.12.