W3docs

Annotations de types Python

Apprenez les annotations de types Python : annoter variables, fonctions et classes, utiliser le module typing et vérifier les types avec mypy.

Les annotations de types vous permettent d'associer des informations de type attendu aux variables, aux paramètres de fonctions et aux valeurs de retour. Python ne les applique pas au moment de l'exécution — ce sont des métadonnées consommées par les éditeurs, les linters et les vérificateurs de types comme mypy pour détecter les bogues avant même d'exécuter une seule ligne.

Ce chapitre couvre :

  • Pourquoi les annotations de types sont importantes et quand les utiliser
  • Annoter les variables et les fonctions
  • Les types intégrés et le module typing (List, Dict, Optional, Union, Tuple, Any, Callable)
  • La syntaxe moderne (Python 3.10+)
  • Annoter les classes et self
  • Les génériques et les alias de types
  • L'analyse statique avec mypy
  • Les pièges courants

Pourquoi les annotations de types ?

Python est typé dynamiquement : une variable peut contenir n'importe quelle valeur de n'importe quel type. Cette flexibilité est puissante, mais elle rend les grandes bases de code plus difficiles à naviguer — on ne peut pas connaître le type d'un argument de fonction simplement en lisant le site d'appel.

Les annotations de types résolvent ce problème sans renoncer au dynamisme de Python :

  • Les éditeurs signalent les erreurs immédiatement. VS Code, PyCharm et autres soulignent les incompatibilités de types au fur et à mesure que vous tapez.
  • Le refactoring devient plus sûr. Modifiez la signature d'une fonction et le vérificateur de types vous indique chaque site d'appel qui cesse de fonctionner.
  • Le code est auto-documenté. def greet(name: str) -> str communique le contrat sans docstring.
  • Les bibliothèques deviennent plus faciles à utiliser. Les bibliothèques typées exposent l'autocomplétion pour chaque attribut et méthode.

Les annotations de types ont été introduites dans Python 3.5 via PEP 484. La syntaxe a été affinée à chaque version majeure depuis. Les exemples ci-dessous indiquent la version Python minimale où la syntaxe est devenue disponible pour la première fois.

Annoter les variables

Ajoutez un deux-points après le nom de la variable suivi du type :

name: str = "Alice"
age: int = 30
price: float = 9.99
is_active: bool = True

Vous pouvez également déclarer le type d'une variable sans lui assigner de valeur pour l'instant. C'est ce qu'on appelle une déclaration anticipée et c'est utile à l'intérieur des classes ou au niveau du module :

user_id: int   # declared but not yet assigned
user_id = 42

Les annotations de types sur les variables au niveau du module n'affectent pas le comportement à l'exécution — elles sont stockées dans le dictionnaire __annotations__ du module mais ignorées par l'interpréteur.

Annoter les fonctions

Placez les annotations sur les paramètres (après le deux-points) et sur la valeur de retour (après -> avant le deux-points qui termine la signature) :

def add(a: int, b: int) -> int:
    return a + b

def greet(name: str) -> str:
    return f"Hello, {name}!"

def send_email(to: str, subject: str, body: str) -> None:
    print(f"Sending '{subject}' to {to}")

result: int = add(3, 5)
message: str = greet("Alice")

-> None signifie que la fonction n'a pas de valeur de retour significative (elle retourne None implicitement). Omettre l'annotation de retour est également valide, mais -> None explicite rend l'intention claire.

Paramètres par défaut

Les valeurs par défaut viennent après l'annotation :

def connect(host: str, port: int = 8080, secure: bool = False) -> None:
    print(f"Connecting to {host}:{port} (secure={secure})")

connect("example.com")          # uses defaults
connect("example.com", 443, True)

*args et **kwargs

Annotez le type de l'élément, pas le type de la collection :

def total(*prices: float) -> float:
    return sum(prices)

def create_user(**fields: str) -> dict:
    return fields

print(round(total(9.99, 4.50, 12.00), 2))   # 26.49
print(create_user(name="Bob", role="admin"))

*prices: float signifie que chaque argument positionnel est un float ; à l'exécution, prices reste un tuple ordinaire de floats. De même, **fields: str signifie que la valeur de chaque argument nommé est un str.

Le module typing

Pour tout ce qui dépasse les types intégrés de base, importez depuis le module typing (Python 3.5+). À partir de Python 3.9, de nombreux types du module typing ont été intégrés directement dans les équivalents intégrés (voir Syntaxe moderne ci-dessous).

List, Tuple, Set, Dict

from typing import List, Tuple, Set, Dict

def first_names(users: List[str]) -> str:
    return users[0] if users else ""

def dimensions() -> Tuple[int, int, int]:
    return (1920, 1080, 32)

def unique_tags(items: List[str]) -> Set[str]:
    return set(items)

def word_count(text: str) -> Dict[str, int]:
    counts: Dict[str, int] = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    return counts

print(first_names(["Alice", "Bob"]))        # Alice
print(dimensions())                         # (1920, 1080, 32)
print(unique_tags(["py", "web", "py"]))     # {'py', 'web'}
print(word_count("one two one"))            # {'one': 2, 'two': 1}

Optional

Optional[X] est un raccourci pour Union[X, None]. Utilisez-le chaque fois qu'une valeur peut être absente :

from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    db = {1: "Alice", 2: "Bob"}
    return db.get(user_id)   # returns None if not found

name = find_user(1)
if name is not None:
    print(name.upper())      # ALICE

missing = find_user(99)
print(missing)               # None

Un vérificateur de types voit Optional[str] et sait que vous devez vérifier la présence de None avant d'appeler des méthodes string sur le résultat. Sans cette vérification, il signale une erreur.

Union

Union[X, Y] signifie que la valeur peut être du type X ou du type Y :

from typing import Union

def stringify(value: Union[int, float, str]) -> str:
    return str(value)

print(stringify(42))      # 42
print(stringify(3.14))    # 3.14
print(stringify("hi"))    # hi

Union est le plus utile lorsqu'une fonction accepte réellement plusieurs types sans relation entre eux. Si vous vous retrouvez à écrire Union[str, None], utilisez plutôt Optional[str] — c'est plus idiomatique.

Callable

Callable[[ArgTypes...], ReturnType] annote une fonction passée en argument :

from typing import Callable

def apply_twice(func: Callable[[int], int], value: int) -> int:
    return func(func(value))

def double(n: int) -> int:
    return n * 2

print(apply_twice(double, 3))   # 12

Callable[[int], int] signifie : un callable qui prend un argument int et retourne un int. Si la liste d'arguments est complexe ou inconnue, utilisez Callable[..., ReturnType].

Any

Any est un type spécial qui désactive la vérification de types pour cette valeur. Tout type est assignable à Any et assignable depuis Any :

from typing import Any

def log(value: Any) -> None:
    print(value)

log(42)
log("hello")
log([1, 2, 3])

Utilisez Any avec parcimonie — c'est une trappe de sortie qui supprime la protection même que les annotations de types procurent. C'est approprié lors de l'interfaçage avec du code tiers non typé, ou lors d'une migration progressive d'une grande base de code.

Syntaxe moderne (Python 3.9+, 3.10+)

Génériques intégrés (Python 3.9+)

À partir de Python 3.9, vous pouvez utiliser les types intégrés directement comme génériques, sans importer depuis typing :

# Python 3.9+
def word_count(text: str) -> dict[str, int]:
    counts: dict[str, int] = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    return counts

def first(items: list[int]) -> int | None:
    return items[0] if items else None

print(word_count("cat dog cat"))    # {'cat': 2, 'dog': 1}
print(first([10, 20, 30]))         # 10
print(first([]))                   # None

Utilisez list[str] au lieu de List[str], dict[str, int] au lieu de Dict[str, int], et ainsi de suite.

Syntaxe d'union X | Y (Python 3.10+)

Python 3.10 a introduit l'opérateur | pour les unions, remplaçant Union[X, Y] et Optional[X] :

# Python 3.10+
def parse(value: str | int | None) -> str:
    if value is None:
        return "nothing"
    return str(value)

print(parse("hello"))   # hello
print(parse(42))        # 42
print(parse(None))      # nothing

str | None est équivalent à Optional[str]. Cette syntaxe est plus propre et plus facile à lire.

Annoter les classes

Annotez les attributs d'instance à l'intérieur de __init__, et ajoutez des annotations de retour aux méthodes :

class BankAccount:
    owner: str        # class-level annotation (no default value)
    balance: float

    def __init__(self, owner: str, initial_balance: float = 0.0) -> None:
        self.owner = owner
        self.balance = initial_balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount

    def withdraw(self, amount: float) -> bool:
        if amount > self.balance:
            return False
        self.balance -= amount
        return True

    def __repr__(self) -> str:
        return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"

account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.withdraw(30.0))   # True
print(account)                  # BankAccount(owner='Alice', balance=120.00)

L'annotation sur self est toujours inférée — vous n'écrivez jamais self: BankAccount. Le type de retour de __init__ est toujours None.

ClassVar

Utilisez ClassVar[T] (depuis typing) pour marquer un attribut qui appartient à la classe, pas à chaque instance :

from typing import ClassVar

class Config:
    MAX_RETRIES: ClassVar[int] = 3
    timeout: int

    def __init__(self, timeout: int) -> None:
        self.timeout = timeout

print(Config.MAX_RETRIES)   # 3

Un vérificateur de types avertit si vous essayez de définir ClassVar sur une instance — il est destiné à être partagé au niveau de la classe.

Alias de types

Un alias de type donne à un type long ou complexe un nom plus court et plus significatif :

from typing import List, Tuple

# Simple alias
UserID = int
Filename = str

# Structured alias
Coordinates = Tuple[float, float]
Matrix = List[List[float]]

def distance(p1: Coordinates, p2: Coordinates) -> float:
    return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5

print(distance((0.0, 0.0), (3.0, 4.0)))   # 5.0

À partir de Python 3.12, utilisez l'instruction type pour des alias explicites et inspectables :

# Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]

Génériques avec TypeVar

TypeVar vous permet d'écrire une seule fonction qui fonctionne avec n'importe quel type tout en préservant les relations de types :

from typing import TypeVar, List

T = TypeVar("T")

def first_item(items: List[T]) -> T:
    return items[0]

x: int = first_item([1, 2, 3])       # x is int
s: str = first_item(["a", "b"])      # s is str

Le vérificateur de types déduit de l'argument ce qu'est T, et transporte cette information jusqu'au type de retour. Sans TypeVar, vous devriez retourner Any et perdre la sécurité des types.

Vous pouvez contraindre TypeVar à un ensemble de types autorisés :

from typing import TypeVar

Numeric = TypeVar("Numeric", int, float)

def double(n: Numeric) -> Numeric:
    return n * 2

print(double(4))      # 8   (int)
print(double(2.5))    # 5.0 (float)

Vérification statique des types avec mypy

mypy est le vérificateur de types statique le plus utilisé pour Python. Installez-le avec pip :

pip install mypy

Exécutez-le ensuite sur un fichier :

mypy my_script.py

Exemple : détecter un bogue avec mypy

Enregistrez ce qui suit sous le nom demo.py :

def greet(name: str) -> str:
    return f"Hello, {name}!"

result = greet(42)   # passing int instead of str
print(result.upper())

L'exécution de mypy demo.py signale :

demo.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)

Python lui-même exécute le code correctement (les f-strings convertissent n'importe quel type), mais mypy a détecté l'incompatibilité avant que vous n'ayez à la découvrir en production.

Options utiles de mypy

OptionEffet
--strictActive toutes les vérifications optionnelles (recommandé pour les nouveaux projets)
--ignore-missing-importsSupprime les erreurs concernant les stubs tiers manquants
--check-untyped-defsVérifie également les fonctions sans annotations
--disallow-untyped-defsExige des annotations sur toutes les définitions de fonctions

Un fichier mypy.ini (ou [tool.mypy] dans pyproject.toml) maintient la configuration hors de la ligne de commande :

[mypy]
strict = true
ignore_missing_imports = true

Typage progressif

Vous n'êtes pas obligé d'annoter chaque fonction en même temps. Python prend en charge le typage progressif : le code annoté et non annoté coexistent paisiblement. mypy ignore les fonctions non annotées par défaut (sauf si --check-untyped-defs est activé).

Une approche pratique pour une base de code existante :

  1. Ajoutez des annotations au nouveau code dès le premier jour.
  2. Annotez d'abord les fonctions les plus appelées ou les plus sujettes aux erreurs.
  3. Activez --strict module par module au fur et à mesure que la couverture s'améliore.
  4. Utilisez Any uniquement lorsqu'une bibliothèque tierce n'est pas typée, et ajoutez un commentaire expliquant pourquoi.

Pièges courants

Références anticipées

Si un type fait référence à une classe définie plus loin dans le même fichier, encapsulez le nom entre guillemets pour en faire une chaîne (une référence anticipée) :

class Node:
    def __init__(self, value: int, next: "Node | None" = None) -> None:
        self.value = value
        self.next = next

head = Node(1, Node(2))
print(head.value, head.next.value)   # 1 2

À partir de Python 3.10+, ajoutez from __future__ import annotations en haut du fichier à la place. Cela rend toutes les annotations des chaînes paresseuses et élimine le besoin de guillemets manuels.

Annotations à l'exécution

Par défaut, les annotations dans Python 3.9 et antérieurs sont évaluées de manière eager. Cela signifie qu'une référence anticipée sans guillemets lève une NameError :

# Works (with quotes):
def clone(self: "MyClass") -> "MyClass": ...

Avec from __future__ import annotations (Python 3.7+), toutes les annotations sont stockées comme des chaînes et évaluées uniquement lors de l'inspection — ce qui résout automatiquement le problème de référence anticipée.

None vs Optional

Une erreur courante est d'annoter un type de retour comme str alors que la fonction peut en réalité retourner None. Utilisez toujours Optional[str] (ou str | None) lorsque None est un retour possible :

from typing import Optional

# Wrong — mypy will flag callers that assume this is always str
def get_name(user_id: int) -> str:
    if user_id == 0:
        return None   # type: ignore  — this is the bug

# Correct
def get_name_safe(user_id: int) -> Optional[str]:
    if user_id == 0:
        return None
    return "Alice"

list vs List (compatibilité de version)

Si votre code s'exécute sur Python 3.8 ou antérieur, vous devez utiliser from typing import List et écrire List[str]. Sur Python 3.9+, list[str] fonctionne directement. Si vous devez prendre en charge les deux, utilisez soit les imports de typing, soit ajoutez from __future__ import annotations.

Référence rapide

AnnotationSignification
x: intLa variable x est un entier
def f(a: str) -> boolLe paramètre a est str ; la valeur de retour est bool
-> NoneLa fonction ne retourne rien de significatif
Optional[str]str ou None
Union[int, str]int ou str
list[int] / List[int]Liste d'entiers
dict[str, int] / Dict[str, int]Dict associant str à int
tuple[int, str] / Tuple[int, str]Tuple de (int, str)
Callable[[int], str]Fonction prenant int, retournant str
AnyN'importe quel type (désactive la vérification)
ClassVar[T]Attribut au niveau de la classe
TypeVar("T")Variable de type générique

Sujets connexes

  • Fonctions Python — où vivent les annotations de types sur les paramètres et les types de retour.
  • Classes et objets Python — pour annoter __init__, les méthodes et les attributs de classe.
  • Dataclasses Python — les annotations de types sont requises pour déclarer les champs d'une dataclass.
  • Classes abstraites Python — les classes de base abstraites fonctionnent naturellement avec les annotations de types.

Pratique

Pratique
What does Optional[str] mean in a Python type hint?
What does Optional[str] mean in a Python type hint?
Pratique
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Pratique
What is the purpose of TypeVar in the typing module?
What is the purpose of TypeVar in the typing module?
Was this page helpful?