Excel doesn't close if I use the "with" keyword instead of "inline" python

Question:

I use python to refresh some power query table in Excel using win32com.client library.

  • If I write everything "inline" (1st version): excel closes correctly
  • If I use a "with" statement to open/close excel and workbooks (2nd version): excel process doesn’t close

==> Why is it not doing the same thing?

==> How can I make the "with" version working?

Code "inline" : excel closes correctly:

# Start excel app
xlapp = win32.gencache.EnsureDispatch('Excel.Application')
xlapp.Visible = False
xlapp.Interactive = False
excel_constant = win32.constants

# Open workbook
file_name = str(file)
wb = xlapp.Workbooks.Open(file_name)

#Refresh power query
wb.RefreshAll()
xlapp.CalculateUntilAsyncQueriesDone()

# Close workbook
wb.Close(SaveChanges=True)
wb = None
gc.collect()

# Quit excel app
xlapp.Quit()
xlapp = None
gc.collect()

Using "with" the excel process stays active

class start_excel(object):
    def __init__(self):
        pass

    def __enter__(self):
        self.xlapp = win32.gencache.EnsureDispatch('Excel.Application')
        self.xlapp.Visible = False
        self.xlapp.Interactive = False

        return self.xlapp

    def __exit__(self, *args):
        self.xlapp.Quit()
        self.xlapp = None
        gc.collect()


class open_workbook(object):
    def __init__(self, xlapp, file_name):
        self.file_name = file_name
        self.xlapp = xlapp
     
    def __enter__(self):
        self.chemin_fichier = str(self.file_name)
        self.wb = self.xlapp.Workbooks.Open(self.chemin_fichier)
        
        return self.wb
 
    def __exit__(self, *args):
        self.wb.Close(SaveChanges=True)
        self.wb = None
        gc.collect()



with start_excel() as xlapp:
    # excel_constant = win32.constants
    with open_workbook(xlapp, file) as wb:
        wb.RefreshAll()
        xlapp.CalculateUntilAsyncQueriesDone()

EDIT: 3rd example, with a contextlib.contextmanager (same issue)

import contextlib

@contextlib.contextmanager
def start_excel():
    try:
        xlapp = win32.gencache.EnsureDispatch('Excel.Application')
        xlapp.Visible = False
        xlapp.Interactive = False
        yield xlapp

    finally:
        xlapp.Quit()
        xlapp = None
        gc.collect()

@contextlib.contextmanager
def open_workbook(xlapp, file_name):
    try:
        chemin_fichier = str(file_name)
        wb = xlapp.Workbooks.Open(chemin_fichier)
        yield wb
    
    finally:
        wb.Close(SaveChanges=True)
        wb = None
        gc.collect()



with start_excel() as xlapp:
    # excel_constant = win32.constants
    with open_workbook(xlapp, file) as wb:
        wb.RefreshAll()
        xlapp.CalculateUntilAsyncQueriesDone()

EDIT: code below works but is not satisfying
I added the 2 last lines: wb = None & xlapp = None, Excel closes correctly.
But it doesn’t feel safe, is there a way to ensure that the with statement cleanly clear its variables? I feel like it’s easy to forget to include these additionnal lines every time I use with.

class start_excel(object):
    def __init__(self):
        pass

    def __enter__(self):
        self.xlapp = win32.gencache.EnsureDispatch('Excel.Application')
        self.xlapp.Visible = False
        self.xlapp.Interactive = False

        return self.xlapp

    def __exit__(self, *args):
        self.xlapp.Quit()
        self.xlapp = None
        gc.collect()


class open_workbook(object):
    def __init__(self, xlapp, file_name):
        self.file_name = file_name
        self.xlapp = xlapp
     
    def __enter__(self):
        self.chemin_fichier = str(self.file_name)
        self.wb = self.xlapp.Workbooks.Open(self.chemin_fichier)
        
        return self.wb
 
    def __exit__(self, *args):
        self.wb.Close(SaveChanges=True)
        self.wb = None
        gc.collect()



with start_excel() as xlapp:
    # excel_constant = win32.constants
    with open_workbook(xlapp, file) as wb:
        wb.RefreshAll()
        xlapp.CalculateUntilAsyncQueriesDone()
        wb = None
    
    xlapp = None
Asked By: Nico

||

Answers:

I understood the issue:

  • Excel needs the variable set to None so it can close
  • Using with keyword, there are 2 references : the reference in the generator and the one in the "main" code. the __exit__ part remove only one of the 2 references.

The last version works as expected, but I don’t like that you need to do some mandatory clean up outside of the __exit__ part.

class start_excel(object):
    def __init__(self):
        pass

    def __enter__(self):
        self.xlapp = win32.gencache.EnsureDispatch('Excel.Application')
        self.xlapp.Visible = False
        self.xlapp.Interactive = False

        return self.xlapp

    def __exit__(self, *args):
        self.xlapp.Quit()
        self.xlapp = None
        gc.collect()


class open_workbook(object):
    def __init__(self, xlapp, file_name):
        self.file_name = file_name
        self.xlapp = xlapp
     
    def __enter__(self):
        self.chemin_fichier = str(self.file_name)
        self.wb = self.xlapp.Workbooks.Open(self.chemin_fichier)
        
        return self.wb
 
    def __exit__(self, *args):
        self.wb.Close(SaveChanges=True)
        self.wb = None
        gc.collect()



with start_excel() as xlapp:
    # excel_constant = win32.constants
    with open_workbook(xlapp, file) as wb:
        wb.RefreshAll()
        xlapp.CalculateUntilAsyncQueriesDone()
        wb = None
    
    xlapp = None
Answered By: Nico
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.