Fix xlsxwriter "Exception caught in workbook destructor"
This message appears in older scripts and older environments:
Exception ignored in: <function Workbook.__del__ at 0x7f...>
Exception: Exception caught in workbook destructor. Explicit close() may be required for workbook.
It means xlsxwriter tried to save the workbook when Python destroyed the Workbook object, because the code never called close(), and saving failed. In current xlsxwriter the message no longer exists, but the underlying mistake does, and it has become quieter: a workbook that is never closed is simply never written. This page explains both, and how to close workbooks so the file is always written or the failure is always reported. It is part of Troubleshooting Common Python Excel Errors.
Prerequisites
pip install xlsxwriter pandas
The examples use xlsxwriter 3.2. xlsxwriter builds the whole file in memory (or temporary files) and writes the .xlsx only in close(); nothing reaches the target path before that.
Where the message comes from
In xlsxwriter 1.0 and earlier, Workbook had a destructor that called close() if the code had not, and turned any error it hit into this exception:
# xlsxwriter 1.0.x, workbook.py
def __del__(self):
"""Close file in destructor if it hasn't been closed explicitly."""
try:
if not self.fileclosed:
self.close()
except:
raise Exception("Exception caught in workbook destructor. "
"Explicit close() may be required for workbook.")
Exceptions raised in a destructor cannot propagate, so Python printed it as "Exception ignored" and carried on, and the real error — a missing directory, a file locked by Excel — was swallowed. Version 1.1 removed the destructor. If you see this message, the environment has a very old xlsxwriter; upgrading is worthwhile, but the fix in the code is the same either way. Check which version the failing job actually runs, which is not always the one on your laptop:
python -c "import xlsxwriter; print(xlsxwriter.__version__)"
Old versions usually survive in long-lived servers, frozen virtual environments and executables built years ago. Pin a current version in the project's requirements, as described in Set Up an Excel Automation Project with uv, so every environment that runs the report gets the same behaviour. After upgrading, a missing close() no longer produces the message at all, which makes the next two steps essential rather than optional.
Step 1 — See the silent version
In a current environment, forgetting close() produces no error and no file:
import gc
import os
import xlsxwriter
def build_report():
wb = xlsxwriter.Workbook("never_closed.xlsx")
wb.add_worksheet().write("A1", "Sales")
# no wb.close()
build_report()
gc.collect()
print("file written:", os.path.exists("never_closed.xlsx"))
The function returns normally, the process exits with status 0, and any monitoring that only watches exit codes sees a healthy job. A scheduled job with this bug "succeeds" every night and delivers nothing, or, worse, attaches last night's file that happens to be lying in the folder.
Step 2 — Close with a context manager
Workbook is a context manager: with calls close() when the block ends, including when it ends because of an error. That guarantees the file is written or the failure is raised at a known place:
with xlsxwriter.Workbook("report.xlsx") as wb:
ws = wb.add_worksheet("Sales")
ws.write_row("A1", ["Region", "Total"])
ws.write_row("A2", ["North", 12500])
print("file written:", os.path.exists("report.xlsx"))
For pandas, the same applies to ExcelWriter. Its old save() method was removed in pandas 2.0; use with or close():
import pandas as pd
with pd.ExcelWriter("pandas_report.xlsx", engine="xlsxwriter") as writer:
pd.DataFrame({"Region": ["North"], "Total": [12500]}).to_excel(writer, index=False)
print("has save():", hasattr(writer, "save"), "| file written:", os.path.exists("pandas_report.xlsx"))
Step 3 — Handle the errors close() raises
Because the file is only created in close(), that is where file-system problems surface, as xlsxwriter.exceptions.FileCreateError. The usual causes are a directory that does not exist, a folder without write permission, and on Windows, the same file open in Excel:
from xlsxwriter.exceptions import FileCreateError
for path in ["missing_dir/report.xlsx", "locked/report.xlsx"]:
wb = xlsxwriter.Workbook(path)
wb.add_worksheet().write("A1", "x")
try:
wb.close()
except FileCreateError as e:
print(f"{path}: {e}")
For an interactive script, a retry prompt is kind to the user who left the file open. For a scheduled job, create the directory first and let the error fail the job loudly:
from pathlib import Path
out = Path("output/2026-09/report.xlsx")
out.parent.mkdir(parents=True, exist_ok=True)
with xlsxwriter.Workbook(out) as wb:
wb.add_worksheet().write("A1", "x")
print("written to", out)
Step 4 — Don't ship a half-built report
The context manager closes the workbook even when the code inside the block fails, so an error halfway through leaves a valid but incomplete file at the target path:
try:
with xlsxwriter.Workbook("half_built.xlsx") as wb:
ws = wb.add_worksheet()
ws.write("A1", "Summary")
raise RuntimeError("database query failed")
except RuntimeError as e:
print("error:", e, "| file exists anyway:", os.path.exists("half_built.xlsx"))
If a later step emails or uploads whatever is in the output folder, the recipients get a report with a title and no numbers. Build to a temporary name and rename only on success; os.replace is atomic on the same file system, so readers see either the old file or the complete new one:
def write_report(path, build):
tmp = Path(path).with_suffix(".tmp.xlsx")
try:
with xlsxwriter.Workbook(tmp) as wb:
build(wb)
except Exception:
tmp.unlink(missing_ok=True)
raise
os.replace(tmp, path)
write_report("final.xlsx", lambda wb: wb.add_worksheet().write("A1", "complete"))
print(sorted(p for p in os.listdir(".") if p.startswith(("final", "half"))))
Step 5 — Check the file before delivering it
Closing correctly guarantees that a written file is complete; it does not guarantee that the file contains what the recipient expects. A last check before the email or upload step catches the rest: the file exists, was written by this run, opens, and has the sheets and rows it should. It costs a fraction of a second:
import time
from openpyxl import load_workbook
def check_report(path, started, sheets, min_rows=2):
p = Path(path)
assert p.exists(), f"{p} was not written"
assert p.stat().st_mtime >= started, f"{p} is left over from an earlier run"
wb = load_workbook(p, read_only=True)
try:
missing = set(sheets) - set(wb.sheetnames)
assert not missing, f"{p} is missing sheets {missing}"
for name in sheets:
rows = sum(1 for _ in wb[name].iter_rows(values_only=True))
assert rows >= min_rows, f"{p}:{name} has only {rows} rows"
finally:
wb.close()
return "ok"
started = time.time() - 1
with xlsxwriter.Workbook("checked.xlsx") as wb:
ws = wb.add_worksheet("Sales")
ws.write_row("A1", ["Region", "Total"])
ws.write_row("A2", ["North", 12500])
print(check_report("checked.xlsx", started, ["Sales"]))
try:
check_report("half_built.xlsx", started, ["Sales"])
except AssertionError as e:
print("blocked:", e)
The modification-time check is what catches the most embarrassing failure: a job whose workbook was never closed, sending the file an earlier run left behind. The sheet and row checks catch the half-built file from Step 4, which here does not even have a Sales sheet.
Common pitfalls
- Relying on garbage collection. Nothing writes an unclosed workbook in current xlsxwriter; always close.
- Calling
close()twice. The second call only warns "Calling close() on already closed file"; harmless, but a sign that close is in two code paths. writer.save()in pandas. Removed in pandas 2.0; usewith pd.ExcelWriter(...)orclose().- Catching everything around
close(). A bareexcepthidesFileCreateError, which is exactly the error you need to see. - Partial files. A
withblock writes the file even after an error; write to a temporary name and rename on success. - Jupyter notebooks. A workbook created in one cell and never closed stays in memory until the kernel restarts; the file never appears.
Conclusion
"Exception caught in workbook destructor" is old xlsxwriter's way of saying the workbook was never explicitly closed and the automatic save failed; newer versions say nothing and write nothing. Close every workbook with a with block or close(), catch FileCreateError where the file is created, create output directories first, and write to a temporary name that is renamed only when the whole report has been built.
Frequently asked questions
What does "Explicit close() may be required for workbook" mean? The workbook was never closed, so xlsxwriter versions up to 1.0 tried to save it when the object was garbage collected, and that save failed. Call close() yourself, or use a with block, and the real error will be raised where you can see it.
Why does xlsxwriter not create my file? The file is only written when close() runs. If the code never calls it, current versions of xlsxwriter write nothing and report nothing.
What is FileCreateError in xlsxwriter? The exception close() raises when it cannot create the output file: the directory does not exist, there is no write permission, or on Windows the file is open in Excel.
How do I save a pandas ExcelWriter now that save() is gone? Use it as a context manager, with pd.ExcelWriter(path) as writer:, or call writer.close() when finished.
Related
- Up one level: Troubleshooting Common Python Excel Errors — the other common errors.
- Fix "ExcelWriter object has no attribute save" in pandas — the pandas side of closing.
- Fix "Excel Found Unreadable Content" After Writing with Python — when the file is written but broken.
- Ignore Excel Lock Files When Processing a Folder — the other side of files open in Excel.
- Detect a Scheduled Report That Did Not Run — catching jobs that silently produce nothing.
More on errors & fixes
- FixHandle Permission Denied When Writing Excel in Python
- How-toWhat openpyxl Loses When You Save an Existing Workbook
- FixFix Excel Serial Numbers Showing Instead of Dates
- How-toHandle Daylight Saving Time in Excel Timestamps
- How-toHandle Microsoft Graph Throttling in Excel Automation
- GuideError Handling and Logging in Excel Automation