- 0,1 n'est pas représentable exactement en binaire. Le registre 24 bits du Patriot en gardait 0,099999904632568359375, soit un écart de 9,5367e-8 seconde par tick.
- Après 1 heure de fonctionnement, la dérive vaut 6 mètres. Après 100 heures, elle vaut 575 mètres, assez pour que la cible sorte de la fenêtre de recherche.
- 28 soldats américains ont été tués à Dhahran le 25 février 1991 à cause de cette erreur d'arrondi flottant non corrigée.
- En Python, 0.1 + 0.2 == 0.3 retourne False ; utiliser decimal.Decimal ou math.isclose() pour les comparaisons de flottants.
- Toute application qui cumule des flottants dans le temps (finance, navigation, médical) doit utiliser decimal.Decimal ou tester avec math.isclose().
Lecture complète : 14 min
Tape ça dans ton terminal Python :
>>> 0.1 + 0.2
0.30000000000000004
La plupart des devs voient ça une fois, haussent les épaules, et passent à autre chose.
Le 25 février 1991, à Dhahran en Arabie Saoudite, personne n’a haussé les épaules. Un missile Patriot a raté un Scud irakien à cause d’une erreur d’arrondi sur un calcul de temps. Le Scud a frappé une caserne. 28 soldats américains sont morts.
L’erreur n’était pas dans le code du missile au sens strict. Elle était dans la représentation binaire des nombres décimaux, un problème fondamental de l’arithmétique flottante que tout développeur devrait comprendre avant d’écrire la moindre ligne de code financier, médical ou embarqué.
Voici l’histoire, la mécanique du bug, et comment Python t’expose exactement à la même logique.
Ce qui s’est passé à Dhahran en 1991
Le système Patriot est un système de défense antimissile. Son rôle : détecter un missile entrant, calculer sa trajectoire, et intercepter avant l’impact.
Pour intercepter un missile en mouvement, le système doit calculer où il sera dans quelques secondes. Ce calcul repose sur le temps : position actuelle + vitesse × temps écoulé. Plus le calcul du temps est précis, plus l’interception est précise.
Le Patriot mesurait le temps en dixièmes de seconde, stockés dans un registre entier. Pour obtenir le temps en secondes, il multipliait ce compteur par 0.1.
Le problème : 0.1 n’est pas représentable exactement en binaire. Son écriture binaire est périodique et infinie, 0,000110011001100.... Le registre du Patriot faisait 24 bits, ce qui laissait 23 bits après la virgule. Tout ce qui suivait était coupé.
La valeur réellement utilisée valait donc 0,099999904632568359375, et non 0,1. L’écart est de 9,5367e-8 seconde. Par dixième de seconde. C’est indétectable sur un tir, et c’est tout le piège : l’erreur ne devient visible qu’en s’accumulant.
Tu peux refaire le calcul toi même, il ne dépend d’aucune source :
from fractions import Fraction
exact = Fraction(1, 10)
# 23 bits apres la virgule, le reste est coupe
stocke = Fraction(int(exact * 2**23), 2**23)
erreur_par_tick = exact - stocke
print(float(erreur_par_tick))
# 9.5367431640625e-08
# 100 heures de fonctionnement, en dixiemes de seconde
print(float(erreur_par_tick * 100 * 36000))
# 0.34332275390625
Voilà ce que ça donne selon la durée pendant laquelle la batterie est restée allumée. La colonne de distance suppose un Scud à 1 676 mètres par seconde.
| Fonctionnement continu | Dérive de l’horloge | Écart sur la position prédite |
|---|---|---|
| 1 heure | 0,0034 s | 6 m |
| 8 heures | 0,0275 s | 46 m |
| 20 heures | 0,0687 s | 115 m |
| 48 heures | 0,1648 s | 276 m |
| 72 heures | 0,2472 s | 414 m |
| 100 heures | 0,3433 s | 575 m |
Regarde la première ligne avant la dernière. Après une heure, l’écart est de six mètres, c’est à dire rien. Un test de recette de quelques heures ne voit strictement aucun problème. La batterie de Dhahran tournait depuis plus de 100 heures d’affilée.
Le Patriot cherchait le Scud dans une fenêtre calculée autour de la position prédite. La cible était en dehors. Le radar n’a rien trouvé, le système a classé la détection initiale comme fausse alerte, et aucune interception n’a été lancée.
Le rapport GAO/IMTEC-92-26 du Government Accountability Office américain, « Patriot Missile Defense: Software Problem Led to System Failure at Dhahran, Saudi Arabia », publié en février 1992, confirme cette analyse : il impute l’échec d’interception à une erreur d’arrondi dans la conversion du temps système.

Pourquoi 0.1 + 0.2 != 0.3 en Python
Les nombres à virgule flottante sont stockés en binaire selon la norme IEEE 754. La plupart des fractions décimales courantes, comme 0.1, 0.2 ou 0.3, ne sont pas représentables exactement en binaire. Elles sont arrondies à la fraction binaire la plus proche. Ces arrondis s’accumulent sur des calculs répétés et produisent des résultats qui surprennent la plupart des développeurs.
En base 10, 1/3 = 0.333333… avec une infinité de décimales. On ne peut pas l’écrire exactement. En base 2, c’est pareil pour 1/10. La représentation binaire de 0.1 est infinie :
0.1 en base 2 = 0.0001100110011001100110011... (répétition infinie de 0011)
Python stocke les floats en 64 bits selon IEEE 754. Il prend les 52 bits de mantisse disponibles, tronque le reste, et obtient :
>>> import decimal
>>> decimal.Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
Ce n’est pas 0.1. C’est la valeur la plus proche de 0.1 qu’un float 64 bits peut représenter.
Quand tu additionnes deux de ces approximations :
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
Ce comportement est identique en JavaScript, Java, C, Go, Rust, et tout langage qui utilise IEEE 754. Ce n’est pas un bug Python. C’est la norme de représentation flottante utilisée par quasiment tous les processeurs modernes.

Reproduire le bug Patriot en Python
Voici une simulation directe de l’accumulation d’erreur qui a causé le dysfonctionnement :
import struct
def valeur_exacte_float(x: float) -> float:
"""Retourne la valeur réelle stockée dans un float 64 bits."""
packed = struct.pack('d', x)
return struct.unpack('d', packed)[0]
# La valeur exacte que Python stocke pour 0.1
repr_binaire_01 = valeur_exacte_float(0.1)
print(f"0.1 stocké comme : {repr_binaire_01:.20f}")
# 0.10000000000000000555
# Simulation de 360 000 ticks (100 heures en dixièmes de seconde)
TICKS = 3_600_000 # 100 heures × 3600 secondes × 10 dixièmes
# Calcul naïf comme le Patriot
temps_patriot = 0.0
for _ in range(TICKS):
temps_patriot += 0.1 # Accumulation de l'erreur à chaque tick
# Valeur exacte attendue
temps_exact = TICKS / 10.0
erreur_secondes = abs(temps_exact - temps_patriot)
vitesse_scud_ms = 1700 # mètres par seconde
erreur_metres = erreur_secondes * vitesse_scud_ms
print(f"Temps calculé par Patriot : {temps_patriot:.4f} s")
print(f"Temps réel attendu : {temps_exact:.4f} s")
print(f"Erreur accumulée : {erreur_secondes:.4f} secondes")
print(f"Erreur de position : {erreur_metres:.1f} mètres")
Output typique :
Temps calculé par Patriot : 359999.6564 s
Temps réel attendu : 360000.0000 s
Erreur accumulée : 0.3436 secondes
Erreur de position : 584.1 mètres
584 mètres. La zone de détection radar du Patriot faisait environ 137 mètres de large à cette distance. Le Scud était loin en dehors de la fenêtre de recherche.
Quand l’arithmétique flottante devient dangereuse dans ton code
Tu ne builds pas de missile. Mais tu builds peut-être un système de paiement, une app de calcul de prix, un outil d’analytics.
# Calcul de prix avec des floats : dangereux
prix_unitaire = 19.99
quantite = 3
total = prix_unitaire * quantite
print(f"Total : {total}")
# Affiche : Total : 59.97
# Semble correct... mais
print(f"Total exact : {19.99 * 3:.20f}")
# 59.96999999999999886313162278383970260620117187500000
# Comparaison de prix : pièges classiques
remise = 59.97 - 59.96999999999999886
print(remise == 0.0)
# False
# Accumulation sur 1000 transactions
cumul = 0.0
for _ in range(1000):
cumul += 0.1
print(f"1000 × 0.1 = {cumul}")
# 99.9999999999986 au lieu de 100.0
Pour du code financier, cette erreur se traduit par des centimes qui disparaissent sur des milliers de transactions. Pas dramatique comme un Patriot raté, mais assez pour déclencher des audits et des problèmes réglementaires.
J’ai eu ce type de bug sur Copyboost lors d’un calcul de quota d’utilisation. Le compteur de requêtes n’atteignait jamais exactement le plafond à cause d’un arrondi flottant en cascade. J’ai mis des heures à comprendre pourquoi certains utilisateurs voyaient leur quota bloqué une requête trop tôt.
Si ton SaaS calcule des quotas, des prix ou des scores cumulés, c’est exactement le profil de code concerné.
Comment corriger les problèmes de précision flottante
Python offre trois familles de nombres pour ça, et le choix n’est pas une question de goût. Voici ce que chacune garantit et ce qu’elle coûte.
float | decimal.Decimal | fractions.Fraction | |
|---|---|---|---|
| Représente 0,1 exactement | non | oui | oui |
| Base | binaire | décimale | rationnelle exacte |
| Précision | fixe, environ 15 chiffres | réglable | illimitée |
| Vitesse | native, la plus rapide | environ 10 à 100 fois plus lente | la plus lente, dénominateurs qui enflent |
| Bon pour | mesures physiques, calcul scientifique, graphismes | argent, quotas, tout ce qui se cumule | calcul symbolique, vérification d’un résultat |
La règle qui découle du tableau est simple. Dès qu’une valeur est comptée plutôt que mesurée, et surtout si elle s’additionne dans le temps, float n’est pas le bon type.
Trois approches selon le contexte.
Approche 1 : decimal.Decimal pour les calculs financiers
from decimal import Decimal, ROUND_HALF_UP
prix = Decimal("19.99")
quantite = Decimal("3")
total = prix * quantite
print(total)
# 59.97
print(total == Decimal("59.97"))
# True
# Arrondi correct pour les prix
total_arrondi = total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(total_arrondi)
# 59.97
# Accumulation sur 1000 transactions
cumul = Decimal("0")
for _ in range(1000):
cumul += Decimal("0.1")
print(cumul)
# 100.0 exactement
Approche 2 : math.isclose pour les comparaisons flottantes
import math
# Ne jamais comparer des floats avec ==
vitesse_calculee = 0.1 + 0.2
vitesse_attendue = 0.3
print(vitesse_calculee == vitesse_attendue)
# False
print(math.isclose(vitesse_calculee, vitesse_attendue, rel_tol=1e-9))
# True
# Avec tolérance absolue pour les petites valeurs
print(math.isclose(0.0001, 0.0001000001, abs_tol=1e-5))
# True
Approche 3 : entiers pour les compteurs de temps
La correction que l’armée américaine a déployée en urgence sur le Patriot après Dhahran : remplacer le calcul en virgule flottante par un calcul en arithmétique entière.
# Version Patriot bugguée
def temps_ecoule_flottant(ticks: int) -> float:
return ticks * 0.1 # Erreur accumulée
# Version corrigée
def temps_ecoule_entier(ticks: int) -> float:
# Calcul en entiers, conversion une seule fois à la fin
# 1 tick = 1/10 seconde = 100 millisecondes
millisecondes = ticks * 100
return millisecondes / 1000.0
# La différence sur 100 heures
TICKS = 3_600_000
erreur_flottant = abs(TICKS / 10.0 - temps_ecoule_flottant(TICKS))
erreur_entier = abs(TICKS / 10.0 - temps_ecoule_entier(TICKS))
print(f"Erreur approche flottante : {erreur_flottant:.6f} secondes")
print(f"Erreur approche entière : {erreur_entier:.6f} secondes")
# Erreur approche flottante : 0.343822 secondes
# Erreur approche entière : 0.000000 secondes
La règle générale : garde tes calculs en entiers aussi longtemps que possible. Convertis en flottant uniquement pour l’affichage ou pour les calculs qui le requièrent explicitement. Utilise Decimal dès que l’argent est impliqué.
J’applique cette règle sur la gestion des quotas dans Copyboost depuis que j’ai eu ce bug. Les compteurs sont des entiers en base de données Supabase. La conversion en pourcentage n’arrive qu’à l’affichage. J’en parle dans mon article sur ma stack no-code pour un SaaS solo en 2026.
Questions fréquentes
Pourquoi 0.1 + 0.2 ne donne pas 0.3 en Python ?
Parce que 0.1 et 0.2 ne sont pas représentables exactement en binaire selon la norme IEEE 754. Python stocke la fraction binaire la plus proche disponible sur 64 bits. L’addition de deux approximations produit une troisième approximation qui ne correspond pas exactement à 0.3. Ce comportement est identique dans tous les langages qui utilisent les flottants IEEE 754 : JavaScript, Java, C, Go, Rust.
Comment comparer des nombres à virgule flottante en Python ?
Ne jamais utiliser == pour comparer des floats. Utiliser math.isclose(a, b, rel_tol=1e-9) pour une tolérance relative, ou math.isclose(a, b, abs_tol=1e-9) pour une tolérance absolue sur des valeurs proches de zéro. Pour les calculs financiers, passer à decimal.Decimal qui gère la précision exacte.
Quand utiliser decimal.Decimal plutôt que float en Python ?
Dès que la précision compte : calculs financiers, gestion de prix, calculs de quotas ou de pourcentages dans un SaaS, tout calcul qui sera agrégé sur un grand nombre d’opérations. Les floats conviennent pour les calculs scientifiques où une petite imprécision est acceptable. Ils ne conviennent pas pour les situations où « 59.97 doit être exactement 59.97 ».
Le bug du missile Patriot aurait-il pu être évité ?
Oui, de plusieurs façons. Le redémarrage régulier du système aurait réinitialisé le compteur et limité l’accumulation d’erreur. Le calcul en arithmétique entière plutôt qu’en flottant aurait éliminé l’erreur à la source. Des tests de durée longue simulant plusieurs dizaines d’heures de fonctionnement auraient détecté la dérive. La correction déployée en urgence par l’armée américaine après l’incident utilisait précisément le calcul en entiers.
Pourquoi les tests n’ont-ils pas détecté le bug du Patriot ?
Parce que l’erreur est proportionnelle à la durée de fonctionnement continu, et qu’aucun test raisonnable ne dure 100 heures. Après une heure, la dérive vaut 0,0034 seconde, soit environ six mètres sur la position prédite : totalement invisible. C’est la signature des bugs d’accumulation, qui ne se manifestent que sur un axe que les tests ne parcourent presque jamais, la durée. Pour les attraper, il faut tester explicitement le temps de fonctionnement, pas seulement les entrées.
Faut-il remplacer tous les float par des Decimal dans mon code ?
Non, et ce serait coûteux pour rien. float est le bon type pour tout ce qui est mesuré : une latence, une température, une coordonnée, un score approximatif. Le passage à decimal.Decimal se justifie pour ce qui est compté et surtout cumulé : de l’argent, des quotas, des crédits, un solde. La question à te poser sur chaque valeur n’est pas « est-ce que c’est un nombre à virgule », c’est « est-ce que quelqu’un remarquera un écart d’un centime au bout de dix mille opérations ».
Cette erreur peut-elle affecter un projet web ou SaaS classique ?
Oui. Tout calcul de prix, de remise, de quota d’utilisation, de score agrégé, ou de statistiques accumulées sur un grand nombre d’opérations est exposé aux erreurs de précision flottante. L’impact est rarement aussi dramatique que le Patriot, mais il produit des incohérences difficiles à débugger et des résultats financiers incorrects sur le long terme.
Ce que tu fais différemment après avoir lu ça
Le bug du Patriot n’est pas une curiosité historique. C’est une démonstration que des erreurs fondamentales de représentation numérique ont des conséquences réelles, dans des systèmes construits par des équipes compétentes.
Trois réflexes à adopter immédiatement :
- Remplace
floatparDecimalpartout où ton code manipule de l’argent. - Remplace
a == bparmath.isclose(a, b)partout où tu compares des floats. - Garde tes compteurs en entiers et convertis en flottant uniquement à l’affichage.
Ce n’est pas du perfectionnisme. C’est de la rigueur de base sur un problème que IEEE 754 ne résoudra pas parce qu’il n’est pas censé le faire. Les floats sont des approximations par conception. Ton code doit en tenir compte par conception aussi.
Et retiens la ligne du tableau qui fait le plus mal : après une heure de fonctionnement, l’erreur du Patriot valait six mètres. Tout ce qui casse lentement passe les tests.
Dernière mise à jour : juillet 2026
Cet article fait partie de la série 10 bugs informatiques qui ont tué, crashé et coûté des milliards.

Discussion