Excel Conversion Guide
Batch Convert Excel Files While Keeping Macros (2026 Guide)
You have 80 XLSM files. Your client needs them in XLSX. You drag them into CloudConvert, come back in 20 minutes, and discover every file converted successfully — with every macro silently deleted.
This happens because batch conversion tools optimise for throughput, not fidelity. They default to XLSX, XLSX cannot contain VBA macros by specification, and no warning is ever raised. The loss is invisible until you open a file and find the automation gone.
This guide shows you the two reliable methods for batch converting Excel files without macro loss.
Why Batch Converters Destroy Macros
XLSM is an XLSX ZIP archive with one extra file inside: xl/vbaProject.bin. This binary file holds all your VBA modules, class modules, user forms, and the project references. When a converter creates an XLSX output, it simply does not include this file. The conversion "succeeds" from the tool's perspective — the data is all there, formulas work, the file opens cleanly. But the entire VBA project is gone.
The problem is compounded in batch mode because:
- Most tools do not read source file extensions — they just use the output format you specify
- There is no per-file success/failure report that would surface macro loss
- Output files pass basic integrity checks (they open without errors)
- The loss is only discovered when someone actually tries to run a macro
Method 1: Python Script (Recommended for Developers)
The cleanest batch conversion approach is a Python script that copies each XLSM's data structure while preserving the vbaProject.bin. The key insight: an XLSM is just a ZIP file, and the VBA project is a binary blob — you can relocate it without parsing it.
Prerequisites
pip install openpyxl
The script
import os
import shutil
import zipfile
from pathlib import Path
def batch_convert_macro_safe(input_dir, output_dir, target_format='xlsm'):
"""
Convert all XLSM/XLS files in input_dir to target_format
while preserving VBA macros.
target_format: 'xlsm' (recommended) or 'xlsb'
"""
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
results = {'success': 0, 'failed': 0, 'macro_preserved': 0, 'no_macro': 0}
source_files = list(input_path.glob('**/*.xlsm')) + list(input_path.glob('**/*.xlsx'))
for src_file in source_files:
out_file = output_path / src_file.with_suffix(f'.{target_format}').name
try:
# Copy source to temp
tmp = out_file.with_suffix('.tmp.zip')
shutil.copy2(src_file, tmp)
# Inspect for vbaProject.bin
has_vba = False
with zipfile.ZipFile(tmp, 'r') as z:
has_vba = 'xl/vbaProject.bin' in z.namelist()
# Rename to target extension
shutil.move(str(tmp), str(out_file))
if has_vba:
results['macro_preserved'] += 1
print(f"[OK - MACROS] {src_file.name} → {out_file.name}")
else:
results['no_macro'] += 1
print(f"[OK - no macros] {src_file.name} → {out_file.name}")
results['success'] += 1
except Exception as e:
results['failed'] += 1
print(f"[FAILED] {src_file.name}: {e}")
print(f"\n=== Batch complete ===")
print(f"Converted: {results['success']} | Failed: {results['failed']}")
print(f"Macros preserved: {results['macro_preserved']} | No macros: {results['no_macro']}")
return results
# Usage
batch_convert_macro_safe('./input', './output', target_format='xlsm')
This script keeps the XLSM extension (which retains the VBA project), reports per-file macro status, and fails loudly rather than silently. Run it with python batch_convert.py from the directory containing your files.
Batch conversion checklist included in the Kit
The Macro-Safe Converter Kit includes the complete batch script, a pre-conversion macro inventory tool, format decision matrix, and post-conversion validation checklist.
Get the Kit — $9One-time · Instant download · 30-day guarantee
Method 2: LibreOffice Headless CLI
If you prefer not to write Python, LibreOffice's headless mode can batch-convert Excel files while keeping macros — but only if you use the right output filter.
Install LibreOffice (if not already installed)
# macOS
brew install libreoffice
# Ubuntu/Debian
sudo apt install libreoffice
# Windows: download from libreoffice.org
Batch conversion command
# Convert all XLSM in /input to XLSM in /output (macro-safe)
for f in /input/*.xlsm; do
libreoffice --headless \
--infilter="Calc MS Excel 2007 XML" \
--convert-to xlsm:"Calc MS Excel 2007 VBA XML" \
--outdir /output "$f"
done
The critical detail is the output filter: Calc MS Excel 2007 VBA XML. This is LibreOffice's macro-aware XLSM filter. The default xlsm conversion without the explicit filter sometimes strips macros — always specify the full filter name.
LibreOffice batch: what works and what doesn't
| Operation | Macro preservation | Notes |
|---|---|---|
| XLSM → XLSM (VBA XML filter) | Preserved | Use explicit filter name |
| XLSM → XLSX (any filter) | Destroyed | XLSX cannot hold VBA |
| XLS → XLSM (VBA XML filter) | Usually preserved | May fail on complex XLS VBA |
| XLSM → ODS | Destroyed | ODS uses a different macro system |
| XLSM → PDF | N/A | PDF has no macro concept; content preserved |
Pre-Conversion: Inventory Your Macros
Before running any batch conversion, take a macro inventory. This gives you a before/after comparison point.
import zipfile
from pathlib import Path
def inventory_macros(directory):
for f in Path(directory).glob('**/*.xlsm'):
with zipfile.ZipFile(f) as z:
has_vba = 'xl/vbaProject.bin' in z.namelist()
vba_size = 0
if has_vba:
vba_size = z.getinfo('xl/vbaProject.bin').file_size
print(f"{f.name}: {'HAS MACROS' if has_vba else 'no macros'} ({vba_size:,} bytes)")
inventory_macros('./input')
Post-Conversion Validation
After batch conversion, always validate. The fastest check is comparing vbaProject.bin presence between source and output:
import zipfile
from pathlib import Path
def validate_batch(input_dir, output_dir):
issues = 0
for src in Path(input_dir).glob('*.xlsm'):
out = Path(output_dir) / src.name
if not out.exists():
print(f"[MISSING] {src.name} not found in output")
issues += 1
continue
with zipfile.ZipFile(src) as z_in, zipfile.ZipFile(out) as z_out:
src_has_vba = 'xl/vbaProject.bin' in z_in.namelist()
out_has_vba = 'xl/vbaProject.bin' in z_out.namelist()
if src_has_vba and not out_has_vba:
print(f"[MACRO LOSS] {src.name}: macros in source, missing from output")
issues += 1
if issues == 0:
print("All macros validated successfully.")
else:
print(f"{issues} issue(s) found — check affected files before deleting originals.")
validate_batch('./input', './output')
Comparison: Batch Conversion Tool Options
| Tool | Batch support | Macro preservation | Cost |
|---|---|---|---|
| CloudConvert | Yes | Destroys macros | Free/paid |
| Zamzar | Yes (paid) | Destroys macros | Paid |
| iLovePDF | Yes | Destroys macros | Free/paid |
| Python + zipfile | Yes (script) | Preserved | Free |
| LibreOffice CLI (VBA filter) | Yes (shell loop) | Usually preserved | Free |
| Excel + VBA macro | Yes (VBA loop) | Preserved | Excel licence |
FAQ
Can I batch convert XLSM files without losing macros?
Yes, but not with most online or GUI batch converters. These strip macros silently. The reliable methods are a Python script (using the ZIP-level copy approach) or LibreOffice headless CLI with the Calc MS Excel 2007 VBA XML filter explicitly specified.
Why do batch converters destroy macros when single-file converters sometimes don't?
Batch converters prioritise throughput over compatibility. They default to XLSX output and never check whether the source contained macros. Single-file GUI converters sometimes give a warning (Excel does), but this warning disappears in batch/API modes.
How do I validate that macros survived a batch conversion?
Compare the xl/vbaProject.bin file presence between source and output ZIP archives. The validation script above automates this comparison across entire directories in seconds.
Related Guides
Stop losing macros on every conversion
The Macro-Safe Converter Kit includes the complete batch script, pre-conversion inventory tool, and post-conversion validation checklist.
Get the Kit — $9One-time · Instant download · 30-day guarantee