Guide
Getting Started With Python Excel AutomationDeep dive

Copy a Formula Down a Column in openpyxl

Fill a formula down every data row from Python — generating references per row, translating an existing formula with openpyxl's Translator, and copying the source cell's style.

Excel users drag the fill handle; from Python there is no handle to drag. Copying a formula down a column means writing one formula per row, and there are two good ways to do it: generate each formula from the row number, or take one source formula and let openpyxl translate it. This guide, part of Working with Excel Formulas in Python, covers both, together with the styling and blank-row details that decide whether the result looks hand-made or generated.

One source formula becoming a per-row formula on every data row The source formula in D2 multiplies B2 by C2 and divides by an anchored cell dollar F dollar 1. Copied down, the relative row numbers become 3, 4 and 5 while the anchored reference stays dollar F dollar 1 on every row. D2: =B2*C2/$F$1 source formula D3: =B3*C3/$F$1 D4: =B4*C4/$F$1 D5: =B5*C5/$F$1 what moves B2, C2 → row of the target $F$1 → unchanged the anchors decide one formula per row, written explicitly no fill handle exists in the file format

Prerequisites

Bash
pip install openpyxl

Approach 1: generate a formula per row

The most transparent method. Loop over the data rows and build the string for each one:

Python
from openpyxl import Workbook

rows = [
    ("A-100", 4, 19.99),
    ("B-200", 2, 49.50),
    ("C-300", 7, 12.25),
    ("D-400", 5, 8.75),
]

wb = Workbook()
ws = wb.active
ws.title = "Orders"
ws.append(["SKU", "Quantity", "Unit_Price", "Line_Total", "Share"])

for sku, qty, price in rows:
    ws.append([sku, qty, price])

first, last = 2, ws.max_row
total_row = last + 2
ws[f"C{total_row}"] = "Total"
ws[f"D{total_row}"] = f"=SUM(D{first}:D{last})"

for r in range(first, last + 1):
    ws[f"D{r}"] = f"=B{r}*C{r}"
    ws[f"E{r}"] = f"=IF($D${total_row}=0,0,D{r}/$D${total_row})"
    ws[f"D{r}"].number_format = "#,##0.00"
    ws[f"E{r}"].number_format = "0.0%"

wb.save("filled_down.xlsx")
print(ws["E3"].value)      # '=IF($D$8=0,0,D3/$D$8)'

Everything the loop needs comes from first, last and total_row, so a longer dataset changes nothing but the numbers. The IF guard is worth the extra characters: without it, an empty dataset gives every row #DIV/0! and the report looks broken rather than empty.

Approach 2: translate an existing formula

When the source formula already exists — because you wrote it once, or because it came from a templateTranslator shifts it for you and gets the anchoring right without string surgery:

Python
from openpyxl import load_workbook
from openpyxl.formula.translate import Translator

wb = load_workbook("filled_down.xlsx")
ws = wb["Orders"]

source_coord = "D2"
source_formula = ws[source_coord].value        # '=B2*C2'

for r in range(3, 6):
    ws[f"D{r}"] = Translator(source_formula, origin=source_coord).translate_formula(f"D{r}")

print(ws["D5"].value)      # '=B5*C5'
wb.save("translated.xlsx")

Translator parses the formula, applies the row and column offset between origin and the destination to every relative reference, and leaves absolute parts untouched — exactly what Excel does on a copy-paste. It also handles the awkward cases string replacement gets wrong, such as a formula mentioning both B2 and AB2, or a sheet name that happens to contain something that looks like a reference.

The origin argument must be the coordinate the formula currently lives at. Passing the wrong origin produces a formula that is offset twice, which is a bug that survives review because the result still looks like a valid formula.

Choosing between generating and translating a formula Generate the formula per row when your own code owns the layout, because the pattern is visible in the loop. Translate an existing formula when the source lives in a template you did not write, because the template author owns the expression. generate per row you own the layout and the maths the pattern is readable in the loop easy to add guards such as IF() ws[f"D{r}"] = f"=B{r}*C{r}" translate an existing one the formula came from a template anchoring is preserved for you no fragile string replacement Translator(…).translate_formula()

Copy the source cell's formatting too

Assigning a value never carries style with it. If the source cell is currency-formatted and bold, the copies are not — which is usually the first thing a reader notices:

Python
from copy import copy
from openpyxl import load_workbook

wb = load_workbook("filled_down.xlsx")
ws = wb["Orders"]

source = ws["D2"]
for r in range(3, 6):
    target = ws[f"D{r}"]
    target._style = copy(source._style)      # font, fill, border, alignment, number format

wb.save("styled_fill.xlsx")

_style is a single object holding the whole style record, so copying it is both faster and more complete than setting font, fill, border and number_format one by one. Copy it — do not assign the same object to many cells — because openpyxl styles are shared immutable records and assigning without copy() can tie cells together in ways that surprise you later. When only the number format matters, setting target.number_format directly is simpler and clearer.

Only fill where there is data

Filling a formula down a fixed range leaves #VALUE! or zeros on empty rows below the data. Two defences, used together:

Python
from openpyxl import load_workbook

wb = load_workbook("filled_down.xlsx")
ws = wb["Orders"]

# 1. Derive the last data row from a column that is always populated
last_data = max(
    (cell.row for cell in ws["A"] if cell.value not in (None, "")),
    default=1,
)

# 2. Make each formula self-blanking, in case a row is cleared later
for r in range(2, last_data + 1):
    ws[f"D{r}"] = f'=IF(A{r}="","",B{r}*C{r})'

wb.save("safe_fill.xlsx")
print("filled rows 2 to", last_data)

Deriving last_data from the key column rather than ws.max_row matters on a sheet that has a total block or stray formatting at the bottom: max_row counts any row Excel considers used, including one that only holds a border, so it routinely overshoots the real data.

Filling across columns as well

The same two approaches extend to a rectangle. Translator shifts columns as readily as rows, which makes a monthly grid a two-line job:

Python
from openpyxl import Workbook
from openpyxl.formula.translate import Translator
from openpyxl.utils import get_column_letter

wb = Workbook()
ws = wb.active
ws.append(["Region", "Jan", "Feb", "Mar", "Q1 %"])
for region, jan, feb, mar in (("North", 100, 120, 140), ("South", 90, 95, 99)):
    ws.append([region, jan, feb, mar])

ws["E2"] = "=SUM(B2:D2)/SUM($B$2:$D$3)"
origin = "E2"
for r in range(3, ws.max_row + 1):
    ws[f"E{r}"] = Translator(ws[origin].value, origin=origin).translate_formula(f"E{r}")
    ws[f"E{r}"].number_format = "0.0%"

print(get_column_letter(5), ws["E3"].value)
wb.save("grid_fill.xlsx")

Note how the denominator stays $B$2:$D$3 on every row: it is anchored, so both the row fill and any later column fill leave it alone. Choosing the anchors carefully in the one source formula is what makes the rest of the grid correct for free.

Filling into an Excel table

If the destination range is a real Excel table, a formula written into one cell of a calculated column is expected to appear in every row of that column when a person edits it — but openpyxl writes only the cell you assign. Fill the whole column yourself, then extend the table reference so the new rows belong to it:

Python
from openpyxl import load_workbook

wb = load_workbook("orders_table.xlsx")
ws = wb.active
table = ws.tables["Orders"]

start, end = table.ref.split(":")
last_row = ws.max_row
table.ref = f"{start}:{end[0]}{last_row}"     # stretch to the new last row

for r in range(2, last_row + 1):
    ws[f"D{r}"] = f"=B{r}*C{r}"

wb.save("orders_table_filled.xlsx")
Rows written below a table's range are outside the table A table whose reference ends at row 4 keeps its banding and structured references for rows 2 to 4. Two rows written at 5 and 6 carry the formula but sit outside the range, so they are plain cells until table.ref is stretched to include them. table.ref = A1:D4 SKU Line_Total A-100 =B2*C2 B-200 =B3*C3 C-300 =B4*C4 D-400 =B5*C5 E-500 =B6*C6 inside the range banded, counted by Orders[Line_Total] outside the range formulas run, summaries ignore them

Stretching table.ref matters because Excel treats the table range as authoritative: rows outside it are ordinary cells, so they lose the banded formatting and are ignored by any structured reference such as Orders[Line_Total]. Writing the formulas and forgetting the range is the usual reason a filled column looks right but a summary elsewhere under-reports.

Common pitfalls and gotchas

SymptomCauseFix
Every copied row references the same cellsFormula string was reused without offsettingGenerate per row, or use Translator
Copies point one row too farWrong origin passed to Translatororigin must be where the formula currently lives
Copies are unstyledAssigning a value does not copy styleCopy _style, or set number_format per cell
Formulas run past the dataRange built from ws.max_rowDerive the last row from a key column
#DIV/0! down the columnDivisor is blank or zeroWrap in IF/IFERROR
Anchored cell moved anywayReference was written D14 instead of $D$14Add the dollar signs in the source formula

Performance and scale notes

Both approaches write one formula string per cell, so the cost is linear in rows. Translator parses each formula it shifts, which makes it measurably slower than an f-string in a tight loop — on 100,000 rows that difference is worth caring about, and generating the string yourself wins. The bigger scale question is whether the reader needs formulas at all: 100,000 live formulas make a workbook slow to open and recalculate, while computing the column in pandas and writing numbers keeps the file fast. Reserve the filled column for sheets where someone is expected to change an input.

Conclusion

There is no fill handle in the file format, so filling down is always "write one formula per row". Generate the strings when your code owns the layout, translate when the source formula belongs to a template, copy _style if the cells should look alike, and derive the last row from a column that always has data. Get the anchors right once and every copied row inherits correct references.

Frequently asked questions

Does openpyxl have a fill handle? No. Copying a formula down means writing one formula per row, either generated in a loop or produced from a source formula with the Translator class.

What does Translator actually change? It shifts relative references by the row and column offset between the origin cell and the destination, and leaves anchored parts such as $B$1 exactly as they are.

Does copying the formula copy the formatting too? No. Assigning a value never copies style. Use copy(source._style) or set number_format, font and fill explicitly on each destination cell.

How do I stop the formula appearing on blank rows? Only write it where the source data exists, or wrap the expression in IF(A2="","",…) so the cell shows nothing when the row is empty.

Up to the parent guide:

Related guides: