Search code examples
pythonoutlooktypeerrorwin32com

TypeError: '_Folders' object is not callable with win32com


The only information I've found regarding my issue are from other StackOverflow questions that don't actually resolve my issue.

TypeError: '_Folders' object is not callable

Using Outlook via win32com, suddenly getting runtime error '_Folders' object is not callable`

Script has been working perfectly for the past 3 months, but has suddenly stopped working today (no changes were made between it working yesterday, and being broken today)

What confuses me the most is that I have tried the exact same script on another PC and it still works perfectly fine. (both transferring the problem .py file and also rewriting the code to a new script file)

Given the above, I assume the error is my PC specific, however I have no idea what it could be.

For reference, my Outlook has two accounts linked to it and I need to access the second account, so DefaultFolder() will not work

import win32com.client

mailbox = ['Address', 'Inbox']

outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI")

# Returns the same error
inbox = namespace.Folders(mailbox[0]).Folders(mailbox[1])
inbox = namespace.Folders('Address').Folders('Inbox')
inbox = namespace.Folders('Address')

inbox = outlook.GetNamespace("MAPI").Folders('Address')
inbox = outlook.GetNamespace("MAPI").Folders('Address').Folders('Inbox')

# Error
>>> File c:\...\script.py, line 15, in <module>
>>>    inbox = namespace.Folders('Address').Folders('Inbox')
>>> TyperError: '_Folders' object is not callable

Not sure if this confirms anything, however another test I did is as follows:

# Deliberately misspell .Folders()
inbox = namespace.folders('Address').Folders('Inbox')

# Error
>>> File c:\...\script.py, line 15, in <module>
>>>    inbox = namespace.folders('Address').Folders('Inbox')
>>> AttributeError: '<win32com.gen_py.Microsoft Outlook 16.0 Object Library._NameSpace instance at 0x2380896888784>' 
>>>  object has no attribute 'folders'. Did you mean: 'Folders'?


# Deliberately misspell .Folders()
inbox = namespace.Folder('Address').Folders('Inbox')

# Error
>>> File c:\...\script.py, line 15, in <module>
>>>    inbox = namespace.Folder('Address').Folders('Inbox')
>>> AttributeError: '<win32com.gen_py.Microsoft Outlook 16.0 Object Library._NameSpace instance at 0x2380896888784>' 
>>>  object has no attribute 'Folder'. Did you mean: 'Folders'?

Solution

  • Since asking one of the original questions above, I have learnt more about the vagueries of win32com.client. The OP has been snagged by the late-vs-early binding gotcha in win32com.client.Dispatch().

    The quickest short-term fix is to change the object creation line to:

    outlook = win32com.client.dynamic.Dispatch('Outlook.Application')
    

    Another quick, but ultimately less durable fix is to locate the %USERPROFILE%\AppData\Local\Temp directory and delete the gen_py folder.

    Yet another is to replace () with [] when indexing, eg:

    inbox = namespace.Folders['Address'].Folders['Inbox']
    

    However, my preference for a durable solution would be to change the object creation line to:

    outlook = win32com.client.gencache.EnsureDispatch('Outlook.Application')
    

    and change Folders() to Folders[].

    Why do I have to do this when my code was working just fine previously?

    The answer to this is that you are calling win32com.client.Dispatch() and letting win32com decide at runtime whether to use Early or Late binding to the COM object you are creating. If you know you want Early binding, use win32com.client.gencache.EnsureDispatch() and if you know you want Late binding, use win32com.client.dynamic.Dispatch(). If you don't choose, win32com will, and may not always make the same choice.

    When you use win32com.client.Dispatch(), you are asking win32com to choose which binding you will get, and win32com will choose Early binding whenever it can. It looks for the generated Python wrapper files for your object on the filesystem: it if finds them, then you get Early binding.

    If when you first wrote your code, the Python wrappers were not available, then you would get Late binding. In terms of code this means you can use () indexing and case-INsensitive method names.

    All is working fine until (maybe without your knowing) some other Python code calls win32com.client.gencache.EnsureDispatch() which generates the wrapper files for your object. The next time your code runs (and calls Dispatch()) win32com will find the wrapper files and switch to Early binding. This is more restrictive and () indexing will no longer work (as the OP has found), and method names are now case-sensitive. On the plus side you now have access to win32com.client.constants which allows you to use any enums associated with your object (eg olMail for an Mail Object Class rather than the less obvious number 43).

    This snippet shows the difference:

    import win32com.client as wc
    
    bEarly = True
    addr = 'john.doe@somewhere.com'
    fldr = None
    
    if bEarly:
        print('Using Early Binding')
        outlook = wc.gencache.EnsureDispatch('Outlook.Application') 
        ns = outlook.GetNamespace('MAPI') 
        fldrs = ns.Folders
        print(type(fldrs))
        fldr = fldrs[addr]
    else:
        print('Using Late Binding')
        outlook = wc.dynamic.Dispatch('Outlook.Application')
        ns = outlook.gEtNaMeSpAcE('MAPI')
        fldrs = ns.fOLdErS
        print(type(fldrs))
        fldr = fldrs(addr)
    
    print(fldr.Name)
    

    With bEarly=False, Late binding is enforced and the Folders object is of type <class 'win32com.client.dynamic.CDispatch'>. I believe this class has the __call__ method implemented, which directs any method calls without names (eg obj()) to that object's default method, which for many Office collection objects is Item(). Hence Folders(addr) is translated to Folders.Item(addr).

    With bEarly=True, Early binding uses the Python wrappers to define a new class type: in this case the rather cryptic <class 'win32com.gen_py.00062FFF-0000-0000-C000-000000000046x0x9x6._Folders._Folders'>. This class has all the methods of the object (you can look in the .py file in the gen_py folder for that CLSID). This wrapper does not provide a default interface, so no (). Instead, the wrapper knows this object is a collection so can be indexed in the usual Python way with [].