How-to
Getting Started With Python Excel AutomationDeep dive

Convert CSV Files to Excel with Python

Turn CSV exports into proper Excel workbooks — detect the encoding and delimiter, keep IDs as text, parse dates, combine many CSVs into one workbook with a sheet each, split files larger than Excel's row limit, and format the result.

CSV is how most systems export data, and Excel is how most people want to receive it. Opening a CSV directly in Excel is the source of many familiar problems — dates flipped between day and month, codes stripped of their leading zeros, accented names garbled — because Excel guesses at every column. Converting with Python moves those decisions into code, where they are made once, explicitly, for every file. This guide, part of Handling Excel File Formats and Conversions, converts single files and folders of files, and handles the limits Excel imposes on the result.

A folder of CSV exports combined into one workbook Three monthly CSV exports are each read with explicit encoding, delimiter and column types, then written as one sheet each in a single Excel workbook named after the quarter. sales_q1.xlsx one sheet per CSV sales_2026_01.csv UTF-8, comma sales_2026_02.csv cp1252, comma sales_2026_03.csv UTF-8, semicolon every file is read with explicit settings, so no column is left to Excel's guesswork

Prerequisites

Bash
pip install pandas xlsxwriter

The examples create three small CSVs that behave like real exports — different encodings, one with semicolons, and a code column with leading zeros:

Python
from pathlib import Path

Path("exports").mkdir(exist_ok=True)
Path("exports/sales_2026_01.csv").write_text(
    "customer_id,name,order_date,amount\n00123,Zoë Adams,2026-01-05,1200.50\n00456,Ben Okafor,2026-01-19,980.00\n",
    encoding="utf-8")
Path("exports/sales_2026_02.csv").write_text(
    "customer_id,name,order_date,amount\n00789,José Núñez,2026-02-02,455.25\n",
    encoding="cp1252")
Path("exports/sales_2026_03.csv").write_text(
    "customer_id;name;order_date;amount\n00123;Zoë Adams;2026-03-11;310,00\n",
    encoding="utf-8")

Step 1 — Convert one file, with explicit types

The one-liner works, but it lets pandas infer every type. State the ones that matter: identifiers as text, dates as dates.

Python
import pandas as pd

df = pd.read_csv(
    "exports/sales_2026_01.csv",
    dtype={"customer_id": str},
    parse_dates=["order_date"],
)
df.to_excel("sales_2026_01.xlsx", index=False)
print(df.dtypes)
print(df["customer_id"].tolist())   # ['00123', '00456']

dtype={"customer_id": str} keeps 00123 intact — read as a number it would become 123, and a lookup against a customer list keyed on text would miss every row. index=False stops pandas from writing its row numbers as an extra first column.

Step 2 — Detect the encoding and the delimiter

Exports from different systems disagree on both. A small reader that tries the likely encodings in order and lets Python's CSV sniffer choose the delimiter copes with most of them:

Python
import csv

def read_export(path, **kwargs) -> pd.DataFrame:
    raw = Path(path).read_bytes()
    for encoding in ("utf-8-sig", "cp1252"):
        try:
            text = raw.decode(encoding)
            break
        except UnicodeDecodeError:
            continue
    else:
        raise ValueError(f"{path}: not UTF-8 or Windows-1252")
    dialect = csv.Sniffer().sniff(text.splitlines()[0], delimiters=",;\t|")
    return pd.read_csv(path, encoding=encoding, sep=dialect.delimiter, **kwargs)

for f in sorted(Path("exports").glob("*.csv")):
    frame = read_export(f, dtype={"customer_id": str})
    print(f.name, frame.shape, frame["name"].iloc[0])

utf-8-sig comes first because it reads plain UTF-8 and also strips the byte-order mark that Excel adds when it saves "CSV UTF-8". Windows-1252 is the usual fallback for files saved by older Excel on Windows. Order matters: almost any bytes decode as Windows-1252, so trying it first would "succeed" on UTF-8 files and garble them.

Step 3 — Fix decimal commas

The March file uses semicolons and a decimal comma (310,00) — the European convention. Tell read_csv about both, and the amount arrives as a number:

Python
march = read_export("exports/sales_2026_03.csv", dtype={"customer_id": str}, decimal=",")
print(march["amount"].tolist())    # [310.0]

Pass decimal="," only for files that use it: applied to a comma-delimited file with ordinary decimals it does nothing useful, and applied to a semicolon file with thousands separators you also need thousands=".". Knowing each source's conventions and recording them in code is what makes the conversion reliable.

A European CSV read with explicit settings The raw file uses semicolons between fields and a comma as the decimal mark, and the customer ID has leading zeros. Read with sep semicolon, decimal comma and customer_id as text, the result has the ID intact and the amount as the number 310. 1 customer_id;name;…;amount 2 00123;Zoë Adams;…;310,00 A B C 1 customer_id name amount 2 00123 Zoë Adams 310.0 raw CSV text DataFrame sep=';', decimal=','

Step 4 — Many CSVs, one workbook

Readers usually want one workbook, not a folder of them. Write each CSV to its own sheet, named after the file, through a single writer:

Python
with pd.ExcelWriter("sales_q1.xlsx", engine="xlsxwriter", datetime_format="yyyy-mm-dd") as writer:
    for f in sorted(Path("exports").glob("*.csv")):
        decimal = "," if f.name.endswith("_03.csv") else "."
        frame = read_export(f, dtype={"customer_id": str}, parse_dates=["order_date"], decimal=decimal)
        sheet = f.stem.replace("sales_", "")[:31]          # '2026_01'
        frame.to_excel(writer, sheet_name=sheet, index=False)
        ws = writer.sheets[sheet]
        ws.freeze_panes(1, 0)
        ws.set_column(0, len(frame.columns) - 1, 16)

print(pd.ExcelFile("sales_q1.xlsx").sheet_names)   # ['2026_01', '2026_02', '2026_03']

Sheet names are limited to 31 characters and may not contain [ ] : * ? / \; a file name is usually safe after trimming, but see Create a Sheet for Each Group in a DataFrame for a helper that cleans and de-duplicates arbitrary names. If the files share columns and should be one table rather than several sheets, concatenate them first with a source_file column instead. One long table is easier to filter, pivot and chart than a dozen tabs with identical headers, and it reads back into pandas in a single call; separate sheets suit readers who look at one period at a time.

Step 5 — Files bigger than one sheet

An Excel sheet holds 1,048,576 rows including the header, so 1,048,575 data rows at most. pandas checks the data rows against the full limit, which leaves an off-by-one gap: a frame of exactly 1,048,576 rows passes the check, and the xlsxwriter engine then writes the header plus every row but the last — silently. The last row is simply missing. Check the limit yourself and split large files across sheets:

Python
MAX_DATA_ROWS = 1_048_576 - 1          # one row is the header

def write_split(frame: pd.DataFrame, path: str, base: str = "data") -> list[str]:
    names = []
    with pd.ExcelWriter(path, engine="xlsxwriter") as writer:
        for part, start in enumerate(range(0, len(frame), MAX_DATA_ROWS), start=1):
            name = f"{base}_{part}"
            frame.iloc[start:start + MAX_DATA_ROWS].to_excel(writer, sheet_name=name, index=False)
            names.append(name)
    return names

small_demo = pd.DataFrame({"n": range(10)})
MAX_DATA_ROWS = 4                       # tiny limit just to show the split
print(write_split(small_demo, "split_demo.xlsx"))   # ['data_1', 'data_2', 'data_3']
MAX_DATA_ROWS = 1_048_576 - 1

The demonstration uses a tiny limit so the split is visible; with the real limit, the same function turns a 2.5-million-row CSV into three sheets. The underlying error when you exceed the limit by more than one row is explained in Fix "This sheet is too large" When Writing Excel with pandas.

What to do with a CSV of a given size Up to about 100,000 rows, convert directly. Up to Excel's limit of 1,048,575 data rows, convert with xlsxwriter in constant memory mode. Beyond the limit, split across sheets or deliver a summary in Excel and the full data as CSV or Parquet. How many data rows? < 100k convert directly default settings up to 1,048,575 constant_memory xlsxwriter option more split or summarise sheets or keep CSV

Step 6 — Verify the conversion

A conversion job should prove it lost nothing. Read the workbook back and compare, per source file, the row count and the total of a numeric column with the CSV it came from. It takes a second and catches every silent failure described above — a dropped last row, a mis-detected delimiter that collapsed columns, a decimal comma read as text:

Python
checks = []
workbook = pd.read_excel("sales_q1.xlsx", sheet_name=None)
for f in sorted(Path("exports").glob("*.csv")):
    decimal = "," if f.name.endswith("_03.csv") else "."
    source = read_export(f, dtype={"customer_id": str}, decimal=decimal)
    written = workbook[f.stem.replace("sales_", "")]
    checks.append({
        "file": f.name,
        "rows_match": len(source) == len(written),
        "amount_match": abs(source["amount"].sum() - written["amount"].sum()) < 0.005,
    })
result = pd.DataFrame(checks)
print(result)
assert result[["rows_match", "amount_match"]].all().all()

Keep the assertion in the scheduled version of the job: a conversion that fails loudly on a bad day is far better than one that delivers a workbook with a missing month nobody notices until the quarter closes.

Common pitfalls

  • Letting Excel open the CSV instead. Double-clicking a CSV applies Excel's own guesses; converting in Python is the only way to control types.
  • Trying Windows-1252 before UTF-8. It decodes almost anything, so it silently garbles UTF-8 text. Try utf-8-sig first.
  • Dates as text. Without parse_dates, dates are written as strings, which Excel cannot sort or filter as dates. Parse them, and set datetime_format on the writer for a clean display.
  • Formulas injected from data. A CSV value starting with =, +, - or @ is still text when written by pandas; it only becomes a formula if someone re-imports the CSV in Excel. Converting through pandas is the safer path for untrusted exports.

Performance and scale notes

Reading a CSV is fast; writing xlsx is the slow part, because every cell becomes XML. For hundreds of thousands of rows use the xlsxwriter engine with engine_kwargs={"options": {"constant_memory": True}}, which streams rows to disk instead of holding the whole sheet in memory. For millions of rows, ask whether the reader really needs them in Excel — often a summary sheet plus the full data as CSV or Parquet serves them better. The trade-offs are in Excel vs CSV vs Parquet for Python Data Pipelines.

Conclusion

Convert CSVs with explicit settings: identifier columns as text, dates parsed, the encoding tried in the right order, the delimiter and decimal mark matched to the source. Combine a folder into one workbook with a sheet per file through a single writer, and respect the real sheet limit of 1,048,575 data rows — checking it yourself, because a frame of exactly 1,048,576 rows loses its last row silently with xlsxwriter.

Frequently asked questions

What is the simplest way to convert a CSV to Excel in Python? pd.read_csv("data.csv").to_excel("data.xlsx", index=False). The rest of this guide is about the details that one line gets wrong — encodings, delimiters, codes with leading zeros, dates, and files too big for one sheet.

Why are my accented characters garbled after converting? The CSV was not saved as UTF-8 — Excel on Windows often writes Windows-1252. Pass the right encoding to read_csv, or try "utf-8-sig" first and fall back to "cp1252".

How many rows can one Excel sheet hold? 1,048,576 rows including the header row, so at most 1,048,575 data rows under a header. Larger files must be split across sheets or kept in another format.

Why did my ZIP codes lose their leading zeros? read_csv inferred the column as numbers. Read identifier columns as text with dtype={"zip": str} so 01234 stays 01234.

More on file formats & conversion

More on file formats & conversion →