How-to
Formatting And Charting Excel Reports With PythonDeep dive

Protect Workbook Structure with openpyxl

Stop readers adding, deleting, renaming, moving, hiding or unhiding sheets in a generated report by locking the workbook structure with openpyxl — combined with very hidden lookup sheets and sheet protection — and understand how weak the stored password is and what the lock does not cover.

A report workbook often has a layout that matters: a summary sheet first, a data sheet the formulas depend on, a lookup table that should stay out of sight. Readers will rename "Data" to "Data (old)", drag sheets into a different order, or unhide the lookup and edit it — and the next run of the report breaks. Protecting the workbook structure stops those changes in Excel without restricting what people can do inside the sheets. This guide, part of Protecting and Sharing Excel Workbooks, applies structure protection with openpyxl, combines it with very hidden sheets and sheet protection, and is honest about how little the password protects.

Three kinds of protection and what each blocks Workbook structure protection blocks adding, deleting, renaming, moving and unhiding sheets but not editing cells. Sheet protection blocks editing locked cells but not changing sheets. A password to open encrypts the file so it cannot be read at all without the password. Edit cells Change sheets Read the file Structure (wb.security) allowed blocked allowed Sheet (ws.protection) blocked allowed allowed Password to open blocked blocked blocked

Prerequisites

Bash
pip install openpyxl

A report with a summary, a data sheet and a lookup table:

Python
import re
import zipfile

from openpyxl import Workbook, load_workbook
from openpyxl.workbook.protection import WorkbookProtection

wb = Workbook()
summary = wb.active
summary.title = "Summary"
summary["A1"] = "Total sales"
summary["B1"] = "=SUM(Data!B2:B4)"
data = wb.create_sheet("Data")
for row in [("Region", "Sales"), ("North", 120), ("South", 95), ("East", 60)]:
    data.append(row)
lookup = wb.create_sheet("Lookup")
lookup.append(["Code", "Region"])
lookup.append(["N", "North"])

Step 1 — Lock the structure

Assign a WorkbookProtection to wb.security with lockStructure=True. The password is what a reader must enter in Excel to unprotect the structure:

Python
wb.security = WorkbookProtection(workbookPassword="change-me", lockStructure=True)
wb.save("report.xlsx")

xml = zipfile.ZipFile("report.xlsx").read("xl/workbook.xml").decode()
print(re.search(r"<workbookProtection[^>]*>", xml).group(0))
# <workbookProtection workbookPassword="…" lockStructure="1"/>

In Excel, the sheet tabs' context menu now has Insert, Delete, Rename, Move or Copy, Hide and Unhide greyed out until someone chooses Review → Protect Workbook and enters the password. Everything inside the sheets remains editable; that is a separate protection, covered in Step 3. The password is optional: WorkbookProtection(lockStructure=True) without one still blocks accidental changes, and anyone who deliberately wants to restructure the workbook can switch the protection off from the Review tab without asking. For many internal reports that is exactly the right level — a reminder, not a gate.

Step 2 — Hide the helper sheets properly

Structure protection is what makes hidden sheets stay hidden: without it, anyone can right-click a tab and unhide. openpyxl offers two levels. "hidden" sheets appear in Excel's Unhide dialog; "veryHidden" sheets do not appear anywhere in the interface and can only be revealed through the VBA editor or code:

Python
wb = load_workbook("report.xlsx")
wb["Lookup"].sheet_state = "veryHidden"
wb.save("report.xlsx")
print([(ws.title, ws.sheet_state) for ws in load_workbook("report.xlsx").worksheets])
# [('Summary', 'visible'), ('Data', 'visible'), ('Lookup', 'veryHidden')]

Formulas can still refer to a very hidden sheet — =VLOOKUP(A2,Lookup!A:B,2,FALSE) works normally — so it suits lookup tables, settings and intermediate calculations. Remember that hidden is not secret: anyone who opens the file with Python sees every sheet. Hiding sheets, rows and columns in general is covered in Hide Sheets, Rows and Columns with openpyxl.

Layers of a protected report The outer layer is workbook structure protection, which keeps sheets in place and hidden sheets hidden. Inside it, the Summary sheet has sheet protection so its formulas cannot be overwritten, the Data sheet is left editable, and the Lookup sheet is very hidden. Workbook structure locked no add, delete, rename, move, unhide Summary: sheet protection formulas cannot be overwritten Data: editable readers correct figures here Lookup: veryHidden invisible in Excel's interface

Step 3 — Combine with sheet protection

Structure protection keeps sheets in place; sheet protection keeps formulas intact. A typical report uses both — locked structure, a protected summary, and an editable data sheet:

Python
wb = load_workbook("report.xlsx")
summary = wb["Summary"]
summary.protection.sheet = True
summary.protection.password = "change-me"
wb.security = WorkbookProtection(workbookPassword="change-me", lockStructure=True)
wb.save("report.xlsx")

check = load_workbook("report.xlsx")
print(check.security.lockStructure, check["Summary"].protection.sheet, check["Data"].protection.sheet)
# True True False

Loading a workbook with openpyxl keeps its protection settings, but assigning wb.security again after loading is a good habit: it makes the script's intent explicit and repairs files where someone removed the protection. Locking individual cells while leaving input cells editable is covered in Lock Cells and Protect a Sheet with openpyxl.

Step 4 — Know what the password is worth

openpyxl stores the password the way old versions of Excel did: as a 16-bit hash, four hexadecimal digits in the file. There are only 65,536 possible values, so a matching password can be found almost instantly, and many different passwords unlock the same file:

Python
protection = WorkbookProtection(workbookPassword="change-me")
print(protection.workbookPassword, len(protection.workbookPassword))     # a 4-digit hex hash, 4
print(16 ** 4)                                                           # 65536

Current Excel versions write a salted SHA-512 hash instead when a person sets the password; openpyxl reads and preserves those, but writes the legacy form. Either way, structure and sheet protection are guard rails against accidental changes by colleagues, not security. For confidentiality, encrypt the file with a password to open, as described in Password Protect an Excel File with Python, and control who receives it.

Step 5 — Your scripts are not blocked

openpyxl does not enforce protection. A script can add, rename and delete sheets in a structure-locked workbook without the password:

Python
wb = load_workbook("report.xlsx")
wb.create_sheet("Archive")
wb.save("report_archived.xlsx")
reloaded = load_workbook("report_archived.xlsx")
print(reloaded.sheetnames, reloaded.security.lockStructure)
# ['Summary', 'Data', 'Lookup', 'Archive'] True

That is convenient — the report job can maintain its own workbook — and it is a reminder of what protection is: an instruction to Excel's user interface, not a property of the data. The same applies to other tools: a pandas read, a database import or a conversion to CSV all see every sheet and every cell regardless of the lock. The lock is still in place afterwards, so readers remain unable to make the same change in Excel.

What structure protection does and does not do Structure protection keeps sheets from being added, removed, renamed, reordered or unhidden in Excel and keeps very hidden sheets out of sight. It does not stop cell edits, does not stop scripts, does not encrypt the file and its legacy password hash is easy to match. Protects against renamed or deleted sheets reordered tabs unhidden helper sheets accidental structural edits Does not protect against edits inside cells scripts and other tools anyone reading the file a determined user

Step 6 — Audit protection across your reports

When a team produces dozens of workbooks, protection settings drift: one job forgets to lock the structure, another leaves a lookup sheet merely hidden. A short audit over the output folder lists what each file actually contains:

Python
from pathlib import Path

def protection_report(folder):
    rows = []
    for path in sorted(Path(folder).glob("*.xlsx")):
        wb = load_workbook(path)
        security = wb.security
        rows.append({
            "file": path.name,
            "structure_locked": bool(security and security.lockStructure),
            "protected_sheets": [ws.title for ws in wb.worksheets if ws.protection.sheet],
            "hidden_sheets": [f"{ws.title} ({ws.sheet_state})" for ws in wb.worksheets
                              if ws.sheet_state != "visible"],
        })
    return rows

for row in protection_report("."):
    if row["file"].startswith("report"):
        print(row)

Run it after the report jobs, or as part of their tests, and compare the result with what each report is supposed to have. A dictionary of expected settings per report, checked with a simple equality test, turns a silent regression into a failing build. The audit reads only settings, so it works on files you did not create too — though for files that someone has encrypted with a password to open, openpyxl cannot read them at all and the audit should record that instead of failing.

Two other workbook settings sit beside lockStructure. lockWindows once locked the size and position of workbook windows; recent Windows versions of Excel no longer offer that option, so leave it unset. lockRevision relates to Excel's legacy shared-workbook change tracking and is rarely wanted in generated reports.

Common pitfalls

  • Expecting cell protection. Structure protection leaves every cell editable. Add sheet protection for formulas.
  • Forgetting the password. Keep the password with the report's code or configuration; the person who needs to change the structure next year is probably you.
  • Very hidden sheets with data people need. A reader cannot unhide a very hidden sheet even with the password to the structure; they need the VBA editor. Use it only for sheets nobody outside the process should see.
  • Protection on a template, not the output. If the job copies a protected template and pandas rewrites sheets, check that protection survives the write — the audit in Step 6 does exactly that.
  • Hard-coding the password in public code. It is weak anyway, but there is no reason to publish it. Read it from configuration.

Conclusion

Workbook structure protection keeps a report's sheets where they belong: assign WorkbookProtection(workbookPassword=…, lockStructure=True) to wb.security, set helper sheets to "veryHidden", and add sheet protection where formulas must not be overwritten. Treat the password as a speed bump — openpyxl writes a 16-bit legacy hash — and use encryption when the content itself must be kept private. Your own scripts can still restructure the file whenever they need to.

Frequently asked questions

What does protecting workbook structure do? In Excel it prevents adding, deleting, renaming, moving, copying, hiding and unhiding sheets. It does not stop anyone editing cells, and it does not encrypt the file — for that you need sheet protection and a password to open respectively.

How do I protect the workbook structure with openpyxl? Assign wb.security = WorkbookProtection(workbookPassword="…", lockStructure=True) before saving. Excel then asks for the password before allowing any change to the sheets.

Is the openpyxl workbook password secure? No. openpyxl stores Excel's legacy 16-bit password hash, which has only 65,536 possible values and can be matched almost instantly. Treat structure protection as a guard against accidents, not as security.

Can I still change a protected workbook with Python? Yes. openpyxl does not enforce the lock, so scripts can add or remove sheets freely. Only applications that honour the protection, such as Excel, prevent the changes.

More on protection & sharing

More on protection & sharing →