CSV-to-XLSX in Python comes down to two questions: do you already have pandas in your project, and do you need type detection? openpyxl handles most conversions cleanly with just the stdlib csv module. pandas handles numbers, dates, and nulls automatically at the cost of a heavier dependency. The ChangeThisFile API is the right call when you want XLSX without adding any library at all.
TL;DR
| Method | Install | Best for |
|---|---|---|
| openpyxl | pip install openpyxl | Simple conversions, minimal deps, full formatting control |
| pandas | pip install pandas openpyxl | Already using pandas, need type inference (dates, numbers) |
| ChangeThisFile API | None | No local libs, serverless, or just want it done |
Method 1: openpyxl (lightweight, full control)
openpyxl writes XLSX natively with no Excel required. Pair it with Python's built-in csv module for robust CSV parsing.
pip install openpyxl
import csv
from pathlib import Path
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
def csv_to_xlsx(csv_path: str, xlsx_path: str) -> None:
wb = Workbook()
ws = wb.active
ws.title = "Data"
header_font = Font(bold=True)
header_fill = PatternFill(
start_color="D9E1F2",
end_color="D9E1F2",
fill_type="solid",
)
with open(csv_path, newline="", encoding="utf-8-sig") as f:
reader = csv.reader(f)
for row_idx, row in enumerate(reader, start=1):
for col_idx, value in enumerate(row, start=1):
cell = ws.cell(row=row_idx, column=col_idx, value=value)
if row_idx == 1:
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center")
# Auto-fit column widths (capped at 50 chars)
for col in ws.columns:
max_len = max((len(str(c.value or "")) for c in col), default=10)
ws.column_dimensions[col[0].column_letter].width = min(max_len + 2, 50)
wb.save(xlsx_path)
csv_to_xlsx("data.csv", "data.xlsx")
print("Done")
Using utf-8-sig as the encoding strips the BOM that Excel sometimes adds to CSV exports — without it, the first column header gets a leading \ufeff character. For large CSVs, the manual width calculation is the slowest part; skip it if you don't need formatted output.
Method 2: pandas (one-liner, automatic type detection)
pandas parses CSVs into a DataFrame and writes XLSX in one method call. Automatically detects numeric columns, dates, and handles nulls as empty cells.
pip install pandas openpyxl
import pandas as pd
def csv_to_xlsx(csv_path: str, xlsx_path: str) -> None:
df = pd.read_csv(csv_path)
df.to_excel(xlsx_path, index=False)
csv_to_xlsx("data.csv", "data.xlsx")
# With ExcelWriter for more control (column widths, multiple sheets)
def csv_to_xlsx_styled(csv_path: str, xlsx_path: str) -> None:
df = pd.read_csv(csv_path)
with pd.ExcelWriter(xlsx_path, engine="openpyxl") as writer:
df.to_excel(writer, sheet_name="Data", index=False)
ws = writer.sheets["Data"]
# Auto-fit column widths
for col in ws.columns:
max_len = max(
len(str(col[0].value or "")), # header
*[len(str(c.value or "")) for c in col[1:]],
)
ws.column_dimensions[col[0].column_letter].width = min(max_len + 2, 50)
csv_to_xlsx_styled("data.csv", "data.xlsx")
pandas infers types automatically: columns that look like numbers become floats, ISO-format dates become datetime objects. If you want all columns as strings (preserving leading zeros in zip codes, for example), pass dtype=str to read_csv().
Memory note: pandas loads the entire CSV into RAM before writing. For CSVs over a few hundred MB, use openpyxl with the stdlib csv module (Method 1) to stream row-by-row.
Method 3: ChangeThisFile API (requests, no local library)
POST the CSV to the API, receive XLSX. Source is auto-detected from the filename — pass only target=xlsx. Free tier: 1,000 conversions/month, no card needed.
# Test with curl first
curl -X POST https://changethisfile.com/v1/convert \
-H "Authorization: Bearer ctf_sk_your_key" \
-F "file=@data.csv" \
-F "target=xlsx" \
--output data.xlsx
import requests
from pathlib import Path
API_KEY = "ctf_sk_your_key_here"
def csv_to_xlsx_api(csv_path: str, xlsx_path: str) -> None:
with open(csv_path, "rb") as f:
response = requests.post(
"https://changethisfile.com/v1/convert",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": (Path(csv_path).name, f, "text/csv")},
data={"target": "xlsx"},
timeout=60,
)
response.raise_for_status()
Path(xlsx_path).write_bytes(response.content)
csv_to_xlsx_api("data.csv", "data.xlsx")
print("Done")
The API returns an XLSX file directly in the response body. No ZIP, no polling — just write the bytes to disk. For batch conversions, use concurrent.futures.ThreadPoolExecutor with max_workers=10 to parallelize requests.
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| openpyxl | Minimal deps, streaming large files, full formatting control | No automatic type detection — all values written as strings unless you parse them |
| pandas | Already using pandas, need type inference, complex transformations | Heavy dependency (NumPy, pandas) for a simple conversion task |
| ChangeThisFile API | No local libs, serverless functions, low-volume one-off conversions | Network call; 25MB file limit on free tier |
Production tips
- Always open CSVs with newline="". Python's csv module expects
open(path, newline="")— omitting it causes double-newline issues on Windows files where lines end with \r\n. - Handle UTF-8 BOM from Excel exports. Excel saves CSVs with a UTF-8 BOM (\ufeff). Open with
encoding="utf-8-sig"to strip it silently, or the first column header will have a spurious leading character. - Preserve leading zeros with dtype=str. pandas infers "00123" as 123 (integer). Pass
dtype=strtoread_csv()if your data has zip codes, phone numbers, or any zero-padded identifiers. - Use openpyxl's write-only mode for very large files.
Workbook(write_only=True)uses significantly less memory than the default mode for 500k+ row XLSX files — rows are flushed to disk as they're appended. - Freeze the header row for usability. After writing headers:
ws.freeze_panes = "A2"keeps the header row visible when scrolling down in Excel.
For most Python projects, openpyxl with the stdlib csv module is the cleanest choice — no heavy dependencies and row-by-row streaming for large files. If you're already using pandas, df.to_excel() takes one line. The ChangeThisFile API is the right call when you want no local library at all. Free tier: 1,000 conversions/month.