How-to
Automating Reporting WorkflowsDeep dive

Zip Large Excel Attachments Before Emailing Them

An .xlsx is already a zip file, so zipping it saves only a few percent — measured here at 4% on an 8.6 MB workbook. Find out what actually shrinks a report enough to email: size budgets that include base64 overhead, repacking at maximum compression, CSV inside a zip for raw data, and splitting attachments across messages.

A monthly export grows until one day the email bounces: "message size exceeds fixed limit". The instinctive fix is to zip the workbook — and it barely helps, because an .xlsx file is already a zip archive of compressed XML. This guide, part of Emailing Excel Reports with smtplib, measures what each option really saves on a 200,000-row workbook, builds a size budget that accounts for email encoding, and shows the approaches that shrink a report enough to send — and when to stop attaching files at all.

The same 200,000 rows in different packages Measured sizes for one dataset of 200,000 rows and eight columns. The xlsx workbook is 8.59 MB. Zipping it gives 8.27 MB and LZMA compression 7.82 MB. Repacking the workbook's contents at maximum compression gives 8.16 MB. The raw CSV is 13.96 MB, but the CSV inside a zip is only 2.93 MB. CSV, raw 13.96 MB xlsx as written 8.59 MB xlsx zipped 8.27 MB xlsx repacked 8.16 MB xlsx LZMA zip 7.82 MB CSV zipped 2.93 MB zipping a workbook saves a few percent; zipping text saves most of it

Prerequisites

Python's standard library covers everything here: zipfile, io and email. pandas is used for the CSV example:

Bash
pip install pandas openpyxl

A stand-in report, large enough to show the effect:

Python
import io
import zipfile
from pathlib import Path

import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
n = 50_000
frame = pd.DataFrame({
    "order_id": np.arange(n),
    "region": rng.choice(["North", "South", "East", "West"], n),
    "product": rng.choice([f"SKU-{i:04d}" for i in range(500)], n),
    "amount": (rng.random(n) * 1000).round(2),
})
frame.to_excel("export.xlsx", index=False)
print(round(Path("export.xlsx").stat().st_size / 1e6, 2), "MB")

Step 1 — Budget for the encoded size

Mail servers limit the size of the whole message, and attachments travel base64-encoded, which turns every 3 bytes into 4. A 25 MB limit therefore allows only about 18 MB of raw attachments once headers and the body are counted. Check before sending, not after the bounce:

Python
import math

def encoded_size(raw_bytes):
    return 4 * math.ceil(raw_bytes / 3) + raw_bytes // 57 * 2      # base64 plus line breaks

LIMIT = 25 * 1024 * 1024
HEADROOM = 256 * 1024                                              # headers, body, MIME parts

def fits(paths):
    total = sum(encoded_size(Path(p).stat().st_size) for p in paths)
    return total + HEADROOM <= LIMIT, total

ok, total = fits(["export.xlsx"])
print(ok, round(total / 1e6, 2), "MB encoded")

Base64 lines are 76 characters with a line break, which is where the extra two bytes per 57 come from. Limits differ by provider and by the receiving side, which may be stricter than yours; if you do not know the recipients' limit, budget for 10 MB. Sending the report itself is covered in Send an Excel Report with Gmail from Python.

Step 2 — Why zipping a workbook saves so little

An .xlsx is a zip container whose XML parts are already deflate-compressed. Compressing compressed data again gains almost nothing:

Python
def zip_bytes(name, data, method=zipfile.ZIP_DEFLATED, level=9):
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w", method, compresslevel=level) as archive:
        archive.writestr(name, data)
    return buffer.getvalue()

workbook = Path("export.xlsx").read_bytes()
zipped = zip_bytes("export.xlsx", workbook)
print(round(1 - len(zipped) / len(workbook), 3))       # a few percent at most

On the 200,000-row test workbook, plain zipping saved 3.7% and the slower LZMA method 9%. Neither turns a message that is too large into one that fits, unless it was only just over. The one workbook-level gain that is free is repacking: rewriting the workbook's own parts at the maximum compression level. It saved 5% on the test file, and the result is still a normal .xlsx:

Python
def repack_xlsx(src, dst):
    with zipfile.ZipFile(src) as original, \
         zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as smaller:
        for item in original.infolist():
            smaller.writestr(item.filename, original.read(item.filename))

repack_xlsx("export.xlsx", "export_small.xlsx")
print(pd.read_excel("export_small.xlsx").shape)        # (50000, 4) — still readable
Why a zipped workbook stays the same size The xlsx file is already a zip archive containing deflate-compressed XML parts. Zipping it again wraps compressed data in another compression layer, which finds almost nothing left to remove. Text such as CSV has not been compressed yet, so zipping it removes most of its size. Outer zip (your attachment) finds little to compress xlsx container (already a zip) deflate-compressed parts sheet XML (60 MB uncompressed) compressed inside the xlsx

Step 3 — Send raw data as a zipped CSV

When the attachment is a data export rather than a formatted report — no charts, no styling, just rows — CSV in a zip is dramatically smaller. The same 200,000 rows were 13.96 MB as CSV and 2.93 MB zipped, a third of the workbook's size:

Python
csv_bytes = frame.to_csv(index=False).encode("utf-8")
bundle = zip_bytes("export.csv", csv_bytes)
Path("export_csv.zip").write_bytes(bundle)
print(round(len(bundle) / len(workbook), 2), "× the workbook size")

The trade-off is what CSV loses: types, formatting, several sheets, formulas. Recipients opening it in Excel may see dates and leading zeros mangled by Excel's import. It suits data handed to another system or an analyst, not a finished report for managers. Converting in the other direction is covered in Convert Excel to CSV with Python.

Step 4 — Several files: pack them into messages under the budget

With a batch of regional workbooks, zipping them together helps a little and bundling them into as few messages as fit helps more. A simple first-fit packing keeps every message under the budget:

Python
def pack_into_messages(paths, budget=LIMIT - HEADROOM):
    messages = []
    for path in sorted(paths, key=lambda p: Path(p).stat().st_size, reverse=True):
        size = encoded_size(Path(path).stat().st_size)
        if size > budget:
            raise ValueError(f"{path} alone exceeds the budget; link to it instead")
        for message in messages:
            if message["size"] + size <= budget:
                message["files"].append(path)
                message["size"] += size
                break
        else:
            messages.append({"files": [path], "size": size})
    return messages

for region in ["North", "South", "East"]:
    frame[frame["region"] == region].to_excel(f"export_{region}.xlsx", index=False)
plan = pack_into_messages([f"export_{r}.xlsx" for r in ["North", "South", "East"]], budget=900_000)
print([m["files"] for m in plan])
# [['export_South.xlsx', 'export_East.xlsx'], ['export_North.xlsx']]

Each regional workbook is about 0.3 MB, 0.4 MB once encoded, so with a 0.9 MB budget two fit in the first message and the third goes in a second. Largest-first ordering keeps the number of messages low without any clever optimisation. Label split messages "part 1 of 2" in the subject so recipients know to expect the rest. Attaching several files to one message is covered in Attach Multiple Excel Files to One Email in Python.

Step 5 — Know when to stop attaching

Every option above has a ceiling. Beyond it — or when the data is confidential — upload the file to shared storage and email a link: recipients get the current version, access can be revoked, and nothing large travels through mail servers. Encrypted zips are a weak substitute: Python's zipfile cannot write them, third-party AES zips need the recipient to have a tool that opens them, and the password still has to be sent somehow. Uploading reports is covered in Upload an Excel Report to Azure Blob Storage with Python.

Choosing how to send a large report If the report fits the encoded budget, attach it as it is, repacked for a few percent. If it is raw data, send CSV inside a zip. If there are several files, pack them into as few messages as fit. If a single file is still too large or the content is confidential, upload it and send a link. What is being sent, and does it fit? fits the budget attach as is repack for a few % raw data export CSV in a zip about a third the size several files pack into messages part 1 of n too big or confidential upload and link access control

Common pitfalls

  • Zip attachments blocked. Many corporate gateways strip or quarantine .zip files. Confirm with the recipients before switching to zips.
  • Measuring the file, not the message. A 20 MB file is a 27 MB message. Budget with the encoded size.
  • Shrinking by deleting. Dropping columns or rows to fit a limit changes the report. Split or link instead, and say so in the email.
  • Forgetting the recipient's side. Your server may accept 35 MB while the recipient's gateway rejects anything over 10 MB, and that bounce arrives hours later, after the job has reported success. When the recipients are outside your organisation, use the smallest limit you have seen from them.
  • Repacking with the wrong entry order. Some strict readers expect [Content_Types].xml to be the first entry in the archive. The repacking loop above copies entries in their original order, which keeps it first; do not sort or filter the entries while repacking.
  • Zipping with the operating system's defaults. A zip built by a shell tool may use a storing method that does not compress at all, or add hidden folders such as __MACOSX. Building the archive in Python keeps the method and contents under your control.
  • Checking the size once. Reports grow month by month. Run the budget check on every send and log the encoded size, so the log shows the trend before the first bounce does.

Conclusion

Zipping an .xlsx saves a few percent because the workbook is already compressed; repacking it at maximum compression saves a little more. Budget attachments by their base64-encoded size, send pure data exports as zipped CSV — a third of the workbook's size in our test — pack several files into as few messages as fit, and switch to a shared link once files outgrow email.

Frequently asked questions

Does zipping an Excel file make it smaller? Only slightly. An .xlsx is itself a zip archive of compressed XML, so zipping it again saved about 4% on an 8.6 MB workbook in our test. Zipping helps much more for several files together or for CSV exports.

How large can an email attachment be? Common limits are around 20 to 25 MB for the whole message, and attachments are base64-encoded, which adds about a third to their size. In practice, keep the raw attachments under roughly 18 MB for a 25 MB limit.

What is the most effective way to shrink a large data export? Send the data as CSV inside a zip. The same 200,000 rows that made an 8.6 MB workbook compressed to 2.9 MB as a zipped CSV, because CSV text compresses far better than a workbook that is already compressed.

Can Python create a password-protected zip? The standard zipfile module can read but not write encrypted archives. A third-party package such as pyzipper can write AES-encrypted zips; alternatively share the file through a service with access control instead of email.

More on email delivery

More on email delivery →