Stop Doing X — Try This Instead: Download Outlook Attachments with Python

Your finance team just sent a 42-email thread titled 'Q3 Vendor Invoices — FINAL (v7) — URGENT', with six PDFs, two Excel files, and one ZIP buried in replies. You’re supposed to extract them all before noon. Opening each message? Clicking ‘Save As’? That’s 8 minutes — and one missed attachment. There’s a faster way.

Quick Answer

You can’t download Outlook email attachments using Python directly from the Outlook desktop client — but you can, reliably, by connecting to Outlook via COM (Windows only) or switching to Microsoft Graph API (cross-platform). For most internal corporate users on Windows, use pywin32 with Outlook’s COM interface: File > Options > Trust Center > Trust Center Settings > Macro Settings > Enable all macros (temporarily), then run Python code that accesses Outlook.Application and iterates MailItem.Attachments. It works with delegate mailboxes and shared mailboxes if your account has Full Access permissions.

Step-by-Step Walkthrough

  1. Install pywin32: Run pip install pywin32 in Command Prompt (Admin). Then run python Scripts/pywin32_postinstall.py -install from your Python Scripts folder — this registers COM objects. Skip this, and your script will fail silently.
  2. Launch Outlook first: The COM interface requires Outlook.exe to be running. No background service. If Outlook isn’t open, win32com.client.Dispatch('Outlook.Application') raises an exception. Start Outlook manually — even minimized — before running your script.
  3. Identify the target folder: Use the Outlook Object Model path: Namespace.GetDefaultFolder(6) for Inbox (6 = olFolderInbox), or navigate deeper with GetSharedDefaultFolder for shared mailboxes. Example for shared mailbox ‘procurement@company.com’: Namespace.CreateRecipient('procurement@company.com').Resolve(), then Namespace.GetSharedDefaultFolder(recipient, 6).
  4. Loop & save attachments: For each MailItem, iterate item.Attachments. Use .SaveAsFile(os.path.join(save_dir, att.FileName)). Avoid att.DisplayName — it’s unreliable. Stick to att.FileName. Also: skip zero-byte files (att.Size == 0) — these are placeholders for linked content.
  5. Add error handling: Wrap attachment saves in try/except. Some filenames contain illegal characters (e.g., / \ : * ? " < > |). Sanitize with re.sub(r'[\\/:*?"<>|]', '_', att.FileName). Also catch pywintypes.com_error — common when an attachment is embedded OLE (like Excel charts) and not a standalone file.

Common Pitfalls

  • You’re trying this on macOS or Linux. pywin32 only works on Windows. Outlook for Mac doesn’t expose COM. Use Microsoft Graph API instead — but that requires Azure app registration and token auth, not just local script access.
  • You’re running the script while Outlook is in Cached Exchange Mode and the target folder hasn’t synced locally. The COM interface reads from the local OST, not the server. Force sync: right-click the folder > Update Folder, or call MAPIFolder.SyncObject.Sync() in code.
  • You’ve enabled Outlook security warnings (e.g., “A program is trying to access…”). These pop up per attachment save and block automation. Disable temporarily via Group Policy: Computer Configuration > Administrative Templates > Microsoft Outlook > Security > “Warn me when a program tries to send email on my behalf” → Disabled.
  • You assume all attachments appear in MailItem.Attachments. They don’t. Embedded images in HTML bodies show up in MailItem.HTMLBody as cid: references — not in the Attachments collection. Those require parsing MIME or using Graph API’s message/attachments endpoint with $expand=attachments.

Pro Tips

The beauty of this approach is how cleanly it handles delegate access. If Sarah Chen (VP Marketing) granted you ‘Owner’ rights to her mailbox, your script sees her Inbox as a separate store — no extra auth needed. Just use Namespace.Stores.Item('Sarah Chen').GetRootFolder().Folders.Item('Inbox').

What most people don’t realize is that Outlook caches attachment metadata — including size and extension — even before downloading. So you can filter aggressively before saving: if att.Type == 5 and att.Size > 10240 and '.pdf' in att.FileName.lower(): (Type 5 = olByValue, i.e., real file; Type 6 = olEmbeddedItem, i.e., OLE object).

Here’s the counterintuitive one: Don’t use Attachments.Item(i) with index loops. It’s unstable across Outlook versions. Always iterate with for att in item.Attachments:. Index-based access fails silently in Outlook 2016 when attachments are added mid-loop — yes, really.

Troubleshooting

If your script runs but downloads zero files, check three things first:

  • Outlook version: Outlook 365 (monthly channel) supports full COM access. Outlook 2016 LTSC does too — but Outlook for Microsoft 365 (web-installed) sometimes restricts COM if installed via Click-to-Run with enterprise policies. Run outlook.exe /safe to test in Safe Mode — if it works there, an add-in is blocking COM.
  • Admin restrictions: Your IT team may have disabled COM programmability via registry key HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Office\16.0\Outlook\Security\DisableCOMAddins set to 1. Only admins can change this.
  • Shared mailbox timing: GetSharedDefaultFolder returns None if the recipient isn’t resolved. Add a 500ms sleep after recipient.Resolve() — especially in Outlook 365 v2308+. Race condition confirmed in build 16.0.16924.20128.

For non-Windows users or strict security environments, Graph API is the fallback — but it’s slower and requires OAuth2 consent. You’ll need https://graph.microsoft.com/Mail.Read scope, and messages must be retrieved with ?$expand=attachments to include attachment data. Unlike COM, Graph gives you base64-encoded content — so you decode and write bytes manually.

Your SituationBest MethodWhy
Windows, local admin, internal Exchangepywin32 + COMFastest, no tokens, works with delegates/shared mailboxes out of the box
Mac/Linux or MFA-enforced tenantMicrosoft Graph APIOnly cross-platform, MFA-compatible option — but needs Azure app reg and user consent
Need embedded images (e.g., email signatures)Graph API + HTML parsingCOM ignores cid: references; Graph returns raw HTML + attachment list separately
Running unattended (e.g., scheduled task)pywin32 + Outlook running as logged-in userCOM won’t work under SYSTEM or headless sessions — Outlook must run interactively
Large attachments (>10 MB)Graph API with chunked uploadCOM hangs or crashes; Graph supports resumable uploads via createUploadSession
Testing on noreply@company.comUse Graph — not COMCOM requires interactive login; noreply accounts lack credentials for Outlook auth
IT blocks external Python toolsPowerShell + Outlook InteropSame COM layer, but PowerShell is often whitelisted where Python isn’t
Tom Bradley

Tom Bradley

Tom has 15 years of experience in office management and supply chain optimization. He shares practical tips for running efficient workplaces.