How-to
Formatting And Charting Excel Reports With PythonDeep dive

Make an Excel File Open Read-Only Recommended

Stop colleagues from accidentally editing a published report: set the read-only recommended flag with xlsxwriter, add it to openpyxl workbooks with a small post-save patch, mark a workbook as final, keep the flag from being dropped when the file is re-saved, and understand what these settings do and do not protect.

A monthly report sits on a shared drive, and every month someone opens it, types a "quick what-if" into a cell, saves by reflex and changes the published numbers for everyone. Protecting the sheet with a password is heavy-handed for a report people are allowed to play with; what is wanted is a gentle nudge. Excel has two: read-only recommended, which makes Excel ask "open as read-only?" when the file is opened, and mark as final, which opens the file with editing turned off and a yellow bar explaining why. Both can be set from Python. Neither is security — anyone can click past them — and the page says exactly where each stops. It is part of Protecting and Sharing Excel Workbooks.

Prerequisites

Bash
pip install xlsxwriter openpyxl

The examples write small report workbooks and look inside the saved files to confirm the settings are there. Both settings are small XML elements in the .xlsx package, which makes them easy to check without Excel — a useful property, because the prompts themselves only appear in Excel.

Nudges and protections compared A comparison table. Read-only recommended prompts on open and can be declined. Mark as final opens with editing disabled until the user clicks Edit Anyway. Sheet protection blocks edits to locked cells until unprotected. A password to open encrypts the file. Prompts or blocks User can bypass Written by Python Read-only recommended prompt on open yes — one click xlsxwriter or patch Mark as final editing off on open yes — Edit Anyway xlsxwriter, openpyxl Sheet protection blocks locked cells with the password openpyxl Password to open cannot open msoffcrypto-tool

xlsxwriter has a method for it. Call it before closing the workbook:

Python
import xlsxwriter

wb = xlsxwriter.Workbook("monthly_report.xlsx")
ws = wb.add_worksheet("Summary")
ws.write_row(0, 0, ["Region", "Revenue"])
for i, (region, revenue) in enumerate([("North", 482100), ("South", 391250), ("East", 297800)], start=1):
    ws.write_row(i, 0, [region, revenue])
wb.read_only_recommended()
wb.close()

import re
import zipfile

def file_sharing(path):
    xml = zipfile.ZipFile(path).read("xl/workbook.xml").decode()
    return re.findall(r"<fileSharing[^>]*/>", xml)

print(file_sharing("monthly_report.xlsx"))

The method writes a <fileSharing readOnlyRecommended="1"/> element into the workbook part. When Excel opens the file, it asks whether to open it read-only; answering yes opens it with saving to the same name disabled, so edits can only be kept under a new name. Answering no opens it normally, with full editing, so the flag guards against habit rather than intent. The prompt appears every time, for everyone, until someone saves the file with the option switched off in Save As → Tools → General Options.

Step 2 — Add it to a file written by openpyxl

openpyxl knows the fileSharing element when it reads a file, but its writer never outputs it, so there is no attribute to set. The element is small and its place in the XML is fixed — right after <fileVersion> if there is one, otherwise first inside <workbook> — so a short post-save patch adds it:

Python
import shutil
from openpyxl import Workbook

def add_read_only_recommended(path):
    tmp = path + ".tmp"
    with zipfile.ZipFile(path) as src, zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as dst:
        for item in src.infolist():
            data = src.read(item.filename)
            if item.filename == "xl/workbook.xml" and b"<fileSharing" not in data:
                xml = data.decode("utf-8")
                tag = '<fileSharing readOnlyRecommended="1"/>'
                if "<fileVersion" in xml:
                    xml = re.sub(r"(<fileVersion[^>]*/>)", r"\1" + tag, xml, count=1)
                else:
                    xml = re.sub(r"(<workbook[^>]*>)", r"\1" + tag, xml, count=1)
                data = xml.encode("utf-8")
            dst.writestr(item, data)
    shutil.move(tmp, path)

wb = Workbook()
wb.active["A1"] = "Published figures — do not edit"
wb.save("report_openpyxl.xlsx")
add_read_only_recommended("report_openpyxl.xlsx")
print(file_sharing("report_openpyxl.xlsx"))
Where each setting lives in the .xlsx package Three layers of the zip package. The read-only recommended flag is an element in xl/workbook.xml, mark as final is a custom property in docProps/custom.xml, and sheet protection is an element in each worksheet's XML. xl/workbook.xml <fileSharing readOnlyRecommended="1"/> — dropped by openpyxl docProps/custom.xml _MarkAsFinal = true — kept by openpyxl xl/worksheets/sheetN.xml <sheetProtection …/> — written by openpyxl

The patch copies every part of the package unchanged except xl/workbook.xml, where it inserts the element in the position the file format requires — Excel is strict about element order, and an element in the wrong place makes it offer to repair the file. It only adds the element when none is present, so running it twice is harmless. Writing to a temporary file and moving it into place means a failure halfway never leaves a half-written report behind.

Step 3 — Check the patched file still opens

A patched package should be checked by something other than the code that patched it. Let LibreOffice open and convert it; a malformed workbook fails to convert:

Bash
soffice -env:UserInstallation=file://$PWD/lo_profile --headless --convert-to pdf report_openpyxl.xlsx > /dev/null 2>&1
test -s report_openpyxl.pdf && echo "converted OK" || echo "conversion failed"
Python
from openpyxl import load_workbook

reloaded = load_workbook("report_openpyxl.xlsx")
print(reloaded.active["A1"].value)

Both LibreOffice and openpyxl open the patched file. That is also the key limitation of openpyxl here: it reads the file fine, and it will drop the flag the next time it saves it — the next step shows it.

Step 4 — Don't lose the flag on a re-save

Pipelines often build a workbook with one library and finish it with another. If openpyxl touches the file after the flag was set, the flag is gone:

Python
wb = load_workbook("monthly_report.xlsx")
wb["Summary"]["C1"] = "Checked"
wb.save("monthly_report_resaved.xlsx")
print("before re-save:", file_sharing("monthly_report.xlsx"))
print("after re-save: ", file_sharing("monthly_report_resaved.xlsx"))
add_read_only_recommended("monthly_report_resaved.xlsx")
print("after patch:   ", file_sharing("monthly_report_resaved.xlsx"))

Make setting the flag the last step of the pipeline, after every library has finished with the file — the same rule as for watermarks and anything else openpyxl does not round-trip. What openpyxl Loses When Saving an Existing Workbook lists the rest.

Set the flag last A report pipeline where data is written, formatting is applied with openpyxl, the file is saved, the read-only flag is added by the patch, and the file is published. Adding the flag before the openpyxl step would lose it. Write data pandas or xlsxwriter Finish with openpyxl styles, formulas Save flag would be lost here Add flag last step Publish shared drive

Step 5 — Mark as final

Mark as final is a stronger nudge: Excel opens the file with the ribbon's editing commands disabled and a yellow message bar saying the author marked it final, with an Edit Anyway button. It is stored as a custom document property named _MarkAsFinal. Both libraries can write it:

Python
from openpyxl.packaging.custom import BoolProperty

wb = xlsxwriter.Workbook("final_xlsxwriter.xlsx")
wb.add_worksheet().write("A1", "Final figures")
wb.set_custom_property("_MarkAsFinal", True)
wb.close()

wb = Workbook()
wb.active["A1"] = "Final figures"
wb.custom_doc_props.append(BoolProperty(name="_MarkAsFinal", value=True))
wb.save("final_openpyxl.xlsx")

for path in ("final_xlsxwriter.xlsx", "final_openpyxl.xlsx"):
    props = load_workbook(path).custom_doc_props
    print(path, [(p.name, p.value) for p in props.props])

load_workbook("final_xlsxwriter.xlsx").save("final_resaved.xlsx")
print("after openpyxl re-save:", [(p.name, p.value) for p in load_workbook("final_resaved.xlsx").custom_doc_props.props])

Unlike fileSharing, custom properties survive an openpyxl round trip, because openpyxl reads and writes them. Use mark-as-final for documents that genuinely should not change — a signed-off month-end pack, say — and read-only recommended for working reports that people may reasonably copy and adapt.

Removing the flag again

A report that was published read-only sometimes needs to become a working file again — for next month's version, say. Excel removes the flag through Save As → Tools → General Options; in a script, drop the element with the same kind of patch:

Python
def remove_read_only_recommended(path):
    tmp = path + ".tmp"
    with zipfile.ZipFile(path) as src, zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as dst:
        for item in src.infolist():
            data = src.read(item.filename)
            if item.filename == "xl/workbook.xml":
                data = re.sub(rb"<fileSharing[^>]*/>", b"", data)
            dst.writestr(item, data)
    shutil.move(tmp, path)

shutil.copy("monthly_report.xlsx", "next_month.xlsx")
remove_read_only_recommended("next_month.xlsx")
print(file_sharing("next_month.xlsx"))

The same fileSharing element also stores Excel's password to modify when one is set, so removing it removes that password too — check whether a file has one before stripping the element from files you did not create. Or simply load and save the file with openpyxl, which has the same effect as a side-effect — but the explicit function says what it does, and leaves the rest of the package untouched.

What these settings do not do

Both are conveniences. They do not encrypt anything, do not stop a determined edit, and are ignored by programs other than Excel — LibreOffice, Google Sheets and every Python library open the file normally. If a file must not be changed, protect the sheets with a password (Lock Cells and Protect a Sheet with openpyxl), publish it as a PDF, or control write access on the shared drive itself. If it must not be read, encrypt it: Password Protect an Excel File with Python.

Common pitfalls

  • Expecting openpyxl to write the flag. It has no setting for it; use xlsxwriter or patch after saving.
  • Setting the flag before a re-save. openpyxl drops it; add it last.
  • Inserting the element in the wrong place. It must come right after fileVersion, or first in the workbook element; otherwise Excel repairs the file.
  • Treating it as protection. It is a prompt, and only Excel shows it.
  • Forgetting it is permanent until removed. Every user sees the prompt every time; remove it via Save As options when the file becomes a working copy.

Conclusion

For a report that should be opened read-only by default, call read_only_recommended() in xlsxwriter, or add <fileSharing readOnlyRecommended="1"/> to the workbook part of an openpyxl file with a small zip patch as the very last step. For documents that are finished, set the _MarkAsFinal custom property, which both libraries write and openpyxl preserves. Check the saved files, and keep real protection — passwords, PDFs, file permissions — for files that must not change.

Frequently asked questions

How do I make an Excel file open read-only recommended with Python? With xlsxwriter, call workbook.read_only_recommended() before closing the workbook. For files written by openpyxl, add the element fileSharing with readOnlyRecommended="1" to xl/workbook.xml after saving.

Does openpyxl support read-only recommended? It can read the setting but never writes it, so saving a workbook with openpyxl removes the flag. Add it after the last openpyxl save.

How do I mark an Excel workbook as final from Python? Add a boolean custom document property named _MarkAsFinal set to true — with xlsxwriter's set_custom_property or openpyxl's custom_doc_props. Excel then opens the file with editing disabled and an Edit Anyway button.

Is read-only recommended secure? No. Users can decline the prompt, and programs other than Excel ignore it. Use sheet protection, a PDF, file permissions or encryption when the file must not be changed or read.

More on protection & sharing

More on protection & sharing →