Blog / Coding tips

How to Read a CSV File in Python Without Pandas

Cover image for How to Read a CSV File in Python Without Pandas

To read a CSV file in Python without pandas, use the built-in csv module: open the file with open("file.csv", newline="", encoding="utf-8-sig"), then loop over csv.reader(f) to get each row as a list, or csv.DictReader(f) to get each row as a dictionary keyed by the header. It ships with Python, so there's nothing to pip install, and it correctly handles commas inside quotes, which a plain split(",") does not. Below are working examples with their real output, plus how to convert numbers, skip the header, handle Excel files and write a CSV back out.

Pandas is brilliant for data analysis, but it's a big install for a script that just reads a spreadsheet export. On a school computer or a cheap web host you might not even be allowed to install it. Everything below was run with Python 3.13 and only the standard library.

The example CSV file

I made a small file the way Excel saves one with "CSV UTF-8": Windows line endings, a hidden byte order mark (BOM) at the start, quoted fields containing commas, an accented name and one missing score. Real files tend to have at least one of these problems:

name,city,course,score
Aisha Khan,London,Web Development,78
Tom Riley,"Leeds, West Yorkshire",Python,64
Zoë Patel,Manchester,"Databases, SQL",91
Rahim Uddin,Dhaka,Web Development,

Why not just split on commas?

It's tempting to read the file line by line and split each line on commas. Here's what happens:

# The tempting way: split each line on commas
with open("students.csv", encoding="utf-8") as f:
    for line in f:
        print(line.strip().split(","))

# Output:
# ['\ufeffname', 'city', 'course', 'score']
# ['Aisha Khan', 'London', 'Web Development', '78']
# ['Tom Riley', '"Leeds', ' West Yorkshire"', 'Python', '64']
# ['Zoë Patel', 'Manchester', '"Databases', ' SQL"', '91']
# ['Rahim Uddin', 'Dhaka', 'Web Development', '']

Two things went wrong. "Leeds, West Yorkshire" was cut in half, so Tom's row now has five columns instead of four, with stray quote marks. And the first column name is '\ufeffname' because of the BOM. Your code would fail the moment it looked for a column called name. The csv module fixes both.

Read a CSV file into lists with csv.reader

csv.reader gives you one list of strings per row. Calling next() on it once reads the header row, so the loop only sees the data:

import csv

with open("students.csv", newline="", encoding="utf-8-sig") as f:
    reader = csv.reader(f)
    header = next(reader)          # first row: column names
    print("header:", header)
    for row in reader:
        print(row)

# Output:
# header: ['name', 'city', 'course', 'score']
# ['Aisha Khan', 'London', 'Web Development', '78']
# ['Tom Riley', 'Leeds, West Yorkshire', 'Python', '64']
# ['Zoë Patel', 'Manchester', 'Databases, SQL', '91']
# ['Rahim Uddin', 'Dhaka', 'Web Development', '']

The quoted commas are handled correctly and the header is clean. Two arguments to open() do a lot of work here:

  • newline="" is what the official csv documentation asks for. It lets the csv module deal with line endings itself, which matters for quoted fields that contain line breaks.
  • encoding="utf-8-sig" reads UTF-8 and quietly removes the BOM if there is one. If there isn't, it behaves exactly like "utf-8", so it's a safe default.

Proof that utf-8-sig removes the BOM

import csv

with open("students.csv", newline="", encoding="utf-8") as f:
    print(next(csv.reader(f))[0].encode())   # plain utf-8 keeps the BOM

with open("students.csv", newline="", encoding="utf-8-sig") as f:
    print(next(csv.reader(f))[0].encode())   # utf-8-sig strips it

# Output:
# b'\xef\xbb\xbfname'
# b'name'

Those three bytes, \xef\xbb\xbf, are the BOM. With plain utf-8 they become part of the first column name; with utf-8-sig they're gone.

Read a CSV file into dictionaries with csv.DictReader

Using column positions like row[2] works, but it breaks if someone adds a column to the spreadsheet. csv.DictReader uses the header row as keys, so you can write row["course"] instead:

import csv

with open("students.csv", newline="", encoding="utf-8-sig") as f:
    rows = list(csv.DictReader(f))

print(rows[1])
print(f"{rows[2]['name']} studies {rows[2]['course']} in {rows[2]['city']}")

# Output:
# {'name': 'Tom Riley', 'city': 'Leeds, West Yorkshire', 'course': 'Python', 'score': '64'}
# Zoë Patel studies Databases, SQL in Manchester

Wrapping the reader in list() loads every row into memory, which is fine for files of a few thousand lines. For very large files, loop over csv.DictReader(f) directly so you only hold one row at a time. If you're new to the f"..." syntax in the print line, I explain it in Python f-strings: formatting examples.

Convert strings to numbers (and handle blanks)

Every value the csv module gives you is a string, even "78". Before you can add or compare scores, convert them. Empty cells are the usual trap: int("") raises a ValueError, so I convert blanks to None instead:

import csv

def to_int(value):
    """Turn '78' into 78 and '' into None."""
    value = value.strip()
    return int(value) if value else None

with open("students.csv", newline="", encoding="utf-8-sig") as f:
    students = [
        {**row, "score": to_int(row["score"])}
        for row in csv.DictReader(f)
    ]

marked = [s for s in students if s["score"] is not None]
average = sum(s["score"] for s in marked) / len(marked)
best = max(marked, key=lambda s: s["score"])

print(f"{len(students)} students, {len(marked)} with a score")
print(f"Average score: {average:.1f}")
print(f"Top student: {best['name']} ({best['score']})")
print("Missing a score:", [s["name"] for s in students if s["score"] is None])

# Output:
# 4 students, 3 with a score
# Average score: 77.7
# Top student: Zoë Patel (91)
# Missing a score: ['Rahim Uddin']

The list comprehension builds a new dictionary for each row with {**row, "score": ...}, copying the other columns and replacing the score with a number. If that syntax is new to you, my post on Python list comprehension examples covers it step by step. The :.1f in the average rounds it to one decimal place.

Read a CSV file without a header

Some exports have no header row at all. With csv.reader you simply don't call next(). With DictReader, give it the column names yourself:

reader = csv.DictReader(f, fieldnames=["name", "city", "course", "score"])

When you pass fieldnames, the first line is treated as data rather than as the header. If your file does have a header and you pass fieldnames anyway, the header will show up as your first "row", so check which case you're in.

Semicolon CSV files from European Excel

If Excel's region is set to a country that uses a comma as the decimal mark, it saves CSV files with semicolons between fields. Tell the reader with delimiter=";", and swap the decimal comma for a dot before calling float():

import csv

# Some European Excel exports use ; between fields and , as the decimal mark
with open("prices.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f, delimiter=";"):
        price = float(row["price"].replace(",", "."))
        print(row["name"], price)

# Output:
# Tea 1.2
# Coffee 2.5

Tab-separated files work the same way with delimiter="\t".

Write a CSV file with csv.DictWriter

Writing is the mirror image of reading. Open the file with "w" and newline="", write the header once, then the rows:

import csv

passed = [
    {"name": "Aisha Khan", "score": 78},
    {"name": "Zoë Patel", "score": 91},
]

with open("passed.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "score"])
    writer.writeheader()
    writer.writerows(passed)

with open("passed.csv", encoding="utf-8") as f:
    print(f.read())

# Output:
# name,score
# Aisha Khan,78
# Zoë Patel,91

The csv writer adds quotes automatically when a value contains a comma, a quote or a line break, so you never have to escape anything yourself. If the file is going to be opened in Excel and contains non-English characters like "ë", save it with encoding="utf-8-sig" so Excel recognises it as UTF-8.

Common errors and fixes

  • KeyError: 'name': almost always the BOM. Use encoding="utf-8-sig", or print reader.fieldnames to see the real column names.
  • UnicodeDecodeError: the file isn't UTF-8. Older Excel versions on Windows often save as cp1252, so try encoding="cp1252".
  • Blank lines between rows on Windows: you opened the file for writing without newline="".
  • ValueError: invalid literal for int(): an empty cell or a value like "78 " or "£1,200". Strip and clean the text before converting it.

When should you use pandas after all?

The csv module is perfect for reading, filtering and converting files, and for scripts that need to run anywhere. Pandas starts to pay off when you're grouping, joining several files, doing statistics on hundreds of thousands of rows, or plotting. For a portfolio project or a quick report from a spreadsheet, the standard library is usually all you need.

FAQ

Can Python read a CSV file without importing anything?

You can open it with open() and split lines yourself, but that breaks on quoted commas, as shown above. The csv module is part of the standard library, so import csv costs nothing and needs no installation.

How do I skip the header row in a CSV file in Python?

Call next(reader) once before your loop. It reads the first row, which you can keep as the header or ignore. csv.DictReader does this for you and uses it as the dictionary keys.

How do I read a CSV file into a list of dictionaries in Python?

Use rows = list(csv.DictReader(f)). Each item is a dictionary such as {"name": "Aisha Khan", "score": "78"}, with every value as a string.

Why does my first column name start with \ufeff?

The file was saved with a UTF-8 byte order mark, which Excel adds to "CSV UTF-8" files. Open it with encoding="utf-8-sig" and the mark is removed.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes

How to Work Out Your UK Degree Classification
Coding tips

How to Work Out Your UK Degree Classification

Work out a First, 2:1 or 2:2 from module marks: credit-weighted averages, 30:70 or 1:2 year weightings, rounding at 69.5 and the mark you need.

October 4, 2026 · 10 min read