Числовые форматы в openpyxl: рубли, проценты и разделители
Строка number_format превращает голое 1234.5 в «1 234.50 ₽»: задаём рубли, проценты и разделители тысяч — и разбираемся, почему значение ячейки при этом не меняется.
Редакция Питоники
Прайс собран, шапка отформатирована, ширины выставлены — но колонка «Цена» выглядит бедно: голые 1234.5 и 250. Человек же ждёт 1 234.50 ₽. За показ числа отвечает не значение, а числовой формат — маска-строка, которую Excel применяет при отрисовке ячейки. В openpyxl за неё отвечает атрибут number_format, и в этом уроке мы научимся писать такие маски: рубли, проценты, штучные счётчики и разделители тысяч.
Все примеры — живые: openpyxl — чистый Python, и песочница ставит его автоматически при импорте. Жми «Запустить» и смотри на настоящие значения и форматы.
Что такое number_format: маска поверх значения
У каждой ячейки есть значение — то, что лежит в файле, — и формат — то, как это значение показывают. Формат это обычная строка с кодами: #,##0.00 значит «разделяй тысячи пробелами и показывай два знака после точки». По умолчанию у ячейки стоит формат General — «показывай как есть».
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws["A1"] = "Капучино"
ws["B1"] = 250 # число хранится как число
print("Значение:", ws["B1"].value)
print("Формат до:", ws["B1"].number_format) # General
ws["B1"].number_format = "#,##0.00 ₽" # маска: рубли с копейками
print("Формат после:", ws["B1"].number_format)
print("Значение не изменилось:", ws["B1"].value)
Значение: 250 Формат до: General Формат после: #,##0.00 ₽ Значение не изменилось: 250
Разбираем маску по кусочкам. # — цифра, если она есть; , — разделитель групп разрядов; 0.00 — обязательная цифра и два знака после точки, хоть значение целое; ₽ — литерал, который просто дописывается к числу. Итог для 250: 250,00 ₽. А для 1234.5 маска даст 1 234.50 ₽ — именно то, чего мы хотели от прайса.
Почему нельзя просто записать в ячейку строку "250,00 ₽" и не возиться с форматами? Строка обманет глаз ровно до первого вычисления: SUM по столбцу с текстом посчитает ноль, сортировка выстроит цены как слова, а фильтры предложат список из сотни уникальных строк. Число под маской — это единственный вариант, при котором прайс и красиво выглядит, и остаётся рабочей таблицей.
#— необязательная цифра: незначащие позиции в начале не рисуются;0— обязательная цифра: недостающие знаки дополняются нулями (0.00для 5 даст 5.00);,— делит целую часть на группы разрядов;.— отделяет дробную часть; сколько нулей после неё — столько знаков видно;- любой другой символ — литерал: ₽, шт, чел; процент
%особый — он ещё и умножает показ на 100.
| Маска | Что в ячейке | Что видит человек в Excel |
|---|---|---|
| General | 1234.5 | 1234.5 |
| #,##0.00 ₽ | 1234.5 | 1 234.50 ₽ |
| 0% | 0.15 | 15% |
| 0.0% | 0.1567 | 15.7% |
| #,##0 | 1234567 | 1 234 567 |
| #,##0 шт | 120 | 120 шт |
Формат ≠ значение: 0.15 выглядит как 15%
Самая коварная маска — процентная. Excel не хранит «пятнадцать процентов» как число 15: он хранит долю от единицы. Хочешь показать 15% — запиши 0.15 и поставь маску 0%. Формат не меняет значение ячейки: в ячейке по-прежнему 0.15, а человек видит 15%.
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws["A1"] = "Скидка"
ws["B1"] = 0.15 # доля: пятнадцать сотых
ws["B1"].number_format = "0%"
print("Значение:", ws["B1"].value)
print("Маска:", ws["B1"].number_format)
# в Excel человек увидит: 15%
Значение: 0.15 Маска: 0%
Почему это удобно: формулы и сортировки работают с настоящим значением. Скидка 15% от цены считается как цена * 0.15 — и маска 0% тут ни при чём, она только про картинку. Запишешь вместо доли число 15 — получишь показательные 1500%.
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws["A1"] = 0.1567
ws["A1"].number_format = "0%" # в Excel видно 16%
print("Значение под маской:", ws["A1"].value) # не округлилось!
ws["A2"] = round(0.1567, 2) # round() меняет само значение
print("После round:", ws["A2"].value)
Значение под маской: 0.1567 После round: 0.16
Разделитель тысяч и литералы в маске
На больших числах маска #,##0 окупается мгновенно: 42517 чашек за месяц глазами читаются как 42 517, а не как кашу из цифр. А литерал в конце — любой текст, который надо приставить к числу: ₽, шт, чел.
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws["A1"] = "Выручка за месяц"
ws["B1"] = 1234567.891
ws["B1"].number_format = "#,##0.00 ₽"
ws["A2"] = "Чашек продано"
ws["B2"] = 42517
ws["B2"].number_format = "#,##0"
print("B1:", ws["B1"].value, "->", ws["B1"].number_format)
print("B2:", ws["B2"].value, "->", ws["B2"].number_format)
# в Excel: 1 234 567.89 ₽ и 42 517
B1: 1234567.891 -> #,##0.00 ₽ B2: 42517 -> #,##0
У маски есть и вторая строка — для отрицательных чисел: секции разделяются точкой с запятой, и каждая описывает свой случай. Запись "#,##0.00 ₽;-#,##0.00 ₽" показывает долги и возвраты со знаком минус, а "#,##0.00 ₽;[Red]-#,##0.00 ₽" — ещё и красным цветом. openpyxl не разбирает маску по секциям — она сохраняет строку как есть, и Excel сам применит нужную секцию к каждому значению. Для убытков в отчёте это бесплатная читаемость.
Проценты в прайсе живут в связке с числами: скидка 0.15 лежит в своей ячейке под маской 0%, а цена со скидкой считается формулой =B2*(1-C2) — Excel перемножает настоящие доли, не глядя на то, что человек видит проценты. Ещё одна причина держать в ячейках значения, а не оформленный текст.
Как применить формат к целому столбцу прайса?
Формат — атрибут ячейки, а не столбца, поэтому на диапазон его навешивают циклом — ровно так же, как в уроке про шрифты и заливки мы раскладывали Font по ячейкам шапки. Пройдись iter_rows по нужным колонкам и поставь каждой ячейке свою маску.
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
data = [
["Товар", "Цена", "Скидка", "Остаток"],
["Капучино", 250, 0.1, 120],
["Латте", 280, 0.15, 80],
]
for row in data:
ws.append(row)
for row in ws.iter_rows(min_row=2):
row[1].number_format = "#,##0.00 ₽" # Цена
row[2].number_format = "0%" # Скидка
row[3].number_format = "#,##0 шт" # Остаток
for row in ws.iter_rows(min_row=2):
for cell in row[1:]:
print(cell.coordinate, "|", cell.value, "|", cell.number_format)
B2 | 250 | #,##0.00 ₽ C2 | 0.1 | 0% D2 | 120 | #,##0 шт B3 | 280 | #,##0.00 ₽ C3 | 0.15 | 0% D3 | 80 | #,##0 шт
Вот он, живой прогон «формат против значения»: в строке C2 значение 0.1, а увидит человек 10%. Печать показывает сырое содержимое файла — Excel покажет то же самое, но уже наряженное в маски. Обрати внимание на индексацию в row[1], row[2], row[3]: iter_rows отдаёт кортеж ячеек строки, и счёт в нём начинается с нуля, от колонки A. И маски едут в файл вместе с книгой:
from openpyxl import Workbook, load_workbook
import os
NAME = "price_format.xlsx"
wb = Workbook()
ws = wb.active
ws.append(["Товар", "Цена"])
ws.append(["Эспрессо", 120])
ws["B2"].number_format = "#,##0.00 ₽"
wb.save(NAME)
print("Файл создан:", os.path.exists(NAME))
wb2 = load_workbook(NAME)
print("Формат из файла:", wb2.active["B2"].number_format)
print("Значение из файла:", wb2.active["B2"].value)
os.remove(NAME)
print("Файл удалён:", os.path.exists(NAME))
Файл создан: True Формат из файла: #,##0.00 ₽ Значение из файла: 120 Файл удалён: False
Маска хранится в самом xlsx — это не property Python-объекта, а часть файла. Поэтому файл с форматами можно отправить кому угодно: Excel, Google Sheets и LibreOffice покажут 120,00 ₽ без единой строки кода с их стороны.
Когда формат можно не ставить вовсе? Если данные — служебные идентификаторы: артикулы, штрихкоды, номера строк отчёта. Общий формат покажет их как есть, и это честно. Правило хорошего прайса другое: у каждого числа, которое увидит человек, должна быть своя маска — цена в рублях, скидка в процентах, количество в штуках. Пустая ячейка без формата в оформленной таблице выглядит не экономией, а недоделкой.
Собираем прайс с форматами целиком
Финальный прогон: прайс кофейни с жирной шапкой из урока 5 и рублёвой колонкой цен. Два стилевых инструмента — шрифт и числовой формат — живут в одном файле и не мешают друг другу.
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font
import os
NAME = "pricelist_fmt.xlsx"
wb = Workbook()
ws = wb.active
ws.append(["Позиция", "Цена, шт"])
ws["A1"].font = Font(bold=True)
ws["B1"].font = Font(bold=True)
price = [("Эспрессо", 120), ("Капучино", 250), ("Латте", 280),
("Раф", 320), ("Какао", 260)]
for item, cost in price:
ws.append([item, cost])
for row in ws.iter_rows(min_row=2, min_col=2, max_col=2):
for cell in row:
cell.number_format = "#,##0.00 ₽"
wb.save(NAME)
print("Прайс сохранён:", os.path.exists(NAME))
wb2 = load_workbook(NAME)
print("Формат B6:", wb2.active["B6"].number_format)
print("Жирная шапка:", wb2.active["A1"].font.bold)
os.remove(NAME)
Прайс сохранён: True Формат B6: #,##0.00 ₽ Жирная шапка: True
ws["B2"] = 250
ws["B2"].number_format = "#,##0.00 ₽"
print(ws["B2"].value)
# то же самое в Excel: 250,00 ₽
Что дальше
Ты умеешь наряжать числа: рубли с копейками, проценты из долей, тысячи с пробелами. Следующий логичный гость прайса — даты: «действует с … по …» и сроки годности зёрен. В следующем уроке запишем в ячейку настоящий datetime, поставим маску DD.MM.YYYY и посчитаем, сколько дней между двумя датами. А если захочешь обрабатывать столбцы цен массово — загляни в groupby из Pandas: после Excel его сводные кажутся волшебством.
Числовой формат — это костюм на числе: значение под ним не меняется, меняется то, что видит человек.
Сначала предскажи ответ в голове — это главный навык программиста.
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws["A1"] = 0.25
ws["A1"].number_format = "0%"
print(ws["A1"].value)
print(ws["A1"].number_format)
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws["A1"] = 1234.5
print(ws["A1"].value)
ws["A1"].number_format = "#,##0.00 ₽"
print(ws["A1"].value)
print(ws["A1"].number_format)
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.append(["Товар", "Цена"])
ws.append(["Чай", 90])
ws["B2"].number_format = "#,##0 шт"
print(ws["A2"].value, ws["B2"].value, ws["B2"].number_format)
1. В ячейку записали 250 и поставили number_format "#,##0.00 ₽". Что лежит в ячейке?
2. Как записать в ячейку скидку 15%, чтобы Excel показал «15%»?
3. Что означает формат General?
4. Что сделает маска "#,##0" со значением 1234567?
5. Значение ячейки 0.1567, маска "0%" — в Excel видно «16%». Что вернёт cell.value?
В редакторе собран прайс из одной строки: Капучино, цена 250, скидка 0.15, остаток 120. Задай ячейкам B2, C2 и D2 числовые форматы: цене — «#,##0.00 ₽», скидке — «0%», остатку — «#,##0 шт». Затем выведи три строки: значение и маску каждой ячейки через разделитель « | ».
Как записать формат рублей в openpyxl?
Присвой ячейке числовой формат-строку: cell.number_format = "#,##0.00 ₽" — разделитель групп разрядов, два знака после точки и литерал рубля. В ячейку при этом записывают обычное число: 1234.5. Один и тот же формат подходит и для цен, и для выручек — сумма разрядов маске безразлична.
Почему в ячейке 0.15, а Excel показывает 15%?
Процентный формат Excel интерпретирует значение как долю от единицы: 0.15 — это 15 сотых, то есть 15%. Меняется только отображение: формулы и сортировки работают с 0.15. Записывать «15» не нужно — под маской «0%» оно показалось бы как 1500%.
Чем number_format отличается от round()?
round() меняет само значение ячейки: 0.1567 превращается в 0.16, и все расчёты дальше идут по 0.16. number_format меняет только количество отображаемых знаков: значение остаётся 0.1567, а видно 16%. Для цен и выручек формат обычно безопаснее — он не искажает данные.
Как узнать числовой формат ячейки готового файла?
Открой файл через load_workbook и прочитай cell.number_format — вернётся строка-маска, например «#,##0.00 ₽». В самом Excel тот же код виден через Ctrl+1 → «Все форматы». Удобный приём: подсмотреть формат в файле-эталоне и переиспользовать его в своём скрипте один в один.
Понравился урок? Сошлитесь на него
«Формат не меняет значение ячейки: в ячейке по-прежнему 0.15, а человек видит 15%.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
openpyxl · Урок 5
Форматирование ячеек: шрифт, заливка и границы в openpyxl
Шапка прайса — жирный белый на синем, границы на всю таблицу: Font, PatternFill и Border, и главное правило — один стиль на весь диапазон.
Pandas · Урок 8
Новые столбцы в Pandas: apply, map и векторные операции над колонками
Добавляем в DataFrame выручку, скидки и категории: векторные операции, np.where, map и apply — и разбираемся, почему apply по строкам почти всегда лишний.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
json · Урок 1
Что такое JSON и зачем он Python: первый json.dumps
Первое превращение словаря в json-строку одной командой: import json, json.dumps и честный взгляд на кракозябры в выводе — всё исполняется прямо на странице.
json для начинающихpython json формат
FastAPI · Урок 1
Что такое API и REST: введение в FastAPI для начинающих
Понять, что такое API, REST, HTTP-методы и JSON — и подготовиться к первому приложению на FastAPI.
rest api для начинающихfastapi для начинающих
openpyxl · Урок 3
Ячейки в openpyxl: адреса, значения и типы данных
Два способа попасть в ячейку: по адресу и по координатам. Числа против строк, ловушка «2026 как текст», очистка через None и чтение значений обратно.
openpyxlopenpyxl ячейки