Handle COM Errors and Excel Dialog Prompts in Python
COM failures come in two shapes, and only one of them looks like an error. The first is a
pywintypes.com_error carrying a tuple of numbers that mean nothing at a glance. The second is
worse: no exception at all, because Excel is waiting on a dialog that is invisible and the script
simply stops. This guide, part of
Automating Excel with COM and pywin32,
covers decoding the first and preventing the second.
Prerequisites
pip install pywin32
Decoding a com_error
The exception's args is a four-element tuple: the HRESULT, a short description of it, an
excepinfo structure and an argument index. Nearly all the useful information is in excepinfo,
and nearly all published examples ignore it.
import pywintypes
import win32com.client as win32
def describe(error: pywintypes.com_error) -> str:
hresult, message, excepinfo, argument = error.args
detail = ""
if excepinfo:
source = excepinfo[1] or ""
description = excepinfo[2] or ""
detail = f" — {source}: {description}".rstrip(": ")
return f"COM 0x{hresult & 0xFFFFFFFF:08X} {message}{detail} (arg {argument})"
try:
excel.Workbooks.Open(r"C:\missing\nothing.xlsx")
except pywintypes.com_error as error:
print(describe(error))
That turns (-2147352567, 'Exception occurred.', (0, 'Microsoft Excel', "'nothing.xlsx' could not be found...", ...), None) into a single line naming the file and the reason — which is the difference
between a loggable failure and a screenshot in a support ticket.
The HRESULTs worth recognising
Four codes cover most of what a reporting script encounters. 0x80020009 (DISP_E_EXCEPTION) means
the call raised inside Excel and the real detail is in excepinfo. 0x800A03EC is Excel's generic
"that operation is not valid here" and usually means a wrong range address, a missing sheet or a
protected cell. 0x80010001 (RPC_E_CALL_REJECTED) means Excel was busy — typically because a dialog
is open or a user is dragging something — and is the one worth retrying. 0x800401E3
(MK_E_UNAVAILABLE) comes from GetActiveObject when no instance is running.
RETRYABLE = {0x80010001, 0x8001010A} # call rejected, message filter timed out
def call_with_retry(operation, attempts=5, delay=1.0):
import time
for attempt in range(attempts):
try:
return operation()
except pywintypes.com_error as error:
if (error.args[0] & 0xFFFFFFFF) not in RETRYABLE or attempt == attempts - 1:
raise
time.sleep(delay * (attempt + 1))
Retrying only the two codes that genuinely mean "busy" is the point. A blanket retry around a missing-file error just delays the failure by five seconds and makes the log harder to read.
Preventing the invisible dialog
Every dialog Excel can raise during an unattended run has a property that suppresses it, and setting them all before opening anything is the single highest-value habit in COM automation.
XL_AUTOMATION_SECURITY_FORCE_DISABLE = 3
excel.Visible = False
excel.DisplayAlerts = False # save prompts, overwrite prompts
excel.AskToUpdateLinks = False # "update external links?"
excel.EnableEvents = False # workbook Open macros that show forms
excel.AutomationSecurity = XL_AUTOMATION_SECURITY_FORCE_DISABLE # macro prompts
excel.FeatureInstall = 0 # "a feature must be installed" prompt
EnableEvents = False deserves the most attention. A workbook with a Workbook_Open macro will run
it the moment your script opens the file, and if that macro shows a message box the script hangs
before it has done anything at all. Disabling events also means Worksheet_Change handlers do not
fire while you write, which is usually what you want and occasionally not — a workbook that relies
on those handlers to maintain a summary will need them back on.
Opening a file without prompts
The Open call has its own set of arguments that suppress the remaining prompts, and they are worth
passing explicitly rather than relying on defaults.
book = excel.Workbooks.Open(
Filename=r"C:\reports\model.xlsx",
UpdateLinks=0, # do not update, do not ask
ReadOnly=False,
IgnoreReadOnlyRecommended=True,
Notify=False, # do not queue a "now available" notification
CorruptLoad=0, # normal load; 1 repairs, 2 extracts data
)
IgnoreReadOnlyRecommended=True handles workbooks saved with the read-only recommendation, which
otherwise produce a prompt that is invisible and blocking. Notify=False matters when the file may
be locked by somebody else: without it Excel queues a notification and the call behaves unexpectedly.
A watchdog for the hangs you cannot prevent
Some hangs survive every setting — a corrupt file that triggers repair, a licence activation prompt, an add-in with its own dialog. For unattended runs, the answer is a timeout around the whole operation rather than a better guess about which flag was missed.
import concurrent.futures as futures
import pythoncom
def run_with_timeout(work, seconds=300):
def target():
pythoncom.CoInitialize()
try:
return work()
finally:
pythoncom.CoUninitialize()
with futures.ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(target)
try:
return future.result(timeout=seconds)
except futures.TimeoutError:
raise TimeoutError(f"Excel did not respond within {seconds}s — likely a dialog")
The thread cannot be killed, so this has to be paired with terminating the Excel process you started — the technique in Close Excel Cleanly and Avoid Orphan COM Processes. Together they turn an indefinite hang into a failed run the scheduler can report and retry.
Failing where the cause is
The pattern that saves the most time is not a better exception handler but a check placed earlier.
Most COM failures are consequences: a missing sheet name surfaces as an invalid-operation error
three calls later, a locked file surfaces as a save failure at the end of a long job, an empty range
surfaces as a None that propagates into a report. Each of those has a cheap precondition.
def open_checked(excel, path, required_sheets):
from pathlib import Path
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"{path} does not exist")
if path.stat().st_size == 0:
raise ValueError(f"{path} is zero bytes — an interrupted download?")
book = excel.Workbooks.Open(str(path), UpdateLinks=0, Notify=False)
names = {sheet.Name for sheet in book.Sheets}
missing = [name for name in required_sheets if name not in names]
if missing:
book.Close(SaveChanges=False)
raise KeyError(f"{path.name} is missing sheet(s): {missing}; has {sorted(names)}")
return book
The error message names the file, the missing sheets and the ones that are actually present, which is everything the person fixing it needs. Compare that with the alternative — a COM exception mentioning an invalid range, raised somewhere in the middle of the run — and the ten lines pay for themselves the first time an upstream export renames a tab.
Making failures visible to the people who care
An unattended job that fails silently is worse than one that fails loudly, and COM automation fails silently by default: the process ends, the file is not updated, and the only evidence is an absence. Two conventions fix that. Write the run's outcome somewhere the recipients already look — a timestamp cell in the workbook, a line in a status sheet — and make the failure path notify a person rather than a log file nobody opens.
from datetime import datetime
def stamp(book, status: str) -> None:
sheet = book.Sheets("Summary")
sheet.Range("A1").Value = f"{status} — {datetime.now():%Y-%m-%d %H:%M}"
A recipient who can see when the file was last refreshed will notice a stale report themselves, which is a far more reliable detector than any monitoring you are likely to build for a single job. The notification half is covered in Emailing Excel Reports with smtplib, and the validation that decides whether to send at all in Validate an Excel Report Before Sending It.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| The script stops with no error and no output | An invisible modal dialog | Set DisplayAlerts, AskToUpdateLinks, EnableEvents and AutomationSecurity before opening |
com_error (-2147352567, 'Exception occurred.', ...) | The call raised inside Excel | Read excepinfo[2] for Excel's own message |
Call was rejected by callee | Excel busy — a dialog, or a user interacting | Retry with a short backoff on 0x80010001 |
GetActiveObject fails on a clean machine | No Excel instance running | Fall back to DispatchEx |
| A macro in the file runs unexpectedly | Workbook_Open fired on open | excel.EnableEvents = False before opening |
| Errors reference the wrong file | Excel reports the active workbook, not the one you meant | Log the path yourself alongside the decoded error |
Performance and scale
Error handling has a performance dimension that is easy to miss: a try/except around every COM
call adds another boundary crossing when the guard itself reads a property. Wrap the operation, not
the property access.
The other scale concern is log volume. A decoded com_error is one useful line; the raw traceback of
a pywin32 exception is thirty lines that say very little. On a job that processes hundreds of
workbooks, logging the decoded form and keeping the traceback at debug level is the difference
between a log somebody reads and one nobody does — the same argument made in
Log Python Excel Script Output to a File.
Conclusion
Suppress the dialogs before opening anything, decode com_error through its excepinfo member so
the log names Excel's own message, retry only the codes that genuinely mean "busy", and put a
timeout around unattended work so a hang becomes a failure. Those four habits turn COM automation
from something that mysteriously stops into something that reports what went wrong.
Frequently asked questions
What do the numbers in a com_error mean? The tuple is (hresult, message, excepinfo, argument_index). The second element is a short description of the HRESULT, and excepinfo — when present — is a nested tuple whose third and fourth elements carry Excel's own source and description, which is the part worth logging.
Why does my script hang instead of raising? Excel is showing a modal dialog. With Visible set to False the dialog is invisible, and the COM call blocks until somebody dismisses it — which nobody can. Setting DisplayAlerts, AskToUpdateLinks and AutomationSecurity before opening anything prevents nearly all of them.
Is -2147352567 always the same problem? No. 0x80020009 is DISP_E_EXCEPTION, which just means the call raised inside Excel. The useful detail is in the excepinfo member, not the HRESULT itself.
How do I stop 'file in use' prompts on open? Open with ReadOnly=True and Notify=False when you only need to read; that suppresses the prompt and the notification. If you must write, resolve the lock first rather than suppressing the warning.
Related
- Up one level: Automating Excel with COM and pywin32 — the session settings these errors come from.
- Close Excel Cleanly and Avoid Orphan COM Processes — cleaning up after the hang a watchdog catches.
- Error Handling and Logging in Excel Automation — the surrounding job's resilience patterns.
- Retry a Failed Excel Report Job in Python — backoff strategies for the retryable codes.
- Troubleshooting Common Python Excel Errors — the file-level errors that have nothing to do with COM.