Round Numbers for Excel Output: float_format vs Number Formats
A report needs amounts with two decimals. There are three obvious ways to get there from pandas, and it is easy to assume they are interchangeable:
df.to_excel(path, float_format="%.2f") # an argument to to_excel
df.round(2).to_excel(path) # round the data first
cell.number_format = "0.00" # format the cells
They are not. Two of them change the stored value and one changes only the display; two of them round the same halves in opposite directions; and only one of them makes Excel show 2.50 rather than 2.5. Choosing the wrong one produces reports whose totals are off by a cent, or whose columns look ragged. This page, part of Writing DataFrames to Excel with Pandas, compares them on the same numbers.
Prerequisites
pip install pandas openpyxl
from decimal import Decimal, ROUND_HALF_UP
import pandas as pd
from openpyxl import load_workbook
amounts = pd.DataFrame({"amount": [0.12345, 2.5, 1234567.891, 0.005, 0.015, 1.005]})
def stored(path):
ws = load_workbook(path).active
return [(c.value, c.number_format) for (c,) in ws.iter_rows(min_row=2, max_col=1)]
Step 1 — float_format rounds the stored value
float_format takes a %-style pattern. pandas formats each float with it and converts the result back to a number, so the file holds rounded numbers, not text:
amounts.to_excel("float_format.xlsx", index=False, float_format="%.2f")
print(stored("float_format.xlsx"))
# [(0.12, 'General'), (2.5, 'General'), (1234567.89, 'General'), (0.01, 'General'), (0.01, 'General'), (1, 'General')]
Two things stand out. The number format is still General, so Excel displays 2.5 and 1 rather than 2.50 and 1.00 — float_format cannot add trailing zeros because it produces a number, and numbers do not carry formatting. And the halves round unevenly: 0.005 became 0.01, 0.015 also became 0.01, and 1.005 became 1.00. That is Python's % formatting working on the exact binary values, which are slightly above or below the decimal you typed — 0.015 is stored as 0.01499999…, so it rounds down.
float_format affects only float columns; integers and nullable Float64 columns are written as they are.
Step 2 — DataFrame.round rounds differently
Rounding the data before writing gives rounded numbers too, but with a different rule:
amounts.round(2).to_excel("rounded.xlsx", index=False)
print(stored("rounded.xlsx"))
# [(0.12, 'General'), (2.5, 'General'), (1234567.89, 'General'), (0, 'General'), (0.02, 'General'), (1, 'General')]
0.005 is now 0.0 and 0.015 is 0.02 — the opposite of float_format for both. DataFrame.round uses NumPy, which scales the value, rounds half to even and scales back, so its answers for near-halves depend on a different chain of floating-point steps. Neither method is wrong; they are different rules, and a report that mixes them (rounded in one script, float_format in another) will disagree with itself.
The third column comes from recalculating =ROUND(A1, 2) in LibreOffice; Excel documents the same rule, rounding halves away from zero as the number appears in decimal. Neither pandas method matches it for every value.
Step 3 — A number format changes only the display
Setting a number format leaves the value alone and tells Excel how to show it. It is the only way to get a fixed number of decimals on screen:
amounts.to_excel("formatted.xlsx", index=False)
wb = load_workbook("formatted.xlsx")
ws = wb.active
for (cell,) in ws.iter_rows(min_row=2, max_col=1):
cell.number_format = "#,##0.00"
wb.save("formatted.xlsx")
print(stored("formatted.xlsx")[:3])
# [(0.12345, '#,##0.00'), (2.5, '#,##0.00'), (1234567.891, '#,##0.00')]
Excel shows 0.12, 2.50 and 1,234,567.89, and the cells still hold 0.12345, 2.5 and 1234567.891. That matters in two directions. Readers who build formulas on the sheet get full precision, which is usually right. But a column of displayed amounts can appear not to add up: three cells showing 0.33 sum to a total cell showing 1.00, because the underlying values are thirds. When the printed numbers must add up exactly, round the values as well as formatting them.
Step 4 — Round like a spreadsheet when totals must match
If figures must agree with an accounting system or with a colleague's ROUND formulas, round with the spreadsheet's rule. Decimal built from the value's shortest decimal representation, with ROUND_HALF_UP, gives the same answers as the ROUND column above:
def round_like_excel(value, places=2):
if pd.isna(value):
return value
step = Decimal(1).scaleb(-places)
return float(Decimal(str(value)).quantize(step, rounding=ROUND_HALF_UP))
amounts["excel_round"] = amounts["amount"].map(round_like_excel)
print(amounts["excel_round"].tolist())
# [0.12, 2.5, 1234567.89, 0.01, 0.02, 1.01]
str(value) is the key step: it gives the short decimal a person would read ('1.005'), not the binary value underneath, so halves are treated as halves. ROUND_HALF_UP in decimal rounds away from zero for negatives too, which matches Excel. Match Excel Rounding in pandas covers ROUNDUP, ROUNDDOWN and MROUND as well.
Step 5 — Combine rounding with a format
The dependable pattern for a money column is both: round the values with the rule the business uses, then apply a number format so every cell shows the same number of decimals:
report = pd.DataFrame({"item": ["A", "B", "C"], "amount": [10.005, 2.5, 7.125]})
report["amount"] = report["amount"].map(round_like_excel)
report.to_excel("report.xlsx", index=False)
wb = load_workbook("report.xlsx")
ws = wb.active
for (cell,) in ws.iter_rows(min_row=2, min_col=2, max_col=2):
cell.number_format = "#,##0.00"
wb.save("report.xlsx")
print([(c.value, c.number_format) for (c,) in ws.iter_rows(min_row=2, min_col=2, max_col=2)])
The stored values are exactly what the report claims, and the display is consistent. Format Excel Cells as Currency with Python extends this to currency symbols and negative numbers in brackets.
Step 6 — Check that rounded lines still add up
Rounding each line and rounding the total are different operations, and on real data they disagree by a cent or two. Three equal shares of 100 show the problem at its smallest:
shares = pd.Series([100 / 3] * 3)
lines = shares.map(round_like_excel)
print(lines.tolist(), round(lines.sum(), 2), round_like_excel(shares.sum()))
# [33.33, 33.33, 33.33] 99.99 100.0
A report that prints the three lines and a total of 100.00 looks wrong to anyone who adds them up; one that prints 99.99 disagrees with the source. Decide which figure is authoritative. If the lines must sum to the total — an allocation, an invoice split — give the rounding difference to one line, usually the largest, and assert the result:
target = round_like_excel(shares.sum())
i = lines.idxmax()
lines.iloc[i] = round_like_excel(lines.iloc[i] + (target - lines.sum()))
assert round(lines.sum(), 2) == target
print(lines.tolist())
# [33.34, 33.33, 33.33]
Re-rounding the adjusted line matters: adding a difference of 0.01 in floating point gives 33.339999999999996, the very noise this page is about. Writing the assertion into the job means a change in the data or the rounding rule that breaks the totals stops the run, instead of reaching a reader.
Which to use when
- Analysis workbooks others will calculate with: keep full precision and use a number format only.
- Printed or emailed figures that must add up: round the values (Step 4) and format them (Step 5).
- Quick exports where exact halves do not matter:
df.round(2)orfloat_formatare fine — pick one and use it everywhere. - CSV output:
float_formatis the right tool there, because a CSV has no number formats;to_csv(float_format="%.2f")writes text with exactly two decimals.
Common pitfalls
- Expecting
float_formatto show trailing zeros. It writes numbers; use a number format. - Formatting and assuming the value is rounded. The display says 0.12; the cell holds 0.12345.
- Mixing rounding methods across scripts. Pick one rule per report.
- Rounding before aggregating when the business rule says otherwise. Sum-then-round and round-then-sum differ; the choice is an accounting policy, not a style preference.
Performance notes
df.round and float_format are vectorised or applied during writing and cost nothing noticeable. The Decimal approach calls Python once per value — around a microsecond each — which is fine for report-sized data and worth replacing with vectorised rounding only for exports of many millions of values. Setting number formats with openpyxl is a per-cell pass; with the xlsxwriter engine you can instead set a column format once with worksheet.set_column.
Conclusion
float_format and df.round both change the stored values, with different rules for halves, and neither makes Excel show a fixed number of decimals. A number format fixes the display and leaves the value untouched. For money in reports, round the values with the rule your business uses — Decimal(str(x)) with ROUND_HALF_UP to match a spreadsheet's ROUND — and then apply a #,##0.00 format.
Frequently asked questions
Does float_format write text or numbers? Numbers. pandas formats each float with the pattern, then converts the result back to a float, so the cell holds the rounded number with the General format. Excel will still display 2.5 rather than 2.50.
Why do float_format and DataFrame.round give different results? float_format uses Python string formatting on the binary value, while DataFrame.round uses NumPy's round-half-to-even on a scaled value. For numbers near a half, such as 0.005 and 0.015, the two can land on different sides.
How do I show exactly two decimals, like 2.50? Apply a number format such as 0.00 or #,##0.00 to the cells. Only the format controls how many decimals are displayed; rounding the value cannot add a trailing zero.
How do I round the way Excel's ROUND does? Excel rounds halves away from zero based on the decimal value you see. In Python, convert each value with Decimal(str(value)) and quantize with ROUND_HALF_UP.
Related
- Up one level: Writing DataFrames to Excel with Pandas — all the to_excel options.
- Fix Floating-Point Noise Like 0.1 + 0.2 ≠ 0.3 in Excel Output — why the halves are not exact.
- Write Custom Number Format Codes in Excel with Python — building format strings.
- Format Numbers in Thousands and Millions in Excel with Python — scaling the display without changing values.
More on writing excel files
- How-toWrite Multiple DataFrames to One Excel File
- How-toWrite a Formatted Excel Report with xlsxwriter
- How-toFind and Replace Text in Excel with openpyxl
- How-toInsert and Delete Rows and Columns with openpyxl
- How-toopenpyxl: Append Data to an Existing Excel Sheet
- How-toReorder Columns to Match a Template Layout with pandas