Fonctions en Python : def, return, arguments, *args et **kwargs, lambda et annotations de types
Une fonction en Python est un bloc de code nommé, déclaré par le mot-clé def, appelé par son nom suivi de parenthèses et qui renvoie son résultat par return ; sans return, elle renvoie None. Les arguments se transmettent par position ou par nom, *args rassemble les valeurs positionnelles supplémentaires dans un tuple, **kwargs les valeurs nommées dans un dictionnaire, et lambda crée une petite fonction anonyme à partir d'une seule expression.
Une fonction en Python est un morceau de code qui porte un nom et peut être appelé autant de fois que nécessaire avec des données différentes. On la déclare avec le mot-clé def, elle reçoit des valeurs par ses paramètres et renvoie un résultat par return. Les fonctions sont le principal moyen de transformer un long script en un ensemble d'étapes claires et vérifiables.
Déclarer une fonction : def en Python
La déclaration commence par def, suivi du nom, des paramètres entre parenthèses et d'un deux-points. Le corps est décalé de quatre espaces : l'indentation n'est pas un ornement en Python, elle fait partie de la syntaxe.
def saluer(nom):
"""Renvoie un message d'accueil pour l'utilisateur."""
return f"Bonjour, {nom} !"
print(saluer("Anna")) # Bonjour, Anna !
La chaîne entre triples guillemets placée juste après l'en-tête est la docstring, la documentation de la fonction. Elle s'affiche avec help(saluer) et dans les suggestions de l'éditeur. Selon la convention PEP 8, les noms de fonctions s'écrivent en snake_case : ajouter_depense, obtenir_taux.
La ligne def saluer(nom): s'appelle la signature de la fonction : le nom et la liste des paramètres (et leurs annotations, s'il y en a). À partir de la signature, le lecteur comprend comment appeler la fonction sans regarder son corps.
Ce que fait return en Python
return termine la fonction et transmet une valeur à l'appelant. Le code situé après un return exécuté n'est pas exécuté. Si la fonction n'a pas de return, ou si celui-ci n'a pas de valeur, elle renvoie None.
def surface(l, h):
l * h # le résultat est calculé puis perdu
print(surface(3, 4)) # None
def surface(l, h):
return l * h
print(surface(3, 4)) # 12
Oublier le return est l'une des erreurs les plus fréquentes chez les débutants : la fonction « fonctionne », mais elle renvoie None, et l'erreur apparaît une ligne plus loin. Une fonction peut aussi renvoyer plusieurs valeurs : il s'agit en réalité d'un seul tuple, qu'on décompose facilement :
def min_max(nombres):
return min(nombres), max(nombres)
bas, haut = min_max([4, 1, 9])
print(bas, haut) # 1 9
Un return anticipé permet d'éviter les imbrications inutiles : on traite d'abord les cas particuliers, puis la logique principale.
def remise(prix, est_membre):
if prix <= 0:
return 0
if not est_membre:
return prix
return prix * 0.9
Arguments de fonction en Python : positionnels et nommés
Les paramètres sont les noms figurant dans la déclaration de la fonction, les arguments sont les valeurs transmises lors de l'appel. On peut passer les arguments dans l'ordre (positionnellement) ou par leur nom (en tant qu'arguments nommés). Les arguments nommés rendent l'appel explicite, et l'ordre cesse alors d'avoir de l'importance.
def ajouter_depense(titre, montant, categorie="divers"):
return {"titre": titre, "montant": montant, "categorie": categorie}
ajouter_depense("Café", 250) # positionnel
ajouter_depense(montant=250, titre="Café") # par nom
ajouter_depense("Taxi", 600, categorie="transport") # mélange des deux
Un paramètre avec valeur par défaut est facultatif. Règles : les paramètres avec valeur par défaut viennent après les obligatoires, et lors de l'appel, les arguments positionnels précèdent les arguments nommés. Sinon, Python lève une SyntaxError.
Piège : valeur par défaut modifiable
La valeur par défaut est calculée une seule fois, au moment où la ligne def s'exécute, et non à chaque appel. Si c'est une liste ou un dictionnaire, il sera partagé entre tous les appels.
def ajouter_element(element, acc=[]):
acc.append(element)
return acc
print(ajouter_element(1)) # [1]
print(ajouter_element(2)) # [1, 2] — la liste « se souvient » de l'appel précédent
La bonne pratique consiste à mettre None par défaut et à créer le nouvel objet dans la fonction :
def ajouter_element(element, acc=None):
if acc is None:
acc = []
acc.append(element)
return acc
*args et **kwargs : un nombre quelconque d'arguments
L'astérisque placé devant un paramètre rassemble tous les arguments positionnels supplémentaires dans un tuple (tuple), et deux astérisques rassemblent les arguments nommés supplémentaires dans un dictionnaire (dict). Les noms args et kwargs ne sont qu'une convention : ce sont les astérisques qui comptent.
def total(*args):
return sum(args)
print(total(100, 250, 40)) # 390
def creer_utilisateur(nom, **kwargs):
utilisateur = {"nom": nom}
utilisateur.update(kwargs)
return utilisateur
print(creer_utilisateur("Anna", ville="Kazan", age=30))
# {'nom': 'Anna', 'ville': 'Kazan', 'age': 30}
Les mêmes astérisques fonctionnent dans l'autre sens : lors de l'appel, ils décomposent une séquence ou un dictionnaire en arguments :
valeurs = [3, 4]
print(pow(*valeurs)) # 81
options = {"sep": " | ", "end": "\n"}
print("a", "b", **options) # a | b
Paramètres uniquement positionnels ou uniquement nommés
La barre oblique / et l'astérisque * placés dans la liste des paramètres fonctionnent comme des barrières : tout ce qui précède / ne peut être passé que par position (syntaxe apparue en Python 3.8, PEP 570), tout ce qui suit * ne peut être passé que par nom.
def convertir(montant, /, *, taux, devise="USD"):
return round(montant / taux, 2), devise
convertir(100, taux=90.5) # (1.1, 'USD')
convertir(montant=100, taux=90.5) # TypeError : montant ne peut être passé que par position
convertir(100, 90.5) # TypeError : taux ne peut être passé que par nom
Les paramètres uniquement nommés protègent contre la confusion dans des appels du type convertir(100, 90.5, "EUR"), où l'on ne sait pas ce que représente chaque valeur.
Portée des variables : la règle LEGB
Python recherche un nom selon une chaîne : Local (à l'intérieur de la fonction), Enclosing (dans la fonction englobante), Global (au niveau du module), Built-in (noms intégrés comme len). On peut lire une variable globale depuis une fonction, mais pour lui affecter une nouvelle valeur, il faut la déclarer avec global.
limite = 1000
def verifier(montant):
return montant > limite # lecture de la globale : autorisé
def relever_limite():
global limite
limite = 2000 # sans global, une variable locale serait créée
En pratique, mieux vaut éviter global : une fonction qui modifie un état extérieur se teste mal. Il est plus sûr de recevoir la valeur en argument et de renvoyer le résultat.
Fonctions imbriquées et fermetures
On peut déclarer une fonction à l'intérieur d'une autre. La fonction interne voit les variables de la fonction externe, même après la fin de celle-ci : c'est ce qu'on appelle une fermeture (closure). Pour modifier une telle variable, on utilise nonlocal.
def creer_compteur():
compte = 0
def compteur():
nonlocal compte
compte += 1
return compte
return compteur
c = creer_compteur()
print(c(), c(), c()) # 1 2 3
Les fermetures sont à la base des décorateurs et des fabriques de fonctions.
Une fonction est aussi un objet
En Python, une fonction est un objet ordinaire : on peut la ranger dans une variable, la passer à une autre fonction, la renvoyer par une fonction, la stocker dans un dictionnaire. Elle possède des attributs : __name__, __doc__, __defaults__, __annotations__.
def double(x):
return x * 2
f = double
print(f(5), f.__name__) # 10 double
print(list(map(double, [1, 2, 3]))) # [2, 4, 6]
Lambda en Python : fonctions anonymes
La fonction lambda est une fonction courte sans nom, dont le corps se réduit à une seule expression. Le résultat de l'expression est renvoyé automatiquement : on n'écrit pas return.
carre = lambda x: x ** 2
print(carre(4)) # 16
depenses = [("Café", 250), ("Taxi", 600), ("Déjeuner", 450)]
depenses.sort(key=lambda d: d[1], reverse=True)
print(depenses) # [('Taxi', 600), ('Déjeuner', 450), ('Café', 250)]
print(list(filter(lambda n: n % 2 == 0, range(10)))) # [0, 2, 4, 6, 8]
Les lambdas conviennent comme court argument pour sorted, max, map ou filter. Affecter une lambda à une variable, comme carre plus haut, n'est pas recommandé par PEP 8 : si la fonction doit avoir un nom, écrivez def — elle aura alors un __name__ lisible dans les traces d'erreur, et on pourra y placer une docstring.
| def | lambda | |
|---|---|---|
| Nom | oui | non (<lambda>) |
| Corps | un nombre quelconque d'instructions | une seule expression |
| return | explicite | implicite |
| Docstring, annotations | oui | non |
| Usage | partout | court key ou callback |
Annotations de types : indications, pas vérifications
Les annotations décrivent les types attendus pour les paramètres et le résultat. Python ne les vérifie pas à l'exécution : une fonction annotée x: int accepte sans broncher une chaîne. Ce sont des outils externes — mypy, pyright, ty — et l'éditeur, qui signalent l'erreur avant même le lancement, qui les exploitent.
def ajouter_depense(titre: str, montant: float, categorie: str = "divers") -> dict[str, object]:
return {"titre": titre, "montant": montant, "categorie": categorie}
def surface(l: float, h: float) -> float:
l * h # mypy : Missing return statement
On peut lancer la vérification sans rien installer avec uvx mypy .. L'exemple ci-dessus montre l'intérêt des annotations : un return oublié dans une fonction annotée -> float est repéré immédiatement.
Depuis Python 3.12 (PEP 695), les fonctions génériques s'écrivent sans TypeVar : le paramètre de type est indiqué entre crochets, et un alias de type se déclare avec le mot-clé type.
def premier[T](elements: list[T]) -> T:
return elements[0]
type Montant = int | float
Dans Python 3.14 (PEP 649), les annotations sont devenues paresseuses : elles sont évaluées lors de leur premier accès. On peut donc citer une classe déclarée plus bas sans guillemets, et from __future__ import annotations n'est plus nécessaire.
« Pas transmis » ou « None transmis » : sentinelle en 3.15
La valeur par défaut None ne permet pas de distinguer « l'argument n'a pas été transmis » de « on a volontairement passé None ». Pendant des années, on a contourné le problème avec une sentinelle maison :
_ABSENT = object()
def obtenir_taux(devise, defaut=_ABSENT):
taux = {"USD": 90.5}
if devise in taux:
return taux[devise]
if defaut is _ABSENT:
raise KeyError(devise)
return defaut
Python 3.15 introduit la fonction intégrée sentinel (PEP 661) : ABSENT = sentinel("ABSENT") crée un marqueur unique, avec une représentation lisible, qui reste identique lors d'une copie, se sérialise avec pickle et peut servir dans une annotation du type float | ABSENT. Sur les anciennes versions, le procédé avec object() reste valable.
Erreurs typiques avec les fonctions
- return oublié — la fonction renvoie
None. - Parenthèses oubliées —
print(saluer)affiche l'objet fonction, et non le résultat de l'appel. - Valeur par défaut modifiable — une liste partagée par tous les appels.
- Mauvais nombre d'arguments —
TypeError: saluer() missing 1 required positional argument: 'nom'. - Nom identique à une fonction intégrée — une fonction nommée
listousummasque la fonction intégrée.
En bref
Les fonctions en Python se déclarent avec def, renvoient un résultat par return, et acceptent des arguments par position ou par nom. *args et **kwargs apportent de la souplesse, / et * imposent une rigueur d'appel, lambda offre la concision pour une fonction jetable, et les annotations de types, combinées à mypy, détectent les erreurs avant l'exécution. Dans le cours sur Python, ce sujet est mis en pratique : le suivi des dépenses se découpe en fonctions annotées, vérifiées par mypy.
Questions fréquentes
Quelle est la différence entre un paramètre et un argument ?
Le paramètre est le nom déclaré dans la définition de la fonction, l'argument est la valeur effectivement transmise lors de l'appel. Dans def saluer(nom), nom est un paramètre ; dans saluer("Anna"), la chaîne "Anna" est un argument.
Peut-on appeler une fonction avant de la déclarer ?
Au niveau du module, non : la ligne def doit être exécutée avant l'appel, sinon on obtient NameError. En revanche, à l'intérieur d'une autre fonction, on peut appeler une fonction déclarée plus bas, à condition que l'appel ait lieu après sa déclaration.
Combien de valeurs une fonction peut-elle renvoyer ?
Formellement une seule, mais cette valeur peut être un tuple : return a, b renvoie un tuple de deux éléments qu'on décompose avec x, y = f().
Faut-il obligatoirement écrire des annotations de types ?
Non, Python fonctionne sans elles. Mais dans du code lu par d'autres personnes, les annotations facilitent la maintenance et permettent à mypy ou pyright de détecter les erreurs avant l'exécution.
Peut-on écrire un if dans une lambda ?
Seulement une expression conditionnelle : lambda x: "pair" if x % 2 == 0 else "impair". Les instructions, les boucles et les affectations ne sont pas autorisées dans une lambda.
Ce sujet est traité en détail avec de la pratique dans le cours « Python 3.15: от новичка до профи ».
Voir le cours