Форматирование чисел в Python: частые ошибки
Почему round(2.675, 2) выдаёт 2.67, откуда берутся 0.1 + 0.2 = 0.30000000000000004 и как форматировать деньги без сюрпризов в Python 3.x.

Вы пишете round(2.675, 2), ждёте 2.68, а получаете 2.67. Или складываете 0.1 + 0.2 и видите 0.30000000000000004. Это не баг Python — это следствие того, как числа с плавающей точкой хранятся в памяти. Ниже — разбор конкретных мест, где программисты на Python 3.x ошибаются при форматировании чисел, и что делать вместо этого. Примеры проверялись на CPython 3.12, но поведение одинаково начиная с 3.6, где появились f-строки.
Откуда берутся «неправильные» результаты
Тип float в Python — это двоичное число с плавающей точкой двойной точности (IEEE 754). Многие десятичные дроби в двоичной системе не имеют точного представления. Число 0.1 хранится не как ровно одна десятая, а как ближайшее к ней двоичное значение — чуть больше или чуть меньше.
Проверить, что на самом деле лежит в переменной, помогает большая точность вывода:
>>> format(0.1, '.20f')
'0.10000000000000000555'
>>> format(2.675, '.20f')
'2.67499999999999982236'
Вот почему round(2.675, 2) даёт 2.67: округляется не «два и шестьсот семьдесят пять тысячных», а число, которое чуть меньше. Округление отработало правильно — врёт наше представление о том, что хранится в переменной.
Ошибка не в round и не в format. Ошибка — в ожидании, что float хранит десятичную дробь точно. Он хранит ближайшее двоичное приближение.
Ошибка 1. round для отображения
Частый паттерн — округлить число функцией round(), чтобы вывести его пользователю. Это лишний шаг: round() возвращает float, который снова может не иметь точного представления.
>>> round(1.255, 2)
1.25
>>> f'{1.255:.2f}'
'1.25'
>>> f'{2.5:.0f}'
'2'
Если число нужно только показать — форматируйте его сразу через f-строку или format(), без промежуточного round(). Формат-спецификатор .2f сам округлит до нужного числа знаков и вернёт строку. Держите правило простым: round() — для вычислений, спецификатор формата — для вывода.
Про банковское округление
И round(), и спецификатор .Nf используют округление «к ближайшему чётному» (banker's rounding). Поэтому round(2.5) даёт 2, а round(3.5) — 4. Это не ошибка, а стандарт IEEE 754. Если бизнес-логика требует привычного «округления от нуля» (2.5 → 3), нужен decimal с явным режимом округления, о нём ниже.
Ошибка 2. Деньги во float
Хранить цену как float — источник копеечных расхождений, которые всплывают в отчётах и сверках. Складывая тысячи транзакций, вы накапливаете ошибку округления.
>>> total = 0.0
>>> for _ in range(10):
... total += 0.1
...
>>> total
0.9999999999999999
Для денег используйте модуль decimal из стандартной библиотеки. Важно: создавайте Decimal из строки, а не из float — иначе вы перенесёте двоичную неточность внутрь Decimal.
from decimal import Decimal, ROUND_HALF_UP
# Правильно: из строки
cena = Decimal('2.675')
# Неправильно: float уже неточен, Decimal это сохранит
ploho = Decimal(2.675) # Decimal('2.67499999999999982236431...')
# Округление до копеек с привычной логикой (2.675 -> 2.68)
itog = cena.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(itog) # 2.68
Метод quantize задаёт число знаков через образец (Decimal('0.01') — два знака), а rounding — режим. ROUND_HALF_UP округляет 0.5 вверх; ROUND_HALF_EVEN (по умолчанию) — к чётному. Оба режима импортируются из decimal.
Ошибка 3. Ручная вставка разделителей разрядов
Разбивать большое число на разряды регулярками или срезами строки не нужно — спецификатор формата умеет это сам.
>>> f'{1234567:,}'
'1,234,567'
>>> f'{1234567.5:,.2f}'
'1,234,567.50'
>>> f'{1234567:_}'
'1_234_567'
Запятая и подчёркивание в спецификаторе — встроенные разделители групп. Проблема в том, что запятая как разделитель тысяч и точка как десятичный — англоязычный формат. В России разряды принято разделять пробелом, а дробную часть — запятой. Через голый спецификатор так напрямую не получить: сначала форматируем, потом заменяем символы.
value = 1234567.5
# Форматируем по-английски, затем меняем разделители
s = f'{value:,.2f}' # '1,234,567.50'
ru = s.replace(',', ' ').replace('.', ',')
print(ru) # '1 234 567,50'
Замена через промежуточный символ безопасна, потому что порядок фиксирован: сначала запятые становятся пробелами, потом точка — запятой. Если данных много и нужна корректная локаль, смотрите в сторону модуля locale или babel — но у locale есть подвох: он глобальный для всего процесса, что опасно в многопоточном коде.
Ошибка 4. Путаница в самом спецификаторе
Синтаксис мини-языка форматов читается непривычно. Разберём поля на примере f'{x:>10.2f}':
| Часть | Что задаёт | Пример |
|---|---|---|
> | выравнивание (вправо) | f'{5:>4}' → ' 5' |
0 | заполнение нулями | f'{5:04}' → '0005' |
, или _ | разделитель разрядов | f'{5000:,}' → '5,000' |
.2 | знаков после точки | f'{3.14159:.2f}' → '3.14' |
f | fixed-point (десятичный) | f'{1000000.0:f}' → '1000000.000000' |
% | проценты (умножит на 100) | f'{0.256:.1%}' → '25.6%' |
e | научная нотация | f'{1000000.0:.2e}' → '1.00e+06' |
Две частые путаницы. Первая: .2 без буквы типа для float означает значащие цифры, а не знаки после точки. f'{123.456:.2}' даёт '1.2e+02', а f'{123.456:.2f}' — '123.46'. Всегда указывайте f, если хотите фиксированное число знаков после точки.
Вторая: спецификатор % умножает число на 100. Если у вас уже 25.6, а не 0.256, применение :.1% даст '2560.0%'.
Ошибка 5. Сравнение float на равенство
Прямое сравнение результата вычислений — источник багов, которые проявляются не всегда.
>>> 0.1 + 0.2 == 0.3
False
Для сравнения с допуском в стандартной библиотеке есть math.isclose (Python 3.5+):
import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
# С абсолютным допуском, если сравниваете с нулём
>>> math.isclose(1e-12, 0.0, abs_tol=1e-9)
True
Для значений около нуля задавайте abs_tol: относительный допуск по умолчанию рядом с нулём не работает, потому что считается в долях от величины.
Что делать дальше
Проверьте свой код по трём точкам. Первое: везде, где считаются деньги, замените float на Decimal из строки — это самое частое место утечки точности. Второе: уберите round(), который стоит только ради вывода, и перенесите округление в f-строку. Третье: найдите сравнения float через == и замените на math.isclose.
Если работаете с массивами чисел — форматирование в NumPy устроено иначе (через np.set_printoptions и собственные dtype), это отдельная тема. Полный справочник по спецификаторам — раздел Format Specification Mini-Language в документации Python.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Format Specification Mini-Language — Python documentation, decimal — Decimal fixed-point and floating-point arithmetic
Частые вопросы
Почему round(2.675, 2) возвращает 2.67, а не 2.68?
У меня Python 3.6, а не 3.12 — примеры сработают?
Не сработало: Decimal('2.675') всё равно даёт странное число
Как вывести число в русском формате: пробел между разрядами и запятая для копеек?
Почему f'{123.456:.2}' даёт '1.2e+02' вместо '123.46'?
Можно ли вообще не использовать float и всё считать в Decimal?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.