Reading and Writing CSV Files in Python

CSV looks simple until a field contains a comma, a line break or a hidden byte-order mark. Reading and writing real-world CSV with Python's csv module: why splitting on commas fails, DictReader and DictWriter, utf-8-sig, semicolon files, types, large files, and when pandas is better.

GloryTecks Team

Training & Placement Faculty

6 May 2026 6 min read

Last updated: 24 September 2026

Share:

A CSV file is plain text with values separated by commas, which is exactly why it is used everywhere and why hand-written parsers break. Real exports contain names with commas in them, addresses with line breaks, quotes inside quotes, a byte-order mark added by Excel, or semicolons instead of commas. Python's csv module handles all of that (Python docs: csv); this guide shows each problem and the one-line fix.

Why splitting on commas fails

The CSV convention, written down in RFC 4180, puts a field in double quotes when it contains a comma, a double quote or a line break, and doubles any quote inside it (RFC 4180). A naive split(",") knows none of this:

python
import csv
from pathlib import Path

Path("customers.csv").write_text(
    'id,name,address,note\n'
    '1,"Rao, Asha","Flat 4B\nBanjara Hills",VIP\n'
    '2,Ravi Kumar,Madhapur,"said ""call after 6"""\n',
    encoding="utf-8", newline="",
)

with open("customers.csv", encoding="utf-8", newline="") as f:
    print(f.readline().rstrip().split(","), f.readline().rstrip().split(","))

with open("customers.csv", encoding="utf-8", newline="") as f:
    for row in csv.reader(f):
        print(row)
text
['id', 'name', 'address', 'note'] ['1', '"Rao', ' Asha"', '"Flat 4B']
['id', 'name', 'address', 'note']
['1', 'Rao, Asha', 'Flat 4B\nBanjara Hills', 'VIP']
['2', 'Ravi Kumar', 'Madhapur', 'said "call after 6"']

Splitting broke the name in two and cut the address at its line break. The csv module read every record correctly, including the doubled quotes. Always open CSV files with newline="" so that line breaks inside quoted fields reach the parser intact; the documentation requires it.

Rows as dictionaries

DictReader uses the header row as keys, so code refers to columns by name and keeps working if a column is added or moved:

python
with open("customers.csv", encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    print(reader.fieldnames)
    names = [row["name"] for row in reader]
print(names)
text
['id', 'name', 'address', 'note']
['Rao, Asha', 'Ravi Kumar']

Writing: DictWriter and quoting

Writing is the mirror image, and the module adds quotes wherever a value needs them:

python
rows = [
    {"sku": "PEN-01", "name": "Gel pen, blue", "price": 25.5},
    {"sku": "PAD-02", "name": 'Notepad "A5"', "price": 120},
]
with open("products.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["sku", "name", "price"])
    writer.writeheader()
    writer.writerows(rows)

print(Path("products.csv").read_text(encoding="utf-8"))
text
sku,name,price
PEN-01,"Gel pen, blue",25.5
PAD-02,"Notepad ""A5""",120

Only the fields that need quoting were quoted. newline="" matters when writing too: without it, Windows gains an extra blank line between rows.

The Excel byte-order mark

Files saved by Excel as "CSV UTF-8" start with an invisible byte-order mark. Read them as plain UTF-8 and the mark becomes part of the first column's name:

python
Path("from_excel.csv").write_bytes("city,orders\nPune,12\n".encode("utf-8"))

with open("from_excel.csv", encoding="utf-8", newline="") as f:
    print(csv.DictReader(f).fieldnames)
with open("from_excel.csv", encoding="utf-8-sig", newline="") as f:
    print(next(csv.DictReader(f))["city"])
text
['city', 'orders']
Pune

With plain UTF-8, row["city"] raises a KeyError, because the key is actually "city", which prints as though it were city. The utf-8-sig encoding skips the mark when reading and writes one when writing (Python docs: utf_8_sig). Reading with utf-8-sig is safe even for files without the mark, and writing with it makes Excel open non-English characters correctly.

Other delimiters

Spreadsheet software in many European locales exports with semicolons, because the comma is the decimal separator there. Pass the delimiter, or let Sniffer guess it from a sample:

python
Path("eu.csv").write_text("city;revenue\nParis;1234,50\n", encoding="utf-8")
with open("eu.csv", encoding="utf-8", newline="") as f:
    dialect = csv.Sniffer().sniff(f.read(1024))
    f.seek(0)
    print(dialect.delimiter, list(csv.reader(f, dialect)))
text
; [['city', 'revenue'], ['Paris', '1234,50']]

Detection is a heuristic, so when you know the format, pass delimiter=";" explicitly. Note that 1234,50 is still text: converting it means replacing the decimal comma before calling float().

Everything is a string

The csv module never converts types (Python docs: csv): "25.5", "120" and "" all arrive as strings. Convert each column deliberately, and decide what an empty field means:

python
with open("products.csv", encoding="utf-8", newline="") as f:
    prices = [float(row["price"]) for row in csv.DictReader(f) if row["price"]]
print(prices, sum(prices))
text
[25.5, 120.0] 145.5

For money, prefer decimal.Decimal over float to avoid binary rounding. Dates need parsing too; datetime in Python covers that.

Validate before you trust a file

CSV files usually come from somewhere else, and their format drifts: a column is renamed, a new one appears, a field is left empty in some rows. Check the structure before processing the data, and report problems with line numbers so someone can fix the source:

  • Compare the header with the columns you expect, using reader.fieldnames. A missing or renamed column should stop the job with a message naming it, not produce a KeyError halfway through.
  • Check required fields in each row, and collect the failures instead of stopping at the first. reader.line_num gives the current line in the file, which is the number to report, since quoted fields can span several lines.
  • Convert types in one place, catching ValueError per row and recording which value failed to parse.
  • Decide on a threshold. A few bad rows can be quarantined to a separate file; a file where half the rows fail is probably the wrong file.

For pipelines that run on a schedule, these checks grow into proper data quality tests.

Designing CSV exports other people will use

When your program writes CSV for someone else, a few conventions save them hours:

  • Always include a header row with stable, lowercase, unambiguous column names, and put units in the name, such as amount_inr or duration_s.
  • Write one record per row, and never mix summary rows or blank separator lines into the data.
  • Write dates in ISO format (2026-09-25), which sorts correctly and is not ambiguous between day-month and month-day readers.
  • Leave numbers unformatted: no thousands separators or currency symbols inside numeric fields.
  • Use UTF-8, with utf-8-sig if the audience will open the file in Excel.

Large files and when to use pandas

csv.reader and DictReader read one row at a time, so a multi-gigabyte file can be filtered or aggregated in constant memory by looping over it. When the job is analysis (types inferred, dates parsed, grouping, joining), pandas.read_csv does in one call what the loops above do by hand, with sep, encoding, parse_dates and chunksize covering the same concerns (pandas docs: read_csv). The pandas guide walks through that workflow. Use the csv module for simple transformations, streaming and scripts without dependencies; use pandas for analysis.

Why do I get blank lines between rows on Windows?

The file was opened without newline="". The csv writer ends rows with \r\n itself, and text mode on Windows translates the \n again. Open the file with newline="" for both reading and writing.

How do I write a CSV that Excel opens with Indian or other non-English text intact?

Write with encoding="utf-8-sig". The byte-order mark tells Excel the file is UTF-8.

How do I skip title rows or comment lines at the top of an export?

Call next(f) on the open file once for each line to skip before creating the reader, or filter lines with a generator such as (line for line in f if not line.startswith("#")) and pass that to csv.reader.

What about tab-separated files?

They are the same format with a different delimiter: pass delimiter="\t" to the reader or writer. Tabs rarely appear inside values, so TSV files need quoting less often.

Can a CSV field contain a newline?

Yes, if the field is quoted. The csv module reads and writes such fields correctly, which is one of the main reasons not to parse CSV by hand.

CSV files are one of the formats in Python file handling; for nested data, JSON is usually the better format, covered in working with JSON in Python. Reading and writing files, including CSV and JSON handling, form the file handling module of the Python Programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Python?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course