Open a Password-Protected Excel File with Python
A workbook saved with Excel's "Password to open" is encrypted, and every Python reader fails on it in a way that hides the cause: openpyxl reports that the file is not a zip, and pandas cannot determine the format. Neither message mentions encryption. The fix is a small library, msoffcrypto-tool, which decrypts the container into a buffer that the normal readers then handle unchanged. This guide decrypts one file and a folder of them, handles wrong passwords cleanly, and draws the line between encryption and the sheet protection it is often confused with. It belongs to Protecting and Sharing Excel Workbooks.
Prerequisites
pip install msoffcrypto-tool pandas openpyxl
msoffcrypto-tool handles the encryption formats Office uses, including the modern AES-based scheme and the older ones.
Decrypt into memory and read
The whole operation is one function, and the plaintext workbook never reaches disk:
"""Read an encrypted workbook without writing a decrypted copy."""
import io
import msoffcrypto
import pandas as pd
def read_encrypted(path: str, password: str, **kwargs) -> pd.DataFrame:
decrypted = io.BytesIO()
with open(path, "rb") as fh:
office_file = msoffcrypto.OfficeFile(fh)
office_file.load_key(password=password)
office_file.decrypt(decrypted)
decrypted.seek(0)
kwargs.setdefault("engine", "openpyxl")
return pd.read_excel(decrypted, **kwargs)
df = read_encrypted("confidential.xlsx", password="hunter2")
print(df.head())
After decrypt() the buffer holds an ordinary .xlsx, so everything downstream is unchanged — openpyxl works the same way:
from openpyxl import load_workbook
decrypted.seek(0)
wb = load_workbook(decrypted)
print(wb.sheetnames)
Detect encryption before trying to read
Rather than catching a confusing error, ask the question directly:
import msoffcrypto
def is_encrypted(path: str) -> bool:
with open(path, "rb") as fh:
try:
return msoffcrypto.OfficeFile(fh).is_encrypted()
except Exception: # not an Office container at all
return False
print(is_encrypted("confidential.xlsx")) # True
That lets an ingest step branch cleanly: decrypt what is encrypted, read the rest directly, and report anything that is neither. The related identification problem is covered in Fix BadZipFile when reading an Excel file in Python.
Handle a wrong password properly
A bad password raises, and the message deserves improving before it reaches a log:
import io
import msoffcrypto
class DecryptionError(RuntimeError):
pass
def decrypt(path: str, password: str) -> io.BytesIO:
out = io.BytesIO()
with open(path, "rb") as fh:
office_file = msoffcrypto.OfficeFile(fh)
if not office_file.is_encrypted():
return io.BytesIO(open(path, "rb").read())
try:
office_file.load_key(password=password)
office_file.decrypt(out)
except msoffcrypto.exceptions.InvalidKeyError as exc:
raise DecryptionError(f"{path}: wrong password") from exc
out.seek(0)
return out
Returning the file unchanged when it is not encrypted makes the function safe to call on everything, which is what an ingest loop wants.
Keep the password out of the code
A password in a source file outlives every job that used it. Read it from the environment, or from whatever secrets manager the deployment already has:
import os
password = os.environ.get("REPORT_PASSWORD")
if not password:
raise SystemExit("REPORT_PASSWORD is not set")
For a folder of files from different senders, keep a mapping outside the code — an environment variable per sender, or a secrets-manager lookup keyed on the sender's name. Never log the password, and never put it in a filename.
Decrypt a whole folder
"""Decrypt every encrypted workbook into a working directory."""
import os
from pathlib import Path
import msoffcrypto
SRC = Path("incoming")
DST = Path("decrypted")
DST.mkdir(exist_ok=True)
password = os.environ["REPORT_PASSWORD"]
for src in sorted(SRC.glob("*.xlsx")):
with open(src, "rb") as fh:
office_file = msoffcrypto.OfficeFile(fh)
if not office_file.is_encrypted():
print("plain:", src.name)
continue
office_file.load_key(password=password)
with open(DST / src.name, "wb") as out:
office_file.decrypt(out)
print("decrypted:", src.name)
If you write decrypted copies to disk, treat that directory as sensitive: restrict its permissions, keep it off shared volumes, and delete the contents when the run finishes.
Write an encrypted file back out
msoffcrypto-tool decrypts; it does not encrypt. To produce a protected workbook you need Excel itself, or a library that wraps it — the options and their trade-offs are in Password protect an Excel file with Python. For most report pipelines the better answer is transport security rather than file encryption: deliver through a link that requires authentication instead of emailing an encrypted attachment and its password separately.
Do not confuse this with sheet protection
Sheet and structure protection are flags inside a readable file. openpyxl opens such a workbook without a password and can read every cell; the protection only stops editing in Excel's UI:
from openpyxl import load_workbook
wb = load_workbook("protected.xlsx") # no password needed
ws = wb["Data"]
print(ws.protection.sheet) # True — but every value is readable
That distinction matters when someone asks for a "password-protected" report: if the intent is confidentiality, protection is not enough, and if the intent is preventing accidental edits, encryption is too much. Setting protection is covered in Lock cells and protect a sheet with openpyxl.
Choose the protection that matches the intent
"Protect the report" means at least three different things, and picking the wrong one produces either a false sense of security or a file nobody can use:
The third column is the one most teams should be moving towards. An encrypted attachment travels with its own password, usually in a second email, and cannot be revoked once sent; a link checked against an identity can be withdrawn the day someone changes team. The delivery mechanics are in Publishing Excel Reports to Cloud Storage.
Fit it into a scheduled ingest
The decryption step belongs at the very front of a pipeline, before anything else forms an opinion about the file. A small wrapper keeps the rest of the job unaware that encryption exists at all:
"""ingest.py — encryption handled once, at the boundary."""
import io
import os
from pathlib import Path
import msoffcrypto
import pandas as pd
PASSWORD = os.environ.get("REPORT_PASSWORD", "")
def open_workbook(path: Path) -> io.BytesIO:
"""Return readable workbook bytes, decrypting only if necessary."""
raw = path.read_bytes()
if raw[:2] == b"PK": # already a plain zip
return io.BytesIO(raw)
buffer = io.BytesIO()
with io.BytesIO(raw) as source:
office_file = msoffcrypto.OfficeFile(source)
office_file.load_key(password=PASSWORD)
office_file.decrypt(buffer)
buffer.seek(0)
return buffer
for path in sorted(Path("incoming").glob("*.xlsx")):
df = pd.read_excel(open_workbook(path), engine="openpyxl")
print(f"{path.name}: {len(df):,} rows")
Checking the first two bytes rather than calling is_encrypted() avoids opening the file twice, and it also handles the case where a sender switches protection on or off between months without telling anybody. Log which files needed decrypting — that record is useful when a password rotates and half the batch suddenly fails.
Common pitfalls and gotchas
- Reading before
seek(0). The decrypted buffer is positioned at the end after writing. - Assuming the error means corruption. "Not a zip file" is exactly what an encrypted workbook looks like to openpyxl.
- Leaving decrypted copies behind. Delete them, or decrypt into memory and avoid the question.
- Hard-coded passwords. They end up in version control and in log lines built with f-strings.
- Legacy
.xlsencryption. Older schemes are supported but weaker; if you control the sender, ask for a modern format.
Performance and scale notes
Decryption is fast — it is a symmetric cipher over a file that is already compressed — so the cost is dominated by the subsequent Excel parse, exactly as it would be for a plain file. Memory is the consideration: the decrypted workbook exists in the buffer alongside the encrypted source, so peak usage is roughly twice the file size before parsing begins. For a large encrypted workbook, decrypt to a temporary file rather than a buffer, parse from the path, and delete it in a finally block. The same reasoning applies to any in-memory pipeline, as covered in Build an Excel workbook in memory with BytesIO.
Conclusion
An encrypted workbook is not a corrupt one — it is an OLE2 container that no zip-based reader can open. Decrypt it with msoffcrypto-tool into a BytesIO, then read it exactly as you would any other file. Detect encryption up front so an ingest loop can branch cleanly, raise a clear error on a wrong password, keep the password in the environment, and remember that sheet protection is a different thing entirely.
Frequently asked questions
Why does openpyxl say a protected file is not a zip? An encrypted workbook is an OLE2 container wrapping the encrypted payload, not a zip. openpyxl opens the zip first, so it fails before it can ask for a password. Decrypt the file first, then read the result.
Is this the same as a sheet-protection password? No. Sheet and workbook-structure protection stop editing but leave the file readable — openpyxl opens it without any password. Encryption ("Password to open") makes the bytes unreadable until decrypted.
Can Python remove the password permanently? You can write out a decrypted copy, which is what most pipelines want. Keep the encrypted original if the protection is a requirement rather than an obstacle.
What if I do not have the password? Then you cannot read the file. Modern Office encryption is AES-based and there is no supported way around it — ask the sender to re-issue the workbook or share the password through a secrets manager.
Does the decrypted copy have to touch disk?
No. msoffcrypto-tool writes to any file-like object, so decrypting into an io.BytesIO and reading from that keeps the plaintext workbook in memory only.
Related
- Up: Protecting and Sharing Excel Workbooks — the other side of the same subject.
- Password protect an Excel file with Python — producing a protected file rather than reading one.
- Lock cells and protect a sheet with openpyxl — the edit-prevention flags this page is often confused with.
- Fix BadZipFile when reading an Excel file in Python — the error an encrypted file produces, among others.
- Build an Excel workbook in memory with BytesIO — the buffer pattern the decryption relies on.