CSV & XML in Python | Python Course | Dataplexa

CSV & XML in Python

Not all data lives in databases. A huge proportion of real-world data exchange happens through files — spreadsheet exports, system reports, configuration files, API responses, government datasets. CSV and XML are two of the most widely used structured file formats, and Python has excellent built-in support for both.

This lesson covers reading and writing CSV files with the csv module and pandas, and parsing and building XML documents with xml.etree.ElementTree.

CSV — Comma-Separated Values

CSV is the simplest structured data format — each row is a line, each field separated by a delimiter (usually a comma). Despite its simplicity, CSV has edge cases: fields containing commas, quoted strings, different line endings, and varying encodings. Python's csv module handles all of these correctly.

1. Reading CSV Files

import csv, io

# In practice replace io.StringIO with: open("file.csv", newline="", encoding="utf-8")
raw = """name,department,salary,start_date
Alice,Engineering,95000,2021-03-15
Bob,Marketing,72000,2020-07-01
Charlie,Engineering,88000,2019-11-20
Diana,HR,65000,2022-01-10
"""

# csv.reader — rows as lists
reader = csv.reader(io.StringIO(raw))
header = next(reader)   # consume header row
print("Columns:", header)
for row in reader:
    print(row)

print()

# csv.DictReader — rows as dicts (keys = header names) — almost always preferred
reader = csv.DictReader(io.StringIO(raw))
for row in reader:
    print(f"{row['name']:10} | {row['department']:12} | ${int(row['salary']):,}")

print()

# Different delimiter — tab-separated
tsv = "name	age
Alice	30
Bob	25"
for row in csv.DictReader(io.StringIO(tsv), delimiter="	"):
    print(row)
Columns: ['name', 'department', 'salary', 'start_date'] ['Alice', 'Engineering', '95000', '2021-03-15'] ['Bob', 'Marketing', '72000', '2020-07-01'] ['Charlie', 'Engineering', '88000', '2019-11-20'] ['Diana', 'HR', '65000', '2022-01-10'] Alice | Engineering | $95,000 Bob | Marketing | $72,000 Charlie | Engineering | $88,000 Diana | HR | $65,000 {'name': 'Alice', 'age': '30'} {'name': 'Bob', 'age': '25'}
  • csv.DictReader is almost always preferred — column names as keys make code far more readable.
  • All values from CSV are strings — convert explicitly: int(row["salary"]), float(row["price"]).
  • For real files: open("file.csv", newline="", encoding="utf-8") — always specify newline="" (prevents double line endings on Windows) and encoding.
  • Change delimiter for TSV or pipe-separated: csv.DictReader(f, delimiter=" ").

2. Writing CSV Files

import csv, io

# csv.writer — write rows as lists
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["product", "quantity", "price"])   # header
writer.writerows([
    ["Notebook",        50,  4.99],
    ["Pen, fine tip",  200,  1.50],   # comma inside value — auto-quoted
    ["Desk",            10, 89.99],
])
print("csv.writer output:")
print(output.getvalue())

# csv.DictWriter — write rows as dicts
output2 = io.StringIO()
fields  = ["name", "score", "grade"]
writer2 = csv.DictWriter(output2, fieldnames=fields)
writer2.writeheader()   # writes field names as first row
writer2.writerows([
    {"name": "Alice",   "score": 92, "grade": "A"},
    {"name": "Bob",     "score": 78, "grade": "B"},
    {"name": "Charlie", "score": 85, "grade": "B+"},
])
print("csv.DictWriter output:")
print(output2.getvalue())

# Writing to a real file:
# with open("output.csv", "w", newline="", encoding="utf-8") as f:
#     writer = csv.DictWriter(f, fieldnames=fields)
#     writer.writeheader()
#     writer.writerows(data)
csv.writer output: product,quantity,price Notebook,50,4.99 "Pen, fine tip",200,1.5 Desk,10,89.99 csv.DictWriter output: name,score,grade Alice,92,A Bob,78,B Charlie,85,B+
  • csv.writer automatically quotes fields containing the delimiter — you never handle this yourself.
  • DictWriter.writeheader() writes field names as the first row automatically.
  • writer.writerows(list) writes multiple rows in one call.
  • Always use newline="" when opening a file for writing CSV.

3. CSV with pandas

For analysis or large files, pandas is far more powerful — it loads the file into a DataFrame with automatic type inference, filtering, aggregation, and export built in.

import pandas as pd, io

raw = """name,department,salary,start_date
Alice,Engineering,95000,2021-03-15
Bob,Marketing,72000,2020-07-01
Charlie,Engineering,88000,2019-11-20
Diana,HR,65000,2022-01-10
Eve,Engineering,102000,2018-05-30
"""

df = pd.read_csv(io.StringIO(raw), parse_dates=["start_date"])

print(df.dtypes, "
")

# Filter and display
eng = df[df["department"] == "Engineering"]
print(f"Engineering ({len(eng)} people):")
print(eng[["name", "salary"]].to_string(index=False))

# Aggregation
print(f"
Overall avg: ${df['salary'].mean():,.0f}")
print(df.groupby("department")["salary"].mean().sort_values(ascending=False))

# Export — index=False omits the row-number column
df.to_csv("employees_export.csv", index=False)
print("
Exported to employees_export.csv")
name object department object salary int64 start_date datetime64[ns] dtype: object Engineering (3 people): name salary Alice 95000 Charlie 88000 Eve 102000 Overall avg: $84,400 department Engineering 95000.0 Marketing 72000.0 HR 65000.0 Name: salary, dtype: float64 Exported to employees_export.csv
  • pd.read_csv() infers types — integers stay int64, dates parse with parse_dates=.
  • df.groupby("col")["col2"].mean() — group-level aggregation in one line.
  • df.to_csv("file.csv", index=False) — export without the row-number index column.
  • Use pandas whenever you need filtering, grouping, merging, or any analysis beyond simple I/O.

XML — eXtensible Markup Language

XML uses nested tags to represent hierarchical data. It is used in configuration files, document formats (Word, SVG), SOAP web services, RSS feeds, and enterprise data exchange. Python's xml.etree.ElementTree is built in and covers most use cases.

4. Parsing XML

import xml.etree.ElementTree as ET

xml_data = """
<library>
    <book id="1" genre="fiction">
        <title>The Great Gatsby</title>
        <author>F. Scott Fitzgerald</author>
        <year>1925</year>
        <price>12.99</price>
    </book>
    <book id="2" genre="non-fiction">
        <title>Sapiens</title>
        <author>Yuval Noah Harari</author>
        <year>2011</year>
        <price>16.99</price>
    </book>
    <book id="3" genre="fiction">
        <title>1984</title>
        <author>George Orwell</author>
        <year>1949</year>
        <price>9.99</price>
    </book>
</library>"""

root = ET.fromstring(xml_data)   # parse from string
# ET.parse("file.xml").getroot() — parse from file

print(f"Root: <{root.tag}> | {len(root)} children
")

for book in root.findall("book"):
    book_id = book.get("id")
    genre   = book.get("genre", "unknown")   # default if attr missing
    title   = book.find("title").text
    author  = book.find("author").text
    price   = float(book.find("price").text)
    print(f"[{book_id}] {title} — {author} | ${price:.2f} ({genre})")

# XPath attribute filter
print("
Fiction only:")
for book in root.findall("book[@genre='fiction']"):
    print(" ", book.find("title").text)

# .//
tag — search all descendants at any depth all_prices = [float(p.text) for p in root.findall(".//price")] print(f" Total library value: ${sum(all_prices):.2f}")
Root: <library> | 3 children [1] The Great Gatsby — F. Scott Fitzgerald | $12.99 (fiction) [2] Sapiens — Yuval Noah Harari | $16.99 (non-fiction) [3] 1984 — George Orwell | $9.99 (fiction) Fiction only: The Great Gatsby 1984 Total library value: $39.97
  • root.findall("tag") — direct children; root.findall(".//tag") — all descendants at any depth.
  • element.find("tag") — first match or None; always check for None before calling .text.
  • element.get("attr") — attribute value or None; element.get("attr", default) — with fallback.
  • element.text — the text content between opening and closing tags.
  • XPath [@attr='value'] filters elements by attribute value.

5. Building and Modifying XML

import xml.etree.ElementTree as ET

root = ET.Element("inventory")
root.set("version", "1.0")   # attribute on root element

items = [
    {"sku": "NB001", "name": "Notebook", "qty": 150, "price": 4.99},
    {"sku": "PN002", "name": "Pen",      "qty": 500, "price": 1.50},
    {"sku": "DS003", "name": "Desk",     "qty": 20,  "price": 89.99},
]

for item in items:
    el = ET.SubElement(root, "item", sku=item["sku"])
    ET.SubElement(el, "name").text  = item["name"]
    ET.SubElement(el, "qty").text   = str(item["qty"])
    ET.SubElement(el, "price").text = f"{item['price']:.2f}"

ET.indent(root, space="    ")   # pretty-print (Python 3.9+)
print(ET.tostring(root, encoding="unicode"))

# Write to file
ET.ElementTree(root).write("inventory.xml", encoding="unicode", xml_declaration=True)

# Modify in-place — update Notebook price
for item in root.findall("item"):
    if item.find("name").text == "Notebook":
        item.find("price").text = "5.49"
        print("Updated:", ET.tostring(item, encoding="unicode"))
<inventory version="1.0"> <item sku="NB001"> <name>Notebook</name> <qty>150</qty> <price>4.99</price> </item> <item sku="PN002"> <name>Pen</name> <qty>500</qty> <price>1.50</price> </item> <item sku="DS003"> <name>Desk</name> <qty>20</qty> <price>89.99</price> </item> </inventory> Updated: <item sku="NB001"><name>Notebook</name><qty>150</qty><price>5.49</price></item>
  • ET.Element("tag") — root element; root.set("attr", "val") — sets an attribute.
  • ET.SubElement(parent, "tag", attr=val) — creates a child element with optional attributes.
  • ET.indent(root) — adds whitespace for human-readable output (Python 3.9+).
  • Modify in-place by finding the element and reassigning its .text or attributes.
  • For complex XML with namespaces or XSLT, use the third-party lxml library.

Quick Reference Table

ToolFormatBest Used For
csv.DictReaderCSVReading CSV rows as named dicts
csv.DictWriterCSVWriting dicts as CSV rows
pandas.read_csvCSVAnalysis, filtering, aggregation
ET.fromstring()XMLParse XML from a string
ET.parse()XMLParse XML from a file
ET.SubElement()XMLBuild XML trees programmatically
.//tag XPathXMLFind all descendants at any depth

Practice

What is the difference between csv.reader and csv.DictReader?



Why must you specify newline="" when opening a CSV file?



In ElementTree, what property retrieves the text between an element's opening and closing tags?



What does df.to_csv("file.csv", index=False) do differently from the default?



Which ElementTree function adds a child element to an existing parent?



What XPath expression in findall() searches all descendants at any depth?



Quick Quiz

What data type does csv.DictReader return for each row?





What does pd.read_csv(..., parse_dates=["start_date"]) do?





In ElementTree, what is the difference between find() and findall()?





How does csv.writer handle a field value that contains a comma?





Which Python version introduced ET.indent() for pretty-printing XML?





How do you safely read an XML attribute that might not exist, with a fallback value?





NEXT UP
NumPy — Fast Numerical Computing
Arrays, vectorised operations, broadcasting, linear algebra, and why NumPy is the foundation of the entire Python data science stack.