L'essentiel
  • 0,1 n'est pas représentable exactement en binaire ; sa valeur réelle stockée en 24 bits est 0,09999847412109375.
  • Sur 100 heures de fonctionnement continu, cette erreur accumulée atteignait 0,34 secondes, soit 578 mètres d'imprécision sur la position prédite d'un Scud.
  • 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 : 12 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 à virgule flottante. Sa représentation binaire est une fraction infinie périodique. Dans un registre 24 bits, cette valeur était tronquée à 0.09999847412109375.

L’erreur semble minuscule. Elle l’est sur une seconde. Mais le système à Dhahran fonctionnait en continu depuis plus de 100 heures sans redémarrage. Sur 100 heures de fonctionnement, soit 360 000 secondes, cette petite erreur s’accumulait à chaque tick.

Erreur accumulée sur 100 heures :

0.1 - 0.09999847412109375 = 0.00000152587890625 secondes d'erreur par dixième de seconde
× 3 600 000 dixièmes de seconde sur 100 heures
= 0.34 secondes d'erreur totale

Un Scud se déplace à environ 1 700 mètres par seconde. Une erreur de 0.34 secondes sur la position prédite représente une erreur de 578 mètres.

Le Patriot cherchait le Scud dans une zone de 0.34 secondes de large centrée sur la prédiction. La cible était 578 mètres à côté. Le radar n’a rien détecté. Le système a classé le Scud comme “faux positif” et n’a pas lancé d’interception.

Le rapport du Government Accountability Office américain, publié en 1992, confirme cette analyse dans ses grandes lignes. [À CONFIRMER : référence exacte GAO/IMTEC-92-26]

Graphique de la dérive d'erreur flottante accumulée sur 100 heures de fonctionnement du Patriot


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.

Tableau comparatif float vs Decimal en Python : précision, cas d'usage, performance


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, passe ta logique de calcul en revue. Et si tu veux savoir si ton texte marketing convertit autant que ton code est robuste, lance un audit gratuit sur copyboost.io.


Comment corriger les problèmes de précision flottante

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.

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 :

  1. Remplace float par Decimal partout où ton code manipule de l’argent.
  2. Remplace a == b par math.isclose(a, b) partout où tu compares des floats.
  3. 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.

Le même niveau de rigueur s’applique à ton copywriting. Si ton texte est une approximation de ce que ton lecteur veut lire, il ne convertit pas. Lance un audit gratuit sur copyboost.io pour le vérifier.

Dernière mise à jour : mai 2026

Cet article fait partie de la série 10 bugs informatiques qui ont tué, crashé et coûté des milliards.