Définition
Vous écrivez une fonction qui doit faire parler un objet. Quels objets a-t-elle le droit de recevoir ? Beaucoup de langages exigent une réponse écrite à l'avance : une classe précise, ou une interface à respecter. Python répond autrement, et sa réponse porte un nom : le duck typing.
La règle veut que Python juge un objet sur ce qu'il sait faire, et non sur la classe dont il descend. La formule d'origine le dit mieux qu'une définition : si un objet marche comme un canard et cancane comme un canard, autant le traiter comme un canard. Aucune déclaration préalable, aucune interface à signer : il suffit que la méthode appelée existe sur l'objet au moment où on l'appelle.
Deux classes sans le moindre lien de parenté, et une fonction qui les traite exactement pareil :
class Canard:
def cancane(self):
return "coin"
class Robot:
def cancane(self):
return "coin synthétique"
def faire_parler(animal):
# Aucune vérification : on appelle, et on verra bien.
print(animal.cancane())
faire_parler(Canard())
faire_parler(Robot())La fonction faire_parler ne demande jamais le type de ce qu'elle reçoit : elle appelle, et Python va chercher le nom cancane sur l'objet au dernier moment. Un objet écrit ailleurs, par quelqu'un qui n'avait jamais entendu parler d'elle, fonctionnera donc sans une ligne d'adaptation : la compatibilité ne se déclare pas, elle se constate.
Ce que Python vérifie, et à quel moment
Si rien n'est contrôlé à l'avance, qu'est-ce qui est contrôlé, et quand ? Beaucoup de langages vérifient la compatibilité avant l'exécution, sur une hiérarchie déclarée à l'avance ; Python la vérifie pendant l'exécution, sur les noms réellement présents. Le tableau les met face à face, Java ou C# à gauche, Python à droite.
| Typage nominal | Duck typing |
|---|---|
| La question posée est « de quel type est cet objet » | La question posée est « cet objet a-t-il ce qu'il faut » |
| Le contrôle a lieu avant que le programme tourne | Le contrôle a lieu à la ligne qui appelle |
| Il faut hériter ou implémenter une interface | Il suffit de porter le bon nom de méthode |
| Une incompatibilité bloque la compilation | Une incompatibilité lève une TypeError |
Ce déplacement du contrôle a une conséquence très concrète : une classe n'a pas besoin de connaître celles qui l'utiliseront, ni l'inverse. Deux bibliothèques développées séparément se branchent l'une sur l'autre du moment qu'elles s'accordent sur des noms de méthodes, sans aucune dépendance commune à installer.
Les protocoles, ces contrats que personne ne signe
Le duck typing n'est pas une tolérance accordée à votre code : Python se l'applique d'abord à lui-même. Ses propres constructions ne réclament pas un type précis, elles réclament une méthode magique, c'est-à-dire une méthode dont le nom est encadré de doubles soulignés. Voici les quatre que l'on rencontre le plus tôt.
| Ce que vous voulez faire | La méthode à écrire |
|---|---|
| Passer l'objet à len | __len__ |
| Le parcourir avec for | __iter__, qui en fait un itérable |
L'ouvrir dans un bloc with | __enter__ et __exit__ |
| L'afficher lisiblement | __str__ |
Rien à hériter, rien à déclarer : écrire la méthode suffit. C'est pourquoi une fonction qui accepte « un fichier » accepte en réalité tout objet muni des méthodes read et write, et pourquoi un faux fichier tenu en mémoire remplace le vrai dans les tests, sans qu'une ligne du code testé ne bouge.
Deux classes sans rapport peuvent porter la même méthode avec un sens différent. Un objet muni d'une méthode send n'envoie pas forcément un message : Python l'acceptera sans broncher et le problème apparaîtra bien plus loin, sous une forme qui ne ressemblera plus à une erreur de type.
Le prix à payer : l'erreur arrive tard
Cette liberté a une contrepartie. Rien ne prévient qu'un objet est inadapté avant la ligne qui l'utilise : dépourvu de la méthode attendue, il traverse sans bruit les appels intermédiaires et déclenche une AttributeError au fond de la pile, loin de l'endroit où il a été fabriqué. Confiez un chat à la fonction du début, et le message d'erreur ne parlera ni du chat ni de la fonction.
class Chat:
def miauler(self):
return "miaou"
faire_parler(Chat())
# Rien ne bronche jusqu'à l'appel de animal.cancane()
# AttributeError: 'Chat' object has no attribute 'cancane'Deux réflexes limitent la casse. Le premier consiste à documenter l'attente au lieu de la laisser deviner : une annotation de type ou une docstring qui nomme les méthodes requises fait apparaître le problème dans l'éditeur, avant l'exécution.
Le second consiste à entourer l'appel d'un try quand recevoir un objet approximatif fait partie des cas prévus. Interroger l'objet avant de s'en servir paraît plus prudent, mais cette prudence finit par refuser des objets qui auraient parfaitement fonctionné.
Questions fréquentes
Faut-il vérifier le type avec isinstance avant d'appeler ?
Rarement. Un isinstance referme la porte que le duck typing vient d'ouvrir : il refuse un objet parfaitement compatible au seul motif qu'il n'appartient pas à la bonne famille. La culture Python préfère tenter l'appel et rattraper l'échec. Le contrôle garde sa place à l'entrée des données venues de l'extérieur.
Le duck typing rend-il les annotations de type inutiles ?
Non, les deux se complètent. Le module typing propose Protocol, qui décrit les méthodes attendues sans imposer d'héritage : le vérificateur y gagne une garantie et l'appelant garde sa liberté de fournir n'importe quel objet conforme. Un outil comme mypy lit ces protocoles et signale l'objet incompatible avant même que le programme tourne.
Quelle est la différence avec l'héritage ?
L'héritage partage du code et crée une famille : une sous-classe reçoit les méthodes de sa classe mère. Le duck typing ne partage qu'un vocabulaire : deux objets qui n'ont rien en commun deviennent interchangeables parce qu'ils répondent au même nom de méthode. La formation Python montre où passe la limite entre les deux.