How-to
Formatting And Charting Excel Reports With PythonDeep dive

Remove Sheet Protection from a Workbook You Own with openpyxl

Update your own protected report templates from Python — turn off sheet and workbook structure protection, make changes, and put the original protection back exactly as it was, including Excel's SHA-512 password hashes. Covers what openpyxl keeps, what "unprotecting" does and does not change, and batch maintenance of many files.

Protected templates are common in reporting: the budget template with locked formulas, the monthly pack whose structure is locked so nobody renames the data sheet. Sooner or later the team that owns those files needs to change them — a new column, a corrected formula, a sheet added to fifty regional copies — and unprotecting each file by hand in Excel is slow and error-prone. openpyxl can turn protection off, make the change and put the protection back exactly as it was. This guide, part of Protecting and Sharing Excel Workbooks, shows how, for workbooks you own or are responsible for maintaining.

Maintaining a protected workbook without losing its protection Load the workbook, copy each sheet's protection and the workbook protection, switch protection off, make the change, assign the saved protection objects back and save. The stored password hashes are written back unchanged, so the original passwords still work. Load load_workbook Save settings copy(ws.protection) Unprotect SheetProtection() Change cells, sheets Restore same hash back

Prerequisites

Bash
pip install openpyxl

A protected template to work on — a budget sheet with protection, a locked structure and a formula that needs fixing:

Python
import re
import zipfile
from copy import copy

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

wb = Workbook()
ws = wb.active
ws.title = "Budget"
ws.append(["Item", "Amount"])
ws.append(["Rent", 1200])
ws.append(["Staff", 5400])
ws["B4"] = "=SUM(B2:B2)"                         # the bug: should include B3
ws.protection.sheet = True
ws.protection.password = "team-password"
ws.protection.formatColumns = False              # readers may still resize columns
wb.security = WorkbookProtection(workbookPassword="team-password", lockStructure=True)
wb.save("budget_template.xlsx")

Step 1 — See what protection a workbook has

Before changing anything, list the protection on each sheet and on the workbook. openpyxl reads both the legacy password hashes it writes itself and the salted SHA-512 hashes current Excel versions write:

Python
def describe_protection(path):
    wb = load_workbook(path)
    lines = [f"structure locked: {bool(wb.security and wb.security.lockStructure)}"]
    for sheet in wb.worksheets:
        p = sheet.protection
        kind = p.algorithmName or ("legacy hash" if p.password else "no password")
        lines.append(f"{sheet.title}: protected={bool(p.sheet)} ({kind})")
    return lines

print(describe_protection("budget_template.xlsx"))
# ['structure locked: True', 'Budget: protected=True (legacy hash)']

Knowing which kind of protection is in place tells you what you are about to change and what you must put back. A sheet marked protected=False with a password is unusual but possible — someone unprotected it in Excel without removing the password setting.

Step 2 — Keep a copy, then unprotect

Copy the protection objects before touching them. Then replace each sheet's protection with a fresh, empty SheetProtection and clear the workbook's structure lock:

Python
wb = load_workbook("budget_template.xlsx")
saved_sheets = {sheet.title: copy(sheet.protection) for sheet in wb.worksheets}
saved_workbook = copy(wb.security)

for sheet in wb.worksheets:
    sheet.protection = SheetProtection()
wb.security = None

A fresh SheetProtection() is off and has no password, so nothing is written to the sheet at all. Setting only sheet.protection.sheet = False also works, but leaves the password hash and permission flags in place — harmless, but confusing for the next person who inspects the file. Cells keep their locked and hidden flags either way; those take effect only while a sheet is protected. The hidden flag is the one to remember: on a protected sheet it hides a cell's formula from the formula bar, so while protection is off — and in any unprotected copy you save along the way — those formulas are visible to anyone who opens the file.

Two ways to switch sheet protection off Setting protection.sheet to False switches protection off but keeps the password hash and the permission flags in the file. Replacing the protection with a new SheetProtection object removes the whole setting, leaving a clean sheet. Both keep cell values and the locked flags of cells. protection.sheet = False one attribute password hash stays in the file permission flags stay cells untouched protection = SheetProtection() nothing written to the sheet clean for the next inspection copy it first to restore later cells untouched

Step 3 — Make the change and restore the protection

With protection off, edit as usual. Then assign the saved objects back. The stored hashes go back into the file unchanged, so the team's existing password keeps working in Excel — without the script ever needing to know it:

Python
wb["Budget"]["B4"] = "=SUM(B2:B3)"                # the fix
wb["Budget"].column_dimensions["B"].width = 14

for title, protection in saved_sheets.items():
    wb[title].protection = protection
wb.security = saved_workbook
wb.save("budget_template_fixed.xlsx")
print(describe_protection("budget_template_fixed.xlsx"))
# ['structure locked: True', 'Budget: protected=True (legacy hash)']

The same works for workbooks protected in a recent Excel version: the SHA-512 hash, salt and spin count are copied and written back as they were. Strictly, openpyxl does not enforce protection at all, so the edits would also succeed with protection left on; switching it off and back explicitly makes the script's intent clear, and the pattern also covers tools that do respect protection.

Step 4 — Maintain many copies at once

Protected templates tend to be copied — one per region, per client, per year. A loop applies the same fix to every file and records what it did:

Python
for region in ["North", "South"]:
    load_workbook("budget_template.xlsx").save(f"budget_{region}.xlsx")

def fix_budget(path):
    wb = load_workbook(path)
    sheets = {s.title: copy(s.protection) for s in wb.worksheets}
    structure = copy(wb.security)
    for s in wb.worksheets:
        s.protection = SheetProtection()
    wb.security = None
    changed = wb["Budget"]["B4"].value != "=SUM(B2:B3)"
    wb["Budget"]["B4"] = "=SUM(B2:B3)"
    for title, protection in sheets.items():
        wb[title].protection = protection
    wb.security = structure
    wb.save(path)
    return changed

print({region: fix_budget(f"budget_{region}.xlsx") for region in ["North", "South"]})
# {'North': True, 'South': True}

Reporting whether each file actually needed the change, rather than just rewriting it, makes the run safe to repeat — a second run reports False everywhere — and gives you a record of which copies were touched. Keep the originals until someone has opened a fixed copy in Excel and confirmed that the protection prompts as before, and write the fixed files to a separate folder first when the change is large.

Step 5 — Prove the protection came back unchanged

"The protection is restored" is a claim worth checking automatically, because a maintenance script that quietly drops a permission flag — say, the right to resize columns — annoys readers for months. Compare every protection attribute of the original file with the fixed one:

Python
def protection_state(path):
    wb = load_workbook(path)
    state = {"workbook": dict(wb.security) if wb.security else {}}
    for sheet in wb.worksheets:
        state[sheet.title] = dict(sheet.protection)
    return state

before = protection_state("budget_template.xlsx")
after = protection_state("budget_template_fixed.xlsx")
print(before == after)                                  # True
print(after["Budget"].get("formatColumns"))             # 0 — resizing still allowed

openpyxl's protection objects can be turned into dictionaries of their XML attributes, which makes the comparison a single equality test: the password hash, every permission flag and the workbook lock must all match. Run it for every file the maintenance loop touched, and fail the run on any difference. The formatColumns flag shows the kind of detail that survives this way — it was set when the template was created, and it is still there after the fix.

Encrypted files are a different thing

Everything above concerns sheet and structure protection, which are settings inside a readable file. A workbook saved with a password to open is encrypted; openpyxl cannot load it and there is nothing to switch off. Decrypting it requires the real password — see Open a Password-Protected Excel File with Python. And whichever kind of protection is involved, only change files you own or are responsible for; protection set by someone else is a request not to edit, and removing it without permission is not a technical question.

What to do depends on the kind of protection Sheet protection in your own workbook is switched off by resetting SheetProtection and restored from a copy. Workbook structure protection is cleared with wb.security set to None and restored the same way. A password to open needs the real password to decrypt. Protection on someone else's file is a request to ask its owner. What kind of protection, and whose file is it? sheet, your file reset and restore copy(ws.protection) structure, your file wb.security = None restore afterwards password to open decrypt needs the real password someone else's file ask the owner not a technical question

Common pitfalls

  • Forgetting to restore. A maintenance script that unprotects and saves ships unprotected templates. Restore in the same function, as above, so the two steps cannot be separated by a later edit to the script.
  • Restoring the wrong sheet's settings. Key saved settings by sheet title and restore by title; sheet order can change.
  • Saving the unprotected state by accident. If the script fails between unprotecting and restoring, a wb.save in an exception handler writes the unprotected file. Save only after restoring, and let failures leave the original untouched.
  • Assuming protection hides data. Unprotected or not, every value and formula is readable by any tool. Protection is about editing in Excel, not confidentiality.

Conclusion

To maintain protected workbooks you own, list their protection first, copy each sheet's protection and the workbook's security, replace them with an empty SheetProtection() and None, make the change, and assign the saved objects back. The stored password hashes — legacy or SHA-512 — are restored unchanged, so the existing passwords keep working. Encrypted files need their real password, and protection set by others is not yours to remove.

Frequently asked questions

How do I remove sheet protection with openpyxl? Replace the sheet's protection with a fresh SheetProtection() object, or set ws.protection.sheet = False, and save. For workbook structure protection, set wb.security = None. Only do this on workbooks you own or are authorised to maintain.

Can I re-protect the sheet with the original password afterwards? Yes, without knowing the password. Copy the protection object before removing it and assign the copy back after your changes. The stored password hash — legacy or SHA-512 — is written back unchanged, so the original password still works in Excel.

Does removing protection change the cells? No. Cells keep their locked and hidden flags; those only take effect while the sheet is protected. Values, formulas and formatting are untouched.

Can openpyxl remove a password to open? No. A password to open encrypts the whole file; openpyxl cannot read it at all. You need the real password to decrypt it, for example with msoffcrypto-tool, before any Python library can work with the contents.

More on protection & sharing

More on protection & sharing →