Définition
Une fonction peut sembler lire une variable qui existe déjà, définie plus haut dans le même fichier, et Python refuse pourtant de l'exécuter. C'est ce que signale UnboundLocalError : la fonction lit une variable locale avant de lui avoir donné une valeur. Le nom existe bien aux yeux de Python, qui l'a repéré en analysant le corps de la fonction, mais il ne désigne encore rien : la case est réservée, et elle est vide.
Le cas le plus simple ressemble à ceci, une fonction qui tente d'incrémenter un compteur défini juste au-dessus d'elle.
compteur = 0
def incrementer():
# l'affectation ci-dessous suffit à rendre "compteur" local
compteur = compteur + 1
return compteur
incrementer()
# UnboundLocalError: cannot access local variable 'compteur'
# where it is not associated with a valueLe message déroute, puisque compteur est bien défini deux lignes plus haut. Cette définition ne joue pourtant aucun rôle ici : l'affectation écrite dans le corps de la fonction a suffi à faire de compteur un nom local, et c'est ce nom local, encore vide, que la lecture réclame.
Le texte du message a changé avec Python 3.11. Les versions plus anciennes affichent « local variable 'compteur' referenced before assignment », une formulation plus sèche qui désigne la même erreur.
Python décide de la portée avant d'exécuter
Ce mécanisme explique tout le reste. À sa définition, la fonction est analysée d'un seul bloc : tout nom qui reçoit une affectation quelque part dans son corps devient local pour la totalité de ce corps, y compris sur les lignes situées avant l'affectation. La décision est prise une fois pour toutes, avant le moindre appel.
Une lecture seule, elle, ne déclenche rien. Sans affectation dans la fonction, Python cherche le nom à l'extérieur et le trouve sans protester. C'est donc bien l'affectation qui bascule la portée, où qu'elle se trouve, y compris sur une ligne jamais atteinte à l'exécution.
Le tableau qui suit résume ces trois situations, et ce qui arrive quand le nom est lu avant l'affectation qui le concerne.
| Dans le corps de la fonction | Portée du nom | Lecture avant affectation |
|---|---|---|
| Lecture seule | Globale | Fonctionne |
| Une affectation quelque part | Locale | UnboundLocalError |
| Affectation et global | Globale | Fonctionne |
Les formes déguisées
Le cas d'école se repère vite dans du code. Les heures perdues viennent des affectations qui ne ressemblent pas à des affectations.
La première est l'affectation augmentée. compteur += 1 raccourcit compteur = compteur + 1 : il lit la valeur puis la réaffecte au même nom, et c'est cette lecture, juste avant, qui échoue.
La deuxième vient de la variable d'une boucle, qui reçoit une valeur neuve à chaque tour : c'est une affectation comme une autre. Relue après la boucle, elle tombe sous la même règle, locale et vide si la boucle n'a jamais tourné.
La troisième est la plus sournoise, parce qu'elle ne produit une erreur que sur certaines données. Voici le même piège, avec une affectation posée dans une seule branche d'un test.
def message(score):
if score >= 10:
# texte n'existe que si cette branche s'exécute
texte = "Réussi"
return texte
message(5)
# UnboundLocalError: cannot access local variable 'texte'Au-dessus de dix, tout va bien. En dessous, la branche n'est jamais exécutée, texte ne reçoit jamais de valeur, et le return échoue. Une condition sans else laisse ce trou ouvert, et le trou ne s'ouvre que sur certaines données : les tests passent, la production tombe.
Les trois corrections
La première consiste à poser une valeur par défaut avant le test. Une ligne placée en tête de fonction referme le trou sans rien changer à la logique, et elle documente au passage ce que vaut la variable quand aucune branche ne s'applique.
La deuxième consiste à déclarer l'intention. global autorise la fonction à réaffecter un nom de module, nonlocal fait la même chose pour un nom appartenant à une fonction englobante. Les deux lèvent l'erreur, mais ils ouvrent aussi la porte aux modifications à distance, difficiles à suivre dès que le programme grossit.
La troisième, souvent préférable, consiste à ne rien partager du tout : la fonction reçoit ce dont elle a besoin en argument et renvoie son résultat. Cette erreur est parfois le premier signal qu'un état global circule là où un simple passage de valeur suffirait.
Questions fréquentes
Quelle est la différence avec NameError ?
UnboundLocalError hérite de NameError : c'est un cas particulier. La distinction porte sur ce que Python sait du nom. Ici il le connaît et l'a classé local, mais aucune valeur ne lui a encore été attribuée, alors qu'une NameError signale un nom introuvable partout, souvent une faute de frappe.
Pourquoi une variable globale se lit-elle sans rien déclarer ?
Parce que lire ne crée aucun nom local. Python parcourt le corps de la fonction, n'y trouve aucune affectation, et va donc chercher le nom au niveau du module. Le mot-clé global ne devient nécessaire qu'au moment de réaffecter ce nom, jamais pour le consulter.
Comment la localiser rapidement ?
Le traceback donne la ligne fautive et le nom concerné. Il suffit ensuite de chercher ce nom plus bas dans la même fonction : l'affectation qui s'y trouve est la cause, même lorsqu'elle semble sans rapport avec la ligne qui a échoué.