31  *args et **kwargs : fonctions flexibles

Jusqu’ici, vos fonctions avaient un nombre fixe de paramètres. Python permet de faire mieux : des fonctions qui acceptent un nombre variable d’arguments, positionnels ou nommés. C’est ce que permettent *args et **kwargs — deux constructions très fréquentes dans le code Python réel.

31.1 Le problème : un nombre variable d’arguments

Imaginez une fonction additionner qui doit fonctionner pour 2 nombres, 3, 10… sans qu’on connaisse le nombre à l’avance.

# ❌ Solution limitée : un paramètre par argument
def additionner(a, b):
    return a + b

print(additionner(1, 2))
# print(additionner(1, 2, 3))    # TypeError !
3

Ou alors on passe une liste, mais l’appel est moins pratique :

def additionner(nombres):
    return sum(nombres)

print(additionner([1, 2, 3, 4]))    # obligation de mettre la liste
10

Idéalement, on voudrait écrire additionner(1, 2, 3, 4) directement. C’est ce que permet *args.

31.2 *args — arguments positionnels variables

Le préfixe * dans la signature collecte tous les arguments positionnels supplémentaires dans un tuple.

def additionner(*args):
    return sum(args)

print(additionner(1, 2))
print(additionner(1, 2, 3))
print(additionner(1, 2, 3, 4, 5))
print(additionner())               # aucun argument : 0
3
6
15
0

args est un simple tuple — vous pouvez l’itérer, le mesurer, etc.

def afficher_tout(*args):
    print(f"Type : {type(args)}")
    print(f"Nombre : {len(args)}")
    for i, val in enumerate(args):
        print(f"  [{i}] {val}")

afficher_tout("Alice", 30, True, [1, 2])
Type : <class 'tuple'>
Nombre : 4
  [0] Alice
  [1] 30
  [2] True
  [3] [1, 2]
Le nom args est une convention, pas une obligation

On pourrait écrire def f(*nombres): et ça fonctionnerait. Mais *args est l’idiome universellement reconnu — tous les développeurs Python savent immédiatement ce que ça signifie. Utilisez args pour rester lisible.

Combiner paramètres fixes et *args

Les paramètres positionnels fixes viennent avant *args.

def rapport(titre, *valeurs):
    print(f"=== {titre} ===")
    for v in valeurs:
        print(f"  - {v}")

rapport("Notes", 12, 15, 17, 9)
=== Notes ===
  - 12
  - 15
  - 17
  - 9

*args est toujours un tuple

Même avec un seul argument :

def f(*args):
    print(type(args), args)

f(42)
f(42, 43)
f()
<class 'tuple'> (42,)
<class 'tuple'> (42, 43)
<class 'tuple'> ()

31.3 **kwargs — arguments nommés variables

Le préfixe ** collecte tous les arguments nommés supplémentaires dans un dictionnaire.

def afficher_infos(**kwargs):
    for cle, valeur in kwargs.items():
        print(f"  {cle} = {valeur}")

afficher_infos(nom="Alice", age=30, ville="Paris")
  nom = Alice
  age = 30
  ville = Paris

Exemple : fabriquer un URL avec paramètres

def construire_url(base, **params):
    if not params:
        return base
    query = "&".join(f"{k}={v}" for k, v in params.items())
    return f"{base}?{query}"

print(construire_url("http://api.fr/search"))
print(construire_url("http://api.fr/search", q="python", page=2))
print(construire_url("http://api.fr/search", user="alice", limit=50, sort="date"))
http://api.fr/search
http://api.fr/search?q=python&page=2
http://api.fr/search?user=alice&limit=50&sort=date

**kwargs est toujours un dict

def f(**kwargs):
    print(type(kwargs), kwargs)

f(a=1, b=2)
f()
<class 'dict'> {'a': 1, 'b': 2}
<class 'dict'> {}

31.4 Combiner *args et **kwargs

On peut utiliser les deux dans la même fonction, dans cet ordre strict :

def f(pos_fixe, *args, **kwargs):
    ...
def tout_afficher(titre, *valeurs, **options):
    print(f"=== {titre} ===")
    print(f"  Positionnels : {valeurs}")
    print(f"  Options : {options}")

tout_afficher("Démo", 1, 2, 3, couleur="rouge", gras=True)
=== Démo ===
  Positionnels : (1, 2, 3)
  Options : {'couleur': 'rouge', 'gras': True}

Ordre obligatoire des paramètres

Dans une signature de fonction, l’ordre est :

  1. Paramètres positionnels fixes (avec ou sans défaut).
  2. *args — positionnels supplémentaires.
  3. Paramètres keyword-only (voir ci-dessous).
  4. **kwargs — nommés supplémentaires.
def f(a, b=0, *args, option=False, **kwargs):
    ...

31.5 Le déballage à l’appel : * et **

* et ** fonctionnent aussi dans l’autre sens : pour déballer une liste ou un dict en arguments lors d’un appel.

Déballer une liste/tuple avec *

def additionner(a, b, c):
    return a + b + c

valeurs = [1, 2, 3]
# Au lieu de additionner(valeurs[0], valeurs[1], valeurs[2])
print(additionner(*valeurs))
6

Déballer un dict avec **

def presenter(nom, age, ville):
    print(f"{nom}, {age} ans, {ville}")

personne = {"nom": "Alice", "age": 30, "ville": "Paris"}
# Au lieu de presenter(nom=personne["nom"], age=personne["age"], ville=personne["ville"])
presenter(**personne)
Alice, 30 ans, Paris
La symétrie *args / *valeurs est élégante

Dans une signature : *args collecte les arguments en tuple. À l’appel : *liste déballe un tuple en arguments.

C’est le même symbole avec deux usages complémentaires — une fois compris, on passe facilement de l’un à l’autre.

Déballage entre fonctions

C’est très utile pour passer ses propres arguments à une autre fonction :

def loguer_appel(func, *args, **kwargs):
    print(f"Appel : {func.__name__}({args}, {kwargs})")
    resultat = func(*args, **kwargs)      # transmet TOUS les args/kwargs
    print(f"Résultat : {resultat}")
    return resultat


def additionner(a, b, c):
    return a + b + c

loguer_appel(additionner, 1, 2, 3)
loguer_appel(additionner, 1, 2, c=30)
Appel : additionner((1, 2, 3), {})
Résultat : 6
Appel : additionner((1, 2), {'c': 30})
Résultat : 33
33

C’est un pattern fondamental pour écrire des décorateurs (chapitre 3 de la Partie 4).

31.6 Paramètres keyword-only (Python 3+)

On peut forcer certains paramètres à ne pouvoir être passés que par nom, jamais par position. On les place après *args (ou après un * solo).

Avec *args

def rapport(titre, *valeurs, separateur=" | "):
    print(titre + " : " + separateur.join(str(v) for v in valeurs))

rapport("Notes", 12, 15, 17)
rapport("Notes", 12, 15, 17, separateur=" ; ")

# On NE PEUT PAS passer separateur par position
# rapport("Notes", 12, 15, 17, " ; ")   → le " ; " irait dans *valeurs !
Notes : 12 | 15 | 17
Notes : 12 ; 15 ; 17

Avec * seul

Pour rendre des paramètres keyword-only sans utiliser *args, on place un * tout seul :

def creer_utilisateur(nom, *, role="user", actif=True):
    return {"nom": nom, "role": role, "actif": actif}

# Par nom : OK
print(creer_utilisateur("Alice", role="admin"))

# Par position : interdit !
{'nom': 'Alice', 'role': 'admin', 'actif': True}
def creer_utilisateur(nom, *, role="user", actif=True):
    return {"nom": nom, "role": role, "actif": actif}

creer_utilisateur("Alice", "admin")     # role passé par position → erreur
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[16], line 4
      1 def creer_utilisateur(nom, *, role="user", actif=True):
      2     return {"nom": nom, "role": role, "actif": actif}
      3 
----> 4 creer_utilisateur("Alice", "admin")     # role passé par position → erreur

TypeError: creer_utilisateur() takes 1 positional argument but 2 were given
Pourquoi forcer le keyword-only ?

Pour les paramètres booléens ou optionnels qui risquent d’être ambigus par position :

# ❌ Peu lisible
creer_utilisateur("Alice", "admin", True)

# ✅ Explicite
creer_utilisateur("Alice", role="admin", actif=True)

C’est une bonne pratique pour des API professionnelles.

31.7 Fusion de dicts avec ** (Python 3.5+)

Un usage très pratique : fusionner plusieurs dicts en un.

defauts = {"theme": "clair", "lang": "fr", "taille": 14}
user =    {"theme": "sombre", "taille": 16}

# Fusion : user écrase defauts
config = {**defauts, **user}
print(config)
{'theme': 'sombre', 'lang': 'fr', 'taille': 16}
Depuis Python 3.9 : opérateur |

Python 3.9+ propose une syntaxe encore plus simple :

defauts = {"theme": "clair", "lang": "fr"}
user = {"theme": "sombre"}

config = defauts | user
print(config)
{'theme': 'sombre', 'lang': 'fr'}

Les deux formes coexistent.

31.8 Récap visuel

Voici une signature complète, dans l’ordre canonique :

def fonction(
    a,                # positionnel obligatoire
    b=0,              # positionnel avec défaut
    *args,            # positionnels supplémentaires → tuple
    kw1,              # keyword-only obligatoire
    kw2="def",        # keyword-only avec défaut
    **kwargs          # nommés supplémentaires → dict
):
    ...

31.9 🧩 Quiz 2.1 — *args et **kwargs {.unnumbered}

Question 1

Que vaut args dans l’appel f(1, 2, 3) avec def f(*args): ?

  1. 1, 2, 3
  2. [1, 2, 3]
  3. (1, 2, 3)
  4. {1: 2, 3: ...}

c) (1, 2, 3)*args produit toujours un tuple, même avec un seul élément ou aucun.

def f(*args):
    print(type(args), args)

f(1, 2, 3)
<class 'tuple'> (1, 2, 3)

Question 2

Que s’affiche-t-il ?

def f(**kwargs):
    print(type(kwargs))

f(a=1, b=2)
  1. <class 'list'>
  2. <class 'tuple'>
  3. <class 'dict'>
  4. Une erreur

c) <class 'dict'>**kwargs collecte toujours en dictionnaire.

def f(**kwargs):
    print(type(kwargs))
f(a=1, b=2)
<class 'dict'>

Question 3

Quel ordre est correct ?

  1. def f(*args, a, **kwargs):
  2. def f(a, **kwargs, *args):
  3. def f(a, *args, **kwargs):
  4. def f(**kwargs, *args, a):

c) def f(a, *args, **kwargs): — l’ordre canonique est : positionnels → *args → keyword-only → **kwargs. L’option a) est valide aussi (elle fait de a un keyword-only) mais c) est la forme standard.

Question 4

Que fait f(*[1, 2, 3]) avec def f(a, b, c): ?

  1. Passe la liste [1, 2, 3] comme seul argument
  2. Appelle f(1, 2, 3) en déballant la liste
  3. Une erreur
  4. Renvoie [1, 2, 3]

b) — le * à l’appel déballe l’itérable en arguments positionnels. Équivalent à f(1, 2, 3).

def f(a, b, c):
    return a + b + c

print(f(*[1, 2, 3]))
6

Question 5

Que s’affiche-t-il ?

d = {"a": 1, "b": 2}
def f(a, b):
    return a * b

print(f(**d))
  1. 2
  2. 12
  3. "ab"
  4. Une erreur

a) 2**d déballe le dict en arguments nommés : équivalent à f(a=1, b=2), soit 1 * 2 = 2.

d = {"a": 1, "b": 2}
def f(a, b):
    return a * b
print(f(**d))
2

Question 6

Que s’affiche-t-il ?

def f(nom, *, politesse="Bonjour"):
    print(politesse, nom)

f("Alice", "Salut")
  1. Salut Alice
  2. Bonjour Alice
  3. Une TypeError
  4. Bonjour Alice Salut

c) Une TypeError — le * seul rend politesse keyword-only. On ne peut pas le passer par position. L’appel correct serait f("Alice", politesse="Salut").

def f(nom, *, politesse="Bonjour"):
    print(politesse, nom)

f("Alice", "Salut")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[23], line 4
      1 def f(nom, *, politesse="Bonjour"):
      2     print(politesse, nom)
      3 
----> 4 f("Alice", "Salut")

TypeError: f() takes 1 positional argument but 2 were given

Question 7

Que vaut config ?

defauts = {"a": 1, "b": 2}
user = {"b": 20, "c": 30}
config = {**defauts, **user}
  1. {"a": 1, "b": 2, "c": 30}
  2. {"a": 1, "b": 20, "c": 30}
  3. {"a": 1, "b": [2, 20], "c": 30}
  4. Une erreur

b) {"a": 1, "b": 20, "c": 30} — les clés du second dict écrasent celles du premier en cas de conflit. Ici b passe de 2 à 20.

defauts = {"a": 1, "b": 2}
user = {"b": 20, "c": 30}
print({**defauts, **user})
{'a': 1, 'b': 20, 'c': 30}

✏️ Exercice 2.1 — Calcul de moyenne flexible

Écrivez une fonction moyenne(*nombres) qui accepte n’importe quel nombre d’arguments numériques et renvoie leur moyenne. Gérer le cas sans argument.

def moyenne(*nombres):
    ...

print(moyenne(10, 20, 30))
print(moyenne(5))
print(moyenne())
def moyenne(*nombres):
    if not nombres:
        return 0
    return sum(nombres) / len(nombres)

print(moyenne(10, 20, 30))
print(moyenne(5))
print(moyenne())
20.0
5.0
0

✏️ Exercice 2.2 — Construire un email structuré

Écrivez creer_email(destinataire, sujet, **entetes) qui renvoie un dict structuré. Les entetes sont des en-têtes optionnels comme cc, bcc, priorite

def creer_email(destinataire, sujet, **entetes):
    ...

email = creer_email(
    "alice@example.com",
    "Rappel",
    cc="bob@example.com",
    priorite="haute",
)
print(email)
def creer_email(destinataire, sujet, **entetes):
    email = {
        "to": destinataire,
        "subject": sujet,
    }
    email.update(entetes)
    return email

email = creer_email(
    "alice@example.com",
    "Rappel",
    cc="bob@example.com",
    priorite="haute",
)
print(email)

# Sans en-têtes optionnels
email_simple = creer_email("charlie@example.com", "Salut")
print(email_simple)
{'to': 'alice@example.com', 'subject': 'Rappel', 'cc': 'bob@example.com', 'priorite': 'haute'}
{'to': 'charlie@example.com', 'subject': 'Salut'}

Plus Pythonique :

def creer_email(destinataire, sujet, **entetes):
    return {"to": destinataire, "subject": sujet, **entetes}

print(creer_email("alice@ex.com", "Salut", cc="bob@ex.com"))
{'to': 'alice@ex.com', 'subject': 'Salut', 'cc': 'bob@ex.com'}

✏️ Exercice 2.3 — Décorateur de logging (aperçu)

Écrivez une fonction tracer(fonction, *args, **kwargs) qui :

  1. Affiche le nom de la fonction et ses arguments.
  2. Exécute la fonction avec ces arguments.
  3. Affiche le résultat.
  4. Renvoie le résultat.
def tracer(fonction, *args, **kwargs):
    ...

def addition(a, b):
    return a + b

def saluer(nom, politesse="Bonjour"):
    return f"{politesse}, {nom}"

tracer(addition, 3, 5)
tracer(saluer, "Alice")
tracer(saluer, "Bob", politesse="Salut")
def tracer(fonction, *args, **kwargs):
    nom = fonction.__name__
    args_str = ", ".join(repr(a) for a in args)
    kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
    signature = ", ".join(filter(None, [args_str, kwargs_str]))
    print(f">>> {nom}({signature})")
    resultat = fonction(*args, **kwargs)
    print(f"    = {resultat!r}")
    return resultat


def addition(a, b):
    return a + b

def saluer(nom, politesse="Bonjour"):
    return f"{politesse}, {nom}"

tracer(addition, 3, 5)
tracer(saluer, "Alice")
tracer(saluer, "Bob", politesse="Salut")
>>> addition(3, 5)
    = 8
>>> saluer('Alice')
    = 'Bonjour, Alice'
>>> saluer('Bob', politesse='Salut')
    = 'Salut, Bob'
'Salut, Bob'
  • fonction.__name__ donne le nom de la fonction (toute fonction a cet attribut).
  • repr(x) et f"{x!r}" affichent la forme « développeur » (avec guillemets pour les chaînes).
  • fonction(*args, **kwargs) déballe à l’appel — le même symbole fait les deux sens !
  • Ce pattern est la base des décorateurs (chapitre 3 de la Partie 4).

✏️ Exercice 2.4 — Fusion de configurations

Écrivez une fonction fusionner_config(*configs) qui reçoit un nombre variable de dicts et les fusionne en un seul — chaque dict écrase les précédents.

def fusionner_config(*configs):
    ...

defaut = {"theme": "clair", "lang": "fr", "taille": 14}
user   = {"theme": "sombre"}
session = {"taille": 18}

print(fusionner_config(defaut, user, session))
def fusionner_config(*configs):
    resultat = {}
    for config in configs:
        resultat.update(config)
    return resultat

defaut = {"theme": "clair", "lang": "fr", "taille": 14}
user   = {"theme": "sombre"}
session = {"taille": 18}

print(fusionner_config(defaut, user, session))
print(fusionner_config())   # dict vide si aucun arg
{'theme': 'sombre', 'lang': 'fr', 'taille': 18}
{}
def fusionner_config(*configs):
    return {k: v for config in configs for k, v in config.items()}

print(fusionner_config(
    {"theme": "clair", "lang": "fr"},
    {"theme": "sombre"},
    {"taille": 18},
))
{'theme': 'sombre', 'lang': 'fr', 'taille': 18}

À retenir

Points clés du chapitre
  1. *args collecte les arguments positionnels en tuple.
  2. **kwargs collecte les arguments nommés en dict.
  3. Ordre : (positionnels, *args, keyword-only, **kwargs).
  4. À l’appel, *liste déballe la liste en arguments, **dict déballe le dict.
  5. * seul rend les paramètres suivants keyword-only.
  6. Fusion de dicts : {**d1, **d2} (ou d1 | d2 en Python 3.9+).
  7. Le pattern func(*args, **kwargs) est la base des décorateurs (Partie 4).

← Chapitre précédent : Les compréhensionsChapitre suivant : Les fonctions lambda →