Les nouveautés de Python 3.12

Dans cet article, nous allons passer en revue toutes les nouveautés de la version 3.12 de Python.

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

Comme tous les ans à l'automne, une nouvelle version de Python voit le jour. Ce cycle de développement annuel a en effet démarré avec la version 3.9 (voir la PEP 602 à ce sujet).

Comme chaque année, nous allons donc passer à travers les nouveautés de cette version 3.12, qui sont moins nombreuses que celles de l'année dernière.

Cet article est également disponible sous forme de vidéo sur notre chaîne YouTube.

Formalisation syntaxique des f-strings

La PEP 701 lève certaines restrictions sur l'utilisation des f-strings.

Les composants d'expression à l'intérieur des f-strings peuvent désormais être n'importe quelle expression Python valide, y compris des chaînes utilisant le même guillemet que le f-string conteneur, des expressions sur plusieurs lignes, des commentaires, des antislashs, et des séquences d'échappement Unicode. Voyons cela en détail :

Utilisation des mêmes guillemets

Auparavant, il fallait jongler entre les guillemets simples et les guillemets doubles ou utiliser un antislash pour pouvoir utiliser les mêmes guillemets à l'intérieur d'un f-string.

C'est désormais chose du passé car vous pouvez utiliser les mêmes guillemets sans que cela ne cause une erreur de syntaxe :

fruits = ['Pommes', 'Poires', 'Bananes']
f"Voici votre liste de courses : {", ".join(fruits)}"

Il est également possible à présent de faire suivre un f-string sur plusieurs lignes et utiliser des commentaires sans avoir besoin d'utiliser de backslash :

f"Voici votre liste de courses : {", ".join([
    'Pommes',  # 3 pommes
    'Poires',  # 2 poires
    'Bananes'  # 5 bananes
])}"

Et pour finir, les backslash rendent possible l'utilisation de caractères d'échappement ou de séquences unicodes :

>>> songs = ['Take me back to Eden', 'Alkaline', 'Ascensionism']
>>> print(f"This is the playlist: {"\n".join(songs)}")
This is the playlist: Take me back to Eden
Alkaline
Ascensionism

>>> print(f"This is the playlist: {"\N{BLACK HEART SUIT}".join(songs)}")
This is the playlist: Take me back to EdenAlkalineAscensionism

Petit bonus de cette nouvelle fonctionnalité : les messages d'erreurs concernant les f-string incluent désormais la ligne complète qui pose problème ainsi qu'une indication précise de l'endroit qui cause l'erreur :

my_string = f"{x z y}" + f"{1 + 1}"
  File "<stdin>", line 1
    my_string = f"{x z y}" + f"{1 + 1}"
                   ^^^
SyntaxError: invalid syntax. Perhaps you forgot a comma?

Messages d'erreurs plus précis

Chaque nouvelle version de Python semble désormais ajouter son lot de nouveaux messages d'erreurs plus précis qu'auparavant.

Par exemple si vous utilisez un module de la librairie standard qui n'a pas été importé, l'interpréteur Python vous l'indiquera explicitement :

>>> sys.version_info
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'sys' is not defined. Did you forget to import 'sys'?

Également, si vous oubliez d'utiliser self devant un attribut qui existe dans une classe, Python vous indiquera que vous l'avez probablement oublié :

class A:
   def __init__(self):
       self.blech = 1

   def foo(self):
       somethin = blech

>>> A().foo()
  File "<stdin>", line 1
    somethin = blech
               ^^^^^
NameError: name 'blech' is not defined. Did you mean: 'self.blech'?

Dernier ajout qui peut être pratique pour certains noms dont on oublie la syntaxe exacte, Python pourra vous suggérer des éléments présents dans le module en cas d'erreur d'import :

>>> from collections import chainmap
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'chainmap' from 'collections'. Did you mean: 'ChainMap'?

Annotations de types améliorées pour les **kwargs

Accrochez-vous, pour bien comprendre l'intérêt de cette nouveauté, il faut remonter un peu en arrière.

En Python, lorsqu'on utilise **kwargs dans la définition d'une fonction, cela signifie que la fonction peut accepter un nombre indéterminé d'arguments sous forme de mots-clés (keywords).

Cependant, jusqu'à présent, il était difficile d'indiquer précisément quel type devrait avoir chaque argument-mot-clé.

Les motivations

  • Actuellement, quand on annote **kwargs avec un type T, cela signifie que tous les arguments mots-clés doivent être de ce type. Par exemple, si vous écrivez une fonction comme celle-ci:
  def foo(**kwargs: str) -> None: ...

Cela signifie que tous les arguments fournis à foo doivent être des chaînes de caractères.

Justification

  • PEP 589 a introduit TypedDict, qui permet de spécifier un dictionnaire dont les clés sont des chaînes et les valeurs peuvent être de différents types.

  • Comme **kwargs est essentiellement un dictionnaire, il serait logique d'utiliser TypedDict pour annoter précisément le type de chaque mot-clé. Cependant, utiliser directement TypedDict avec **kwargs peut prêter à confusion.

Exemple

Prenons l'exemple du TypedDict nommé Movie:

class Movie(TypedDict):
    name: str
    year: int

Si on utilisait Movie directement pour annoter **kwargs:

def foo(**kwargs: Movie) -> None: ...

Cela signifierait que chaque argument fourni à foo devrait être lui-même un dictionnaire avec les clés name et year.

En somme, on pourrait penser qu'on peut / doit appeler foo ainsi :

foo(arg1={"name": "Blade Runner", "year": 1982}, arg2={"name": "Harry Potter", "year": 2011})

Pour éviter cette confusion, une nouvelle approche est proposée: utiliser Unpack.

L'utilisation d'Unpack permet de préciser que les arguments fournis directement à la fonction doivent correspondre aux clés du TypedDict.

Exemple:

def foo(**kwargs: Unpack[Movie]) -> None: ...

Ici, on s'attend à ce que foo soit appelée avec deux mots-clés, name et year, plutôt qu'avec un seul mot-clé qui serait un dictionnaire :

foo(name="Blade Runner", year=1982)

Le décorateur override

Un nouveau décorateur typing.override() a été ajouté au module typing.

Il indique aux "type checker" que la méthode est destinée à remplacer une méthode dans une classe.

Cela permet aux vérificateurs de types de détecter des erreurs où une méthode censée remplacer quelque chose dans une classe de base ne le fait pas réellement.

from typing import override

class Base:
  def get_color(self) -> str:
    return "blue"

class GoodChild(Base):
  @override  # Ici, pas d'erreur, la classe surcharge bien la méthode get_color de Base.
  def get_color(self) -> str:
    return "yellow"

class BadChild(Base):
  @override  # Erreur : on a fait une faute sur le nom de la méthode
  def get_colour(self) -> str:
    return "red"

On indique ici avec @override que la méthode get_colour de BadChild devrait surcharger la méthode de la classe parente (Base).

À cause de la faute de syntaxe (colour au lieu de color), nous ne surchargeons pas la méthode comme souhaitée, un type checker nous indiquer donc ici qu'il y a un problème.

Un GIL par interpréteur

Le GIL (Global Interpreter Lock) est un verrou que l'interpréteur Python utilise pour garantir qu'un seul thread s'exécute dans l'interpréteur à la fois.

C'est une des raisons pour laquelle les programmes Python traditionnels n'utilisent pas entièrement les capacités des processeurs multicœurs pour l'exécution multithread.

La PEP 684 introduit une nouveauté majeure : au lieu d'avoir un seul GIL pour tout l'interpréteur, on peut maintenant avoir un GIL unique pour chaque sous-interpréteur.

Cela signifie qu'en utilisant plusieurs sous-interpréteurs, un programme Python pourrait théoriquement utiliser pleinement plusieurs cœurs CPU.

Cette fonctionnalité est actuellement disponible uniquement via l'API C, bien qu'une API Python soit prévue pour la version 3.13.

Des compréhensions plus rapides

Les compréhensions (listes, dictionnaires et ensembles) sont des expressions couramment utilisées en Python pour générer des collections de manière concise.

Auparavant, chaque fois qu'une compréhension était exécutée, Python créait en coulisse une fonction anonyme (les fonction lambda) pour la réaliser.

Avec cette nouvelle proposition (PEP 709), cette étape est optimisée : les compréhensions sont maintenant "intégrées" (ou inlinées), ce qui signifie qu'elles sont exécutées directement sans créer de fonction temporaire.

Cela améliore les performances des compréhensions, pouvant les rendre jusqu'à deux fois plus rapides.