Fixing Python openpyxl That Writes Data to the Wrong Sheet Tab

June 17, 2026 8 min read 1 views

Your openpyxl script finishes without a single error, but when you open the workbook, the data is sitting on the wrong sheet tab. Maybe it landed on Sheet instead of Sales_Q1, or it silently created a new tab called Sheet1 that you never intended. This is one of those bugs that hides in plain sight because Python never complains.

The fix is almost always a one-line change once you understand how openpyxl decides which sheet is "active". This guide walks through every common cause and the exact code to resolve each one.

What You'll Learn

  • How openpyxl's active sheet property works and why it misleads you
  • How to select a sheet by name reliably, with safe fallback handling
  • Why sheet name case sensitivity and whitespace cause silent failures
  • How to verify the right sheet before writing a single cell
  • How to avoid creating ghost sheets when create_sheet is called accidentally

Prerequisites

How openpyxl Tracks the Active Sheet

Every Excel workbook has one "active" sheet — the tab that was selected the last time the file was saved. openpyxl preserves this through the wb.active property. When you call wb.active, you get back whichever Worksheet object corresponds to that stored index, not necessarily the sheet you want to write to.

This matters because most tutorial examples look like this:

import openpyxl

wb = openpyxl.load_workbook("report.xlsx")
ws = wb.active          # danger: assumes the right sheet is active
ws["A1"] = "Revenue"
wb.save("report.xlsx")

If the file was last saved with a different tab selected, your data goes to that tab. No warning, no exception.

Root Cause 1: Relying on wb.active Without Checking Which Sheet Is Active

The quickest diagnostic is to print the sheet name before writing anything:

wb = openpyxl.load_workbook("report.xlsx")
print(wb.active.title)   # tells you exactly which sheet you're about to touch

If that prints something other than your target tab, you have the root cause confirmed. The fix is to select the sheet by name explicitly rather than relying on active:

wb = openpyxl.load_workbook("report.xlsx")
ws = wb["Sales_Q1"]     # select by exact tab name
ws["A1"] = "Revenue"
wb.save("report.xlsx")

You can also set wb.active by assigning the worksheet object back to it, but selecting by name is clearer and less fragile.

Root Cause 2: Sheet Name Typos and Case Sensitivity

openpyxl sheet names are case-sensitive. wb["sales_q1"] and wb["Sales_Q1"] are not the same thing. If the name doesn't match exactly, openpyxl raises a KeyError — but if you catch that error somewhere upstream and fall back to wb.active, you end up writing to the wrong place silently.

Trailing spaces are another silent killer. A sheet named "Summary " (with a trailing space) will never match a lookup for "Summary". Print the sheet names from the workbook itself to see what you're actually working with:

wb = openpyxl.load_workbook("report.xlsx")
print(wb.sheetnames)    # e.g. ['Summary ', 'Sales_Q1', 'Sheet']

If you see a name that looks right but has extra whitespace, strip it before comparing or rename the tab. A defensive lookup function helps here:

def get_sheet(wb, name):
    target = name.strip().lower()
    for sheet in wb.worksheets:
        if sheet.title.strip().lower() == target:
            return sheet
    raise ValueError(f"Sheet '{name}' not found. Available: {wb.sheetnames}")

ws = get_sheet(wb, "Sales_Q1")

This is especially useful when the workbook was created by someone else and the tab names weren't set programmatically.

Root Cause 3: Creating a New Sheet When You Meant to Open an Existing One

A very common mistake is calling wb.create_sheet() when the intent was to write to a sheet that already exists. create_sheet always adds a new tab; it does not return an existing one.

# Wrong — creates a NEW sheet named 'Sales_Q1' even if one already exists
ws = wb.create_sheet("Sales_Q1")

If a sheet named Sales_Q1 already exists, openpyxl will silently create Sales_Q11 (or append a number to make the name unique). Your data goes there, not to the original tab. Always use the dictionary-style lookup for existing sheets, and only use create_sheet when you actually intend to add a new tab:

# Correct — fetch existing sheet or create it if it genuinely doesn't exist
if "Sales_Q1" in wb.sheetnames:
    ws = wb["Sales_Q1"]
else:
    ws = wb.create_sheet("Sales_Q1")

This pattern is safe to call every time your script runs without accumulating ghost tabs in the workbook.

Root Cause 4: Writing to the Wrong Index When Iterating Sheets

Sometimes scripts iterate over worksheets by index. If the workbook's sheet order changes (someone added or reordered a tab manually), the index points at the wrong sheet:

# Fragile — breaks if tab order changes
ws = wb.worksheets[0]

Replace index-based access with name-based access everywhere you need a specific sheet. Reserve index-based access only for operations that genuinely need to process all sheets in order:

# Robust — always resolves the right tab regardless of order
ws = wb["Sales_Q1"]

If you're writing a script that processes multiple known sheets, loop over a list of names rather than indices:

sheet_names = ["Sales_Q1", "Sales_Q2", "Sales_Q3"]

for name in sheet_names:
    ws = wb[name]
    ws.append(["processed"])   # writes to the correct tab each time

Root Cause 5: load_workbook Options That Drop Sheets Silently

Two load_workbook keyword arguments can cause sheets to vanish or appear different from what you expect. The first is data_only=True: this strips cached formula results and can change how certain sheet structures are read. More importantly, if you're working with macro-enabled workbooks (.xlsm files), not passing keep_vba=True can cause openpyxl to drop or mis-index sheets that are tied to VBA modules.

# For macro-enabled workbooks (.xlsm)
wb = openpyxl.load_workbook("report.xlsm", keep_vba=True)

The second scenario: if you load the same file path twice in the same script without saving between loads, you're working with two separate in-memory workbook objects. Writes to the second object won't reflect what you wrote to the first. Consolidate your writes to a single load → modify → save cycle per file.

Verifying Your Target Sheet Before Writing

Build a small guard into any script that writes to a specific sheet. This catches mismatches before your data lands anywhere wrong:

import openpyxl

FILE = "report.xlsx"
TARGET_SHEET = "Sales_Q1"

wb = openpyxl.load_workbook(FILE)

if TARGET_SHEET not in wb.sheetnames:
    raise ValueError(
        f"Expected sheet '{TARGET_SHEET}' but found: {wb.sheetnames}"
    )

ws = wb[TARGET_SHEET]
assert ws.title == TARGET_SHEET, "Sheet mismatch — aborting"

# Safe to write now
ws.append(["2024-Q1", 48200, 51300])

wb.save(FILE)
print(f"Data written to '{ws.title}' successfully.")

The assert is redundant once you trust the lookup, but it acts as a sanity check during development. Remove it when you move to production or replace it with a proper exception.

If your script writes data produced by Pandas, the same name-verification logic applies before you pass the sheet to ExcelWriter. For background on how pandas and openpyxl interact when reading date-like columns, the article on fixing Pandas read_csv silently misreading date columns covers the underlying type-handling patterns that affect Excel round-trips too.

Common Pitfalls

Saving to a Different File Path

Calling wb.save("report_v2.xlsx") instead of wb.save("report.xlsx") means you're inspecting the original file while the changes went to the new one. Always confirm the save path matches what you're opening in Excel afterward.

Forgetting to Save After Writing

Changes made to the in-memory workbook object are lost if you forget wb.save(). If you think data went to the wrong sheet but actually no data was saved at all, this is the culprit. A try/finally block guarantees the save runs:

try:
    ws = wb["Sales_Q1"]
    ws.append(["2024-Q1", 48200])
finally:
    wb.save(FILE)

Confusing wb.active After You've Already Changed It

If you write wb.active = wb["Sales_Q1"] to switch the active sheet, and then later in the same script write ws = wb.active, you get whatever the active sheet is at that moment — which may have changed if another function touched it. Assign the worksheet to a named variable and use that variable consistently throughout the function scope.

Excel Formulas Cached on Load

If you load a workbook with data_only=True to read cached formula values, then write new data and save, Excel may display stale formula results in cells you didn't touch because the cache is now out of sync. Open the file and press Ctrl+Alt+F9 to force a full recalculation. This isn't a wrong-sheet bug but it looks like one. For a full breakdown of how openpyxl handles formula storage, see fixing openpyxl that reads formulas instead of values.

Multiple Scripts Touching the Same File Concurrently

If two processes load and save the same workbook file at overlapping times, one write will overwrite the other's changes. The second save wins, and whichever sheet was written by the first process appears empty. Use file locking or a task queue if you need concurrent writes.

Wrapping Up: Next Steps

The wrong-sheet bug almost always comes down to one of five things: trusting wb.active blindly, a name or case mismatch, accidentally calling create_sheet on an existing tab, fragile index-based sheet access, or load options that alter the workbook structure. Here are the concrete steps to apply right now:

  1. Print wb.sheetnames and wb.active.title at the top of any debugging session to confirm what openpyxl actually sees before you write anything.
  2. Replace every wb.active call with a named lookup like wb["Your Sheet Name"] unless you genuinely want whatever tab was last active.
  3. Guard against missing sheets with a if name not in wb.sheetnames check that raises a descriptive error rather than silently falling back.
  4. Use create_sheet only for new tabs; use dictionary-style access for existing ones. The pattern wb[name] if name in wb.sheetnames else wb.create_sheet(name) is safe for both cases.
  5. Add a post-write assertion during development — confirm ws.title == TARGET_SHEET before saving, then remove it when the script is stable.

Once your sheet targeting is solid, you can explore more advanced openpyxl patterns. If your workbook also uses Excel's lookup formulas, the guide on fixing XLOOKUP returning #N/A when the match exists covers the data-consistency issues that often appear alongside scripted writes.

Frequently Asked Questions

Why does openpyxl always write to Sheet instead of my named tab?

This happens because wb.active returns the sheet that was last selected when the file was saved, which may not be the tab you want. Fix it by selecting your sheet by name directly: ws = wb["Your Sheet Name"] instead of using wb.active.

How do I write to a specific sheet by name in openpyxl without creating a new one?

Use the dictionary-style lookup: ws = wb["SheetName"]. This returns the existing sheet object. Only call wb.create_sheet() when you want to add a brand-new tab — using it on an existing name creates a duplicate with a modified name.

Does openpyxl sheet name lookup care about capitalization?

Yes, openpyxl sheet name lookups are case-sensitive. wb["sales_q1"] and wb["Sales_Q1"] refer to different sheets. Always print wb.sheetnames to see the exact tab names as stored in the file, and match them character for character.

How can I check which sheets exist in a workbook before writing?

Call wb.sheetnames on your loaded workbook object to get an ordered list of all sheet tab names. You can then check if your target name is in that list before attempting to write, and raise a clear error if it's missing.

Can openpyxl create a duplicate sheet tab accidentally?

Yes — if you call wb.create_sheet("Sales_Q1") when a sheet by that name already exists, openpyxl creates a new tab named "Sales_Q11" or similar rather than returning the existing one. Always check sheetnames first and use the bracket lookup for existing tabs.

📤 Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

📬 Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.