Guide
Getting Started With Python Excel AutomationDeep dive

Handle COM Errors and Excel Dialog Prompts in Python

Decode pywintypes.com_error through excepinfo, recognise the four HRESULTs that matter, suppress every dialog before opening a file, and add a watchdog for the hangs left over.

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.

Two failure shapes, only one of which raises A COM error arrives as an exception carrying a tuple of numbers that needs decoding, while a modal dialog produces no exception at all and blocks the call indefinitely. com_error an exception is raised numbers, not a message decode excepinfo invisible dialog no exception at all the call never returns prevent, do not catch two shapes the silent one is the expensive one

Prerequisites

Bash
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.

Python
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

The four COM result codes a reporting script meets DISP_E_EXCEPTION means the call raised inside Excel, 800A03EC is an invalid operation such as a wrong range, call rejected means Excel is busy and is worth retrying, and MK_E_UNAVAILABLE means no instance is running. Code Meaning What to do 0x80020009 raised inside Excel read excepinfo 0x800A03EC invalid operation check range or sheet 0x80010001 Excel is busy retry with backoff 0x800401E3 no running instance fall back to DispatchEx only one of these four is worth retrying

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.

Python
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.

Python
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.

Python
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.

Python
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.

Python
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.

Python
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

SymptomCauseFix
The script stops with no error and no outputAn invisible modal dialogSet DisplayAlerts, AskToUpdateLinks, EnableEvents and AutomationSecurity before opening
com_error (-2147352567, 'Exception occurred.', ...)The call raised inside ExcelRead excepinfo[2] for Excel's own message
Call was rejected by calleeExcel busy — a dialog, or a user interactingRetry with a short backoff on 0x80010001
GetActiveObject fails on a clean machineNo Excel instance runningFall back to DispatchEx
A macro in the file runs unexpectedlyWorkbook_Open fired on openexcel.EnableEvents = False before opening
Errors reference the wrong fileExcel reports the active workbook, not the one you meantLog 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.

Where to put the guard Wrapping the whole operation costs one boundary crossing, while a try block around each property access adds a crossing per guard and makes the log harder to read. handling shape wrap the operation one guard, one crossing decode the error excepinfo names the cause log one line not thirty of traceback guard the unit of work, not every property read

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.