Skip to the converter
Pandas to CSV

Blog

How to Read a CSV File with Pandas

Published 8 October 2026

A CSV file is plain text: one row per line, one value per field, a delimiter between them — usually a comma. It carries no types, no index and no schema, which is exactly why it travels between languages and tools so well. Pandas is the Python library that turns that text into a DataFrame — a labelled table you can filter, group, join and plot — and pd.read_csv() is the function that does the turning.

import pandas as pd

df = pd.read_csv("data.csv")
The whole operation: import the library, read the file, keep the DataFrame.

The call opens the file, takes the first row as column names, infers a type for every column and hands back a DataFrame. Everything below is about the arguments that change those four decisions — and the errors that tell you when one of them came out wrong.

The quick answer

If you only need the working call, it is this — two statements, written once per script:

import pandas as pd

df = pd.read_csv("data.csv")
  • pd — the alias pandas is imported under, by universal convention. The name comes from the module (import pandas), not from the library's own API.
  • read_csv() — reads a delimited text file and returns a DataFrame. It is a module-level function, so it always carries the pd. prefix; there is no df.read_csv().
  • "data.csv" — the source: a relative or absolute path, a URL, or any object with a read() method. A relative path resolves against the process's current working directory, not against the script's own folder.
  • df — the resulting DataFrame: one column per header field, one row per line of the file, and a RangeIndex (0, 1, 2, …) down the side.

Nothing is written back and nothing leaves your machine: pandas opens the file, parses it into memory and closes it. The file on disk is untouched.

Install and import Pandas

Pandas installs from PyPI like any other package:

pip install pandas
Conda and mamba use the same package name.

Then import it before the first read — once per session is enough:

import pandas as pd

Google Colab, Anaconda and most Kaggle notebooks already ship with pandas, so the install step is only needed in a bare Python environment. If you are unsure what is installed, print(pd.__version__) tells you — worth checking when you are about to rely on a parameter that behaves differently between major versions. Importing the bare module (import pandas) works too; pd is purely a convention, and every example on this page assumes it.

Read a CSV file into a DataFrame

Take a small file, students.csv:

name,score,city
Alice,91,Delhi
Bob,87,Mumbai
Carol,95,Chandigarh
students.csv — a header row, three data rows, comma-delimited.

Reading it is one call:

df = pd.read_csv("students.csv")

print(df)
Columns come from the header row; the numbers down the left are the new index.
    name  score       city
0  Alice     91      Delhi
1    Bob     87     Mumbai
2  Carol     95  Chandigarh

Four things happened, and each one is a decision you can override later on this page. The first line became the column names (name, score, city) and was consumed as a header, not as data. A RangeIndex was created, because the file has no index of its own. Each column got a type by inference: score became int64 because every value is a whole number, and the text columns became strings — on pandas 3.0 that is the dedicated str dtype, while pandas 2.x reports object. And the file was read as a whole into memory, which is fine here and becomes a real constraint on large files further down.

Inspect the DataFrame after reading

The five checks worth running before you trust any file you have not seen before:

df.head()      # first 5 rows — did the header land correctly?
df.shape       # (rows, columns) — the row count you expect?
df.columns     # the column labels, as they really are
df.info()      # dtypes, non-null counts and memory in one printout
df.dtypes      # one dtype per column, without the rest of the noise

Each check catches a specific class of mistake. head() reveals a header that was swallowed as data, or a preamble line that was taken for the header. shape answers "is anything missing?" — one row short usually means a stray line break inside a quoted field. columns exposes delimiter and encoding damage: a column literally named name;score;city means the separator was wrong, and Unnamed: 0 means the file carries an index column. info() shows which columns have nulls and whether a numeric column came through as text. Getting into the habit of these five lines costs seconds and removes most silent data errors before they reach an analysis.

Read a CSV from a different file path

The first argument accepts anything Python considers a path:

df = pd.read_csv("data/sales.csv")               # relative to the working directory
df = pd.read_csv("/home/me/data/sales.csv")      # absolute (Linux, macOS)
df = pd.read_csv("C:/Users/me/data/sales.csv")   # absolute (Windows)

A relative path resolves against the current working directory of the Python process — the folder you launched the script or notebook from, not the folder the script lives in. When a file "is not there" and you can see it in the file explorer, print os.getcwd() to see what the process thinks the working directory is, or switch to an absolute path and the ambiguity disappears.

On Windows, forward slashes work everywhere in Python, so C:/Users/me/data.csv is the simplest form. If you prefer backslashes, use a raw string (r"C:\Users\me\data.csv"): in a normal string \n and \t are escapes, so a folder like C:\new silently becomes a newline plus ew. pathlib.Path objects are accepted as-is, which is the tidiest option when you are building paths programmatically. And because filepath_or_buffer also accepts URLs, the same call reads a remote file — pd.read_csv(url) — with no download step.

Read a CSV with no header row (header=None)

Not every CSV has a header row. If the first line is data, tell pandas with header=None:

df = pd.read_csv("data.csv", header=None)

print(df.columns)   # Index([0, 1, 2], dtype='int64')
No header row: columns fall back to positional numbers.

Label the columns in the same call with names:

df = pd.read_csv(
    "data.csv",
    header=None,
    names=["name", "score", "city"],
)
header=None stops the first line being treated as labels; names= supplies the real ones.

One trap worth knowing: passing names on its own behaves exactly like header=None, so on a file that does have a header row, that row is kept as the first line of data. To replace existing column names, pass both — header=0 together with names=[...] — because header=0 says "the first line is the header, discard it and use these names instead". If you forget either half, the symptom is easy to recognise: one extra row at the top whose values are all strings.

Read a CSV with a different delimiter (sep)

"CSV" describes a family of files, and the comma is only the default. Semicolons, tabs, pipes and carets all appear in exports from real systems — European spreadsheets in particular use ; so the comma stays free for the decimal separator. The parameter is sep (delimiter is an alias):

df = pd.read_csv("data.csv", sep=";")   # semicolon-delimited
df = pd.read_csv("data.tsv", sep="\t")   # tab-delimited (TSV)
df = pd.read_csv("data.psv", sep="|")    # pipe-delimited

Get it wrong and you usually get one of two outcomes: the whole line collapses into a single column (the comma you expected is not there), or the parser raises a ParserError because rows disagree about how many fields they contain. Both are diagnosed in the same way — look at df.columns and at the first row.

Two related options. sep=r"\s+" splits on runs of whitespace, which is what you want for space-aligned files such as the output of print(df). And sep=None asks pandas to detect the delimiter with Python's csv.Sniffer — that is supported only by the Python parsing engine, which is then selected for you. Sniffing is convenient for a one-off file you know nothing about; for a path that runs every day, an explicit sep is better, because a sniffer can confidently guess wrong.

Select specific columns while reading (usecols)

When a file is wider than you need, usecols keeps only the columns you name — the rest are never turned into a DataFrame at all:

df = pd.read_csv(
    "data.csv",
    usecols=["name", "score"],
)
Only these two columns are materialised; the file itself is read the whole way through.

The pandas documentation is direct about the benefit: using usecols results in much faster parsing and lower memory usage, because type inference and allocation only happen for the columns you keep. That makes it the right first move on a wide export — think dozens of telemetry fields where the analysis uses three. Two details to remember: the names must match the header exactly (a typo raises ValueError), and the order you list them is ignored — the result keeps the file's order. Reorder after the fact with df[["score", "name"]] if it matters. A callable works too, for columns you can describe rather than enumerate — pass lambda c: c.startswith("sale") as the usecols value.

Handle encoding

CSV is text, and text needs an encoding. Pandas defaults to UTF-8, which is correct most of the time — and wrong in two very common cases: files exported by Windows tools (often Windows-1252 or Latin-1) and files that begin with a UTF-8 byte-order mark.

df = pd.read_csv("data.csv", encoding="utf-8")       # the default
df = pd.read_csv("data.csv", encoding="utf-8-sig")   # UTF-8 that starts with a BOM
df = pd.read_csv("data.csv", encoding="latin-1")     # never raises, but never guesses

When the declared encoding does not match the bytes, the read raises UnicodeDecodeError, usually naming the byte offset — that error is a statement about the file, not about your code. utf-8-sig is the fix for the BOM case: it reads UTF-8 with or without a leading byte-order mark and strips the mark, which otherwise hides inside the first column name and shows up as a stray character in df.columns. It is also the encoding to pass to df.to_csv() (encoding="utf-8-sig") when a file has to survive a round trip through Excel on Windows.

latin-1 is the reliable last resort: it maps every possible byte to a character, so the decode cannot fail — but it cannot know what the file really is either, so accented characters can come out wrong. Check the first few rows before trusting it. When you need to read past damaged bytes rather than fail, encoding_errors="replace" substitutes a placeholder character instead of raising. No single encoding fixes every CSV problem: the reliable order is to establish what the file actually is, then say so.

Handle missing values (na_values)

An empty field is the obvious missing value, but CSVs are written by many different programs and "missing" is spelled many ways. Pandas recognises the empty string, NaN, N/A, NULL, None and several other common spellings, turning them all into NaN while parsing. Anything else is just text.

df = pd.read_csv("data.csv", na_values=["-", "unknown", "?"])

df = pd.read_csv(
    "data.csv",
    na_values={"score": ["-", "n/a"], "city": ["-"]},
)
Top: add markers for the whole file. Bottom: different markers per column.

na_values adds to the defaults — it does not replace them. To replace them, turn the defaults off with keep_default_na=False, which is the right move when a token like N/A is real data rather than a hole: a survey column where "N/A" is a legitimate answer should not become NaN the moment it is read.

df = pd.read_csv(
    "survey.csv",
    keep_default_na=False,   # nothing is treated as missing...
    na_values=[""],          # ...except a truly empty field
)

For a file you know is complete, na_filter=False switches missing-value detection off entirely and can speed up reading a large file. The three parameters interact in a documented order, so check it once if you are chaining them: na_filter=False ignores both of the others.

Control data types (dtype)

Type inference is a guess, and an identifier is where the guess usually goes wrong. Read a column of codes like 00123 and inference gives you the integer 123 — leading zeros gone, and the value no longer matches anything downstream. Pin the type when you know it:

df = pd.read_csv(
    "data.csv",
    dtype={"id": "string"},
)
The whole column is treated as text; no numeric interpretation happens.

dtype takes a single type for the entire file or a dictionary keyed by column name. Beyond identifiers, three cases earn their place in a call: a column that must stay a decimal (dtype={"score": "float64"}), a count column that contains blanks — dtype={"count": "Int64"}, the nullable integer type, keeps the missing values as NA instead of promoting every value to float — and any file where pandas warns about mixed types. That warning appears on large files read with the default low_memory=True, where the C parser infers types in batches: an identifier column can come back as numbers in one batch and text in another. Specifying dtype removes the ambiguity at the source.

Two related notes. Since pandas 3.0, text columns infer to the dedicated string dtype (str) instead of object — the values behave the same in most code, but df.dtypes looks different and pandas-specific string methods attach differently. And if you need a per-value conversion rather than a per-column cast, use converters: when both are given, converters wins.

Parse dates (parse_dates)

Dates are read as text by default — a column that says 2026-10-08 arrives as a string, not a datetime, and sorting it sorts lexically. Ask for the conversion at read time:

df = pd.read_csv("orders.csv", parse_dates=["ordered_at"])
ordered_at becomes datetime64[ns]; every other column keeps its inferred type.

When the format is not one pandas can recognise on sight, state it. The date_format argument (added in pandas 2.0) takes a strftime pattern, a dictionary of per-column patterns, or the special values "ISO8601" and "mixed":

df = pd.read_csv(
    "orders.csv",
    parse_dates=["ordered_at"],
    date_format="%d/%m/%Y",
)
Day first — the pattern follows the file, not your locale.

If a value cannot be parsed, the column is not partially converted: pandas leaves it alone and returns it as text, which is why a silent failure here is worth checking with df.dtypes after the read. For awkward cases — mixed formats in one column, or a column with mixed timezones — read the column as text and convert afterwards with pd.to_datetime(), which gives you format=, dayfirst= and utc=True for the mixed-timezone case. One version note: older code often passes infer_datetime_format=True; that argument was removed in pandas 3.0, because the fast path it asked for is now the default behaviour.

Skip rows or read part of a file (skiprows, nrows)

Three parameters cover the "not all of this file, and not from the top" cases:

df = pd.read_csv("report.csv", skiprows=3)      # drop the first 3 lines
df = pd.read_csv("report.csv", skiprows=[0, 2]) # drop line 0 and line 2
df = pd.read_csv("report.csv", nrows=1000)      # first 1000 data rows
  • skiprows as an integer counts lines from the top of the file. It is the standard way to get past a preamble — a vendor banner, export metadata, a blank line — that sits above the real header.
  • skiprows as a list names specific line numbers (0-indexed) to drop. As a callable it receives each row index and skips it when it returns True.
  • nrows caps the number of data rows returned — the header row does not count. It is the cheapest way to look at a file before committing to loading it.

Together they also read a window from the middle of a large file — the pattern from the pandas documentation reads rows 1,000,000 onward with skiprows=1_000_000, nrows=999_999. One constraint belongs to the engine parameter: trimming lines off the bottom is skipfooter, and that one requires the Python parser.

Read large CSV files (chunksize)

A normal read loads the entire file into one DataFrame. When the file does not fit — or you only need to process it row-block by row-block — chunksize turns the read into an iterator of DataFrames:

for chunk in pd.read_csv("large.csv", chunksize=10_000):
    process(chunk)
Each iteration receives a DataFrame of at most 10,000 rows.

The function returns a TextFileReader rather than a DataFrame, so you iterate it directly, accumulating whatever the job needs — a running total, a filtered subset, batches written to a database. Memory stays bounded by the chunk size instead of by the file, which is the entire point: a 4 GB CSV can be summarised on a laptop that could never hold it as one frame.

What chunking does not do is make parsing faster — you are adding Python-level iteration over an already-fast parser, and since pandas 3.x the C engine can read sufficiently large local files in parallel on its own. Reach for chunksize to bound memory, not to gain speed. There is one real cost to manage: type inference now happens per chunk, so a column can be an integer in one block and text in the next (the DtypeWarning that mentions mixed types). Streaming jobs should pin dtype so every chunk agrees. For a quick look at the top of a big file, nrows= is still the simpler tool.

For complete parameter syntax, lazy evaluation details, multi-stage aggregation patterns, and disk streaming recipes, see our comprehensive guide on reading large CSV files with pandas chunksize.

How to read a CSV file in Google Colab with Pandas

Colab ships with pandas installed, so there is no setup step — only the question of where your file lives. The working directory is /content, and all three approaches below end in the same pd.read_csv() call.

Option 1: upload a file from your computer

from google.colab import files

uploaded = files.upload()   # a file picker opens — choose data.csv

import pandas as pd
df = pd.read_csv("data.csv")
df.head()
files.upload() writes into the current directory, so read_csv() needs the same filename.

The uploaded file lands in /content and the name must match exactly — Colab's filesystem is case-sensitive. If the read raises FileNotFoundError, import os; os.listdir(".") shows what is actually there. Uploaded files are part of the runtime: they disappear when the runtime is recycled or reset, so a notebook that matters should read from Drive instead.

Option 2: read from Google Drive

from google.colab import drive

drive.mount("/content/drive")   # authorise once per session

df = pd.read_csv("/content/drive/MyDrive/data.csv")
MyDrive is the root of your Drive; add subfolders to the path as needed.

Mounting gives the notebook a persistent path to your Drive, and the files survive runtime resets — this is the right option for a dataset you come back to. The first run opens an authorisation link; after that the path behaves like any other absolute path, so anything covered earlier on this page (encoding, sep, usecols) works unchanged.

Option 3: read straight from a URL

df = pd.read_csv(
    "https://raw.githubusercontent.com/owner/repo/main/data.csv"
)
Because read_csv() accepts URLs, a public file needs no upload at all.

filepath_or_buffer accepts http and https URLs, so a public CSV is one call away — use the raw file URL rather than the GitHub page that hosts it. The same works for any URL pandas can open, including cloud storage with storage_options.

Common read_csv() errors and how to fix them

Every one of these points at a specific decision that came out wrong. The cause, the check and the fix:

FileNotFoundError

Cause. The path is wrong, or it is right but relative to a different directory than the one you are looking at. Check. Print os.getcwd() and os.listdir("."); compare the filename exactly, including the extension and letter case. In Colab, confirm the file was actually uploaded to /content. Fix. Correct the path, or use an absolute one. pandas reads files — it does not create directories or download anything for you.

UnicodeDecodeError

Cause. The bytes are not valid in the declared encoding, which is UTF-8 by default; a Windows export in Windows-1252 or Latin-1 is the usual culprit. Check. The exception names a byte offset — open the file around that position in an editor that shows encoding, or try reading a small sample with a different encoding. Fix. Pass the encoding the file was written in (encoding="latin-1" reads any byte sequence; encoding="utf-8-sig" handles a byte-order mark), or encoding_errors="replace" to read on with substitutions when accuracy of those characters does not matter.

ParserError — "Error tokenizing data"

Cause. A row has a different number of fields than the header: an unquoted comma inside a value, a stray line of text, or the wrong sep entirely. Check. The message names the line number — go and read that line in the raw file. Fix. Correct the separator, quote the offending values, or, when the file genuinely contains junk lines you do not care about, on_bad_lines="warn" reports them without aborting and on_bad_lines="skip" drops them quietly. Do not reach for that first: skipping bad lines hides real data loss.

Everything landed in one column

Cause. No exception at all — the separator in the file is not the comma pandas assumed. Check. df.columns shows one long column, and df.head() shows whole lines with ; or \t still inside them. Fix. Set sep to the delimiter the file actually uses; sep=r"\s+" for space-aligned text, sep=None if you want pandas to sniff it.

An Unnamed: 0 column, or a missing first row

Cause. Two different mistakes with two different symptoms. An Unnamed: 0 column means the file carries a saved row index — pandas wrote it because to_csv() includes the index by default. A missing first row means names= was passed without header=0, so the real header became data. Check. df.head() for a row of strings where numbers belong; df.columns for the generated name. Fix. For the index column, drop it on the way in with df.drop(columns=["Unnamed: 0"]) — and the guide on saving a DataFrame to CSV without the index explains how to stop it being written next time. For the header, pass header=0 alongside names=.

Wrong data types

Cause. Inference followed the values, not your intent: an ID with leading zeros became an integer, a column with a stray "n/a" stayed text, or low_memory gave two parts of the same file different types. Check. df.dtypes and df.info() after every read of an unfamiliar file; look for the DtypeWarning on large files. Fix. Pass dtype= for the columns that matter, use the nullable Int64 where blanks are expected, and remember that thousands="," is what turns "1,234" into a number rather than text.

EmptyDataError — no columns to parse from file

Cause. The file is empty, zero bytes, or contains only blank lines — the upload saved nothing, or the export failed. Check. File size, and whether the first line is empty. Fix. Fix the source; there is nothing in the file to read. Guarding with os.path.getsize(path) > 0 before the call turns a hard crash into a handled condition.

When should you use engine="python" in read_csv()?

The engine argument chooses the parser behind the call: "c" (the default), "python", or "pyarrow". The C engine is a compiled parser and the fastest of the two built-in options; the Python engine is slower but more feature-complete. For an ordinary comma- or tab-separated file, the right setting is no setting at all — leave engine out and let the default do its job.

Pandas also selects the Python engine for you when an argument requires it, so these two calls are equivalent:

df = pd.read_csv("data.csv", engine="python")   # explicit
df = pd.read_csv("data.csv")                    # the same file, default engine
Naming the engine changes nothing on a file the C parser already handles.

There are three situations where the Python engine is genuinely the answer:

  • Trailing lines to discard. skipfooter is unsupported by the C engine, so trimming an export footer forces the switch.
  • Separator sniffing and regex separators. sep=None runs Python's csv.Sniffer, and a multi-character separator other than r"\s+" is treated as a regular expression — both are Python-engine features.
  • A custom bad-line handler. on_bad_lines accepts a callable — a function that receives the offending fields and returns what the row should be — which only the Python engine can run.

A realistic case — an export with a footer that would otherwise become three junk rows:

df = pd.read_csv(
    "report.csv",
    skipfooter=3,
    engine="python",
)
skipfooter needs the Python engine; specifying both keeps the intent visible.

And a separator that is a pattern rather than a character:

df = pd.read_csv(
    "log.txt",
    sep=r"\s*;\s*",      # semicolon with optional surrounding spaces
    engine="python",
)
A regex separator forces the Python engine even if you omit engine=.

The trade-off is speed. The Python engine parses more slowly and gives up C-engine options — float_precision, lineterminator, low_memory and the parallel read of large local files are all off the table. So the rule of thumb: set engine="python" because a specific argument needs it, or because you are debugging a parse by running it under the more forgiving parser — never as a default, and never "just in case". Feature support also moves between releases, so if a parameter behaves unexpectedly, the read_csv() reference for your installed pandas version is the authority.

The full workflow: CSV → DataFrame → CSV

Reading is the first half of most data jobs. The complete loop looks like this:

CSV file
  │
  │  pd.read_csv()
  ▼
DataFrame
  │
  │  filter / clean / aggregate
  ▼
DataFrame
  │
  │  df.to_csv()
  ▼
CSV file
One direction loads, the other writes back — the same file format at both ends.

After cleaning or transforming the DataFrame, you can turn it back into a file with the pandas to csv converter: paste the frame, set the options, and it produces the matching df.to_csv() call and the CSV itself, all inside the browser. The arguments that call takes — paths, filenames, selected columns, encoding, and how to verify the file that lands on disk — are covered end to end in how to save a DataFrame to CSV, which is the page to read next for the writing direction.

Two follow-on cases have their own pages: when the export must not carry a row index (and the round trip above must not produce an Unnamed: 0 column), see pandas to CSV without index; when the job is to add rows to a file that already exists rather than replace it, see appending to a CSV with Pandas. This article stays on the reading half of the loop — everything about writing belongs to those pages.

Frequently asked questions

How do I read a CSV file with Pandas?

Import pandas and call pd.read_csv("file.csv"). The call returns a DataFrame with the file's first row as column names and a fresh 0..n-1 row index. Run df.head() straight afterwards to confirm the header landed where you expected.

What function does Pandas use to read CSV files?

pd.read_csv(). It accepts a path, a URL or any object with a read() method and returns a DataFrame. read_table() is the general delimited variant, but read_csv() with sep= covers every other delimiter as well, so read_csv() is the one to reach for.

How do I read a CSV without a header?

Pass header=None: pd.read_csv("data.csv", header=None). The columns are then numbered 0, 1, 2 and the first line becomes a data row. Add names=["name", "score", "city"] to the same call to label them. If the file does have a header row you want to replace, pass header=0 together with names=.

How do I specify a delimiter in read_csv()?

Use sep: sep=";" for semicolons, sep="\t" for tabs, sep="|" for pipes. delimiter= is an exact alias. A file read as one giant column is almost always a separator mismatch, not a broken file.

How do I read a CSV file in Google Colab?

pandas is preinstalled, so only the file location matters. For a file on your computer: from google.colab import files; files.upload(), then pd.read_csv("data.csv") — uploads land in /content. For a file in Drive: drive.mount("/content/drive") and read from /content/drive/MyDrive/data.csv. A public raw URL can be passed to read_csv() directly.

When should I use engine="python"?

Only when you need something the default C parser does not offer: skipfooter, separator sniffing with sep=None, a multi-character or regex separator, or a callable on_bad_lines. It parses more slowly, so leave engine unset for ordinary comma- or tab-separated files.

How do I handle encoding errors when reading a CSV?

A UnicodeDecodeError means the bytes are not valid in the declared encoding, which is UTF-8 by default. Read the file with the encoding it was written in — utf-8-sig when it starts with a byte-order mark, latin-1 as a byte-for-byte fallback that never raises — or pass encoding_errors="replace" to keep going with substitutions.

Why does my DataFrame have an "Unnamed: 0" column?

The CSV carries an extra index column, usually because it was written with df.to_csv() and the default index=True. Drop it with df.drop(columns=["Unnamed: 0"]) on the way in, and see the guide on writing without an index so the next export does not include it.

Reviewed against the pandas 3.0.6 pandas.read_csv() reference — 8 October 2026.

← All blog posts