Le type hinting de *args et **kwargs en Python pose des problèmes que nous allons voir en détail.
Mettre en place du typage en python est possible depuis la version 3.5 et cela est une bonne pratique pour donner des instructions au développeur.
Il est important de savoir que ce typage n'est pas inclus dans le runtime mais bien là pour assister le développeur à implémenter correctement ses instructions, grâce notamment à MyPy
Voici un exemple de typage simple
Copier le code
1 2 def add(a:int, b:int) -> int: return a + b
Rappel sur les paramètres positionnels et nommés
Dans la signature d'une fonction on peut trouver :
- Des paramètres positionnels, ce sont les premiers et, lors de l'appel, on ne précise pas le nom du paramètre. Ils sont stockés dans un tuple et notés par convention
*args
- Des paramètres nommés, ce sont les derniers et lors de l'appel on précise forcément le nom du paramètre. Ils sont stockés dans un dictionnaire et notés par convention
**kwargs
J'en parle plus longuement sur un article dédié à l'usage de ces paramètres args et kwargs
Comment utiliser ces paramètres
Il suffit de les placer comme suit, vous remarquez qu'en fonction des implémentations, la fonction
add()On appelle cela la variadicité
Copier le code
1 2 3 4 5 6 7 8 9 10 11 12 13 def add(*args, **kwargs): print(args) print(kwargs) >>> add(1, 2) (1, 2) {} >>> add(1, b=2) (1,) {'b': 2} >>> add(a=1, b=2) () {'a': 1, 'b': 2}
Appliquer le type hinting sur *args et **kwargs
En suivant ce principe si on veut mettre du typage sur *args et **kwargs on ferait
Copier le code
1 2 3 4 5 # Mauvais typage sur args et kwargs def add(*args:tuple[int], **kwargs:dict[str, int]) -> int: print(args) print(kwargs) ...
L'analyseur de type (comme MyPy) retournera une erreur en indiquant
Argument 1 to "add" has incompatible type "int"; expected "tuple[int]"
La PEP 484 permet de simplifier le typage pour faire le binding entre
int
tuple[int]
Ce qui donne
Copier le code
1 2 def add(*args: int, **kwargs: int) -> int: ...
On peut ainsi typer de manière plus lisible comme
Copier le code
1 2 def add(*args: int | float, **kwargs: int | float) -> float: ...
Mais cette notation avec le pipe est limitée. Le typage
**kwargs: intTyper précisément **kwargs avec TypedDict et Unpack (PEP 692)
Depuis Python 3.12, la PEP 692 permet de décrire précisément chaque clé de **kwargs grâce à un TypedDict combiné à Unpack.
Copier le code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from typing import NotRequired, TypedDict, Unpack class Options(TypedDict): timeout: int retry: NotRequired[bool] def fetch(url: str, **kwargs: Unpack[Options]) -> None: ... fetch("https://rigaudie.fr", timeout=10) # OK fetch("https://rigaudie.fr", timeout=10, retry=True) # OK fetch("https://rigaudie.fr", timeout="10") # erreur MyPy fetch("https://rigaudie.fr", timeout=10, debug=True) # erreur MyPy
NotRequired
Unpack
typing_extensions
Typer *args avec *tuple et TypeVarTuple (PEP 646)
On rencontre le même problème pour les paramètres positionnels :
*args: int*tuple[...]
Copier le code
1 2 3 4 5 def add(*args: *tuple[int, float]) -> float: ... add(1, 2.3) # OK add(2.3, 1) # erreur MyPy : l'ordre des types compte
Pour aller plus loin,
TypeVarTuple
Copier le code
1 2 3 4 5 6 7 8 9 10 from typing import TypeVarTuple, reveal_type Ts = TypeVarTuple("Ts") def to_tuple(*args: *Ts) -> tuple[*Ts]: return args reveal_type(to_tuple(1, "a", 2.0)) # tuple[int, str, float]
Depuis Python 3.12, la syntaxe se simplifie, plus besoin d'importer
typing
Copier le code
1 2 def to_tuple[*Ts](*args: *Ts) -> tuple[*Ts]: return args
*args: Any ou *args: object : lequel choisir ?
Quand une fonction accepte vraiment n'importe quoi, on est tenté d'écrire
*args: AnyAttention :
Any
MyPy
Copier le code
1 2 3 4 5 from typing import Any def do_something(*args: Any) -> None: for arg in args: print(arg.upper()) # aucune erreur MyPy... mais AttributeError si arg est un int
Préférez
object
Copier le code
1 2 3 4 5 6 def do_something(*args: object) -> None: for arg in args: if isinstance(arg, str): print(arg.upper()) # OK : le type est vérifié (grâce au if du dessus) else: print(arg)
Sans le
isinstance
MyPy
do_something
error: "object" has no attribute "upper"
Réservez
Any
- => accepte tous les types et ne fait pas de vérification (mypy OK)
Any
- => accepte tous les types mais fait des vérifications avant de l'utiliser (mypy KO sans les vérifications, ex:
object
)isinstance
Est-ce que le typage impacte les performances d'une application python ?
Bien sûr la réponse est non, puisque ce typage n'est pas interprété au runtime. Il sert aux IDE avec le couplage de Mypy (ou autre) et dans la CI pour fournir des indications sur l'usage des méthodes / fonctions
L’interpréteur ne vérifie jamais les types quand on appelle une fonction.
add(1, 2, 3)
Il y a cependant une nuance. Jusqu’à Python 3.13, les annotations sont évaluées au moment où la fonction est définie, c’est-à-dire à l’import du module. Python calcule chaque expression d’annotation et stocke le résultat dans
__annotations__
Copier le code
1 2 3 4 5 6 7 class Panier: def __init__(self) -> None: self.articles: list[str] = [] def ajouter(self, *articles: str) -> Panier: # NameError: name 'Panier' is not defined self.articles.extend(articles) return self
Ce coût est négligeable dans la très grande majorité des cas, mais il existe. Un module avec des centaines de fonctions annotées par des types complexes (génériques imbriqués,
TypedDict
On pouvait déjà l’éviter avec
from __future__ import annotations
Sinon dans l'exemple ci-dessus on mettrait
-> "Panier"
Depuis Python 3.14, les annotations sont paresseuses (PEP 649). Elles ne sont plus évaluées à la définition de la fonction, mais seulement lorsqu’on accède à
__annotations__
Copier le code
1 2 >>> add.__annotations__ {'args': int | float, 'kwargs': int | float, 'return': <class 'float'>}
Le coût à l’import disparaît donc, et on gagne au passage la possibilité de référencer une classe qui n’est pas encore définie, sans la mettre entre guillemets.
Une petite nuance : certaines librairies comme
pydantic