Load a CSV file — from your disk, or straight from a URL — into a data frame.
What does the first line of a Pandas project usually look like? It is pd.read_csv, and in nearly every Bamboo Weekly solution that line does not name a file at all. It names a URL:
import pandas as pd
url = ('https://earthquake.usgs.gov/fdsnws/event/1/query.csv'
'?starttime=2024-01-01&endtime=2024-12-31&minmagnitude=6')
pd.read_csv(url)
Pandas fetches the URL for you. No requests, no download step, no temporary file to clean up — which is why the examples on this page run anywhere, and why so many exercises here start at a live government API.
That is the easy part. The hard part is that read_csv takes more than forty keyword arguments, and getting a handful of them right at load time saves you an afternoon of repair work afterward. Dates left as strings, an identifier column guessed as an integer, forty columns read when you needed four — each is cheap to prevent and painful to undo.
Official documentation: pandas.read_csv
The arguments that earn their keep
pd.read_csv(source,
usecols=['a', 'b'], # read only these columns
dtype={'zip': str}, # stop Pandas from guessing
parse_dates=['when'], # real datetimes, not strings
date_format='%d/%m/%Y', # ... for when the format is ambiguous
na_values=['-', 'n/a'], # what counts as missing
skiprows=3, # step over a title block
header=0, # which row holds the column names
names=['a', 'b'], # or supply the names yourself
index_col='id', # which column becomes the index
sep='\t', # not everything is comma-separated
thousands=',', # so "1,234" arrives as a number
encoding='Latin-1', # when the file is not UTF-8
nrows=1000) # peek at something enormous
I go through the most important of these in The six most important read_csv arguments in Pandas, and cover getting the index right on the way in with Load your data with the right index in Pandas.
A worked example, on real data
The USGS publishes every earthquake it records as CSV, through a public API. No key, no signup — just a URL with a date range and a minimum magnitude. Bamboo Weekly #3 used this same source, and reached for parse_dates for exactly the reason below.
Let's find the five largest earthquakes of 2024:
(
pd.read_csv(url,
usecols=['time', 'place', 'mag', 'depth'],
parse_dates=['time'])
.sort_values('mag', ascending=False)
.head(5)
)
time depth mag place
98 2024-01-01 07:10:09.476000+00:00 10.000 7.5 2024 Noto Peninsula, Japan Earthquake
43 2024-07-19 01:50:48.571000+00:00 127.291 7.4 41 km ESE of San Pedro de Atacama, Chile
73 2024-04-02 23:58:12.173000+00:00 40.000 7.4 15 km S of Hualien City, Taiwan
2 2024-12-17 01:47:25.741000+00:00 54.372 7.3 24 km WNW of Port-Vila, Vanuatu
49 2024-06-28 05:36:36.902000+00:00 24.000 7.2 10 km WSW of Atiquipa, Peru
Two arguments did real work there. usecols meant the eighteen columns we did not want were never read. parse_dates meant that time arrived as a datetime, so it can be sorted, filtered by range, and grouped by month — none of which works on a string that merely looks like a date. And the chain reads cleanly because sort_values hands back a data frame; only inplace=True returns None, which is one of several reasons the core developers ask us not to use it.
usecols: memory you never allocate
Here is a US zip code database, 42,522 rows and sixteen columns, which Bamboo Weekly #42 used to map plant hardiness zones:
zip_url = 'http://uszipcodelist.com/zip_code_database.csv'
everything = pd.read_csv(zip_url)
everything.memory_usage(deep=True).sum() / 1024**2
7.9
Nearly 8 MB, and I wanted three of those columns. Ask for three:
needed = pd.read_csv(zip_url, usecols=['zip', 'primary_city', 'state'])
needed.memory_usage(deep=True).sum() / 1024**2
1.6
Be precise about what happened there: usecols did not free 6 MB, because those 6 MB were never allocated. The columns were skipped during parsing, whereas dropping them afterward would have cost you the full read first. On a file that fits comfortably in memory this is a nicety; on one that does not, it is the difference between working and not. (Note the deep=True; without it you get a number that ignores the contents of string columns — see memory_usage.)
nrows is the same instinct applied to rows. Meeting a large file for the first time, pd.read_csv(url, nrows=100) shows you the columns and the shape of the values in a fraction of a second.
dtype: where the leading zero goes to die
Look closely at the zip codes we just read:
needed.head(3)
zip primary_city state
0 501 Holtsville NY
1 544 Holtsville NY
2 601 Adjuntas PR
Holtsville, New York is 00501, not 501. The file even says so — the raw line reads "00501","UNIQUE","Holtsville",…, quoted and everything. Quoting does not save you. A column of digits looks numeric to Pandas, so it becomes an int64, and the leading zeros are gone. Tell it otherwise:
pd.read_csv(zip_url, usecols=['zip', 'primary_city', 'state'],
dtype={'zip': str}).head(3)
zip primary_city state
0 00501 Holtsville NY
1 00544 Holtsville NY
2 00601 Adjuntas PR
This is the single most common real-world CSV bug I see, and zip codes are only the friendliest example; FIPS county codes, phone numbers, account numbers and ISBNs all have the same shape. If a column is a label rather than a quantity, pass dtype=str for it, even when — especially when — it is made entirely of digits. You will never sum these values, and an integer zip code will not join against a text one.
dtype earns its keep in the other direction too. A column with few distinct values is a good candidate for 'category', and dtype_backend='pyarrow' gives you Arrow-backed columns throughout:
pd.read_csv(zip_url, usecols=['zip', 'primary_city', 'state'],
dtype_backend='pyarrow').dtypes
zip int64[pyarrow]
primary_city string[pyarrow]
state string[pyarrow]
dtype: object
Note that dtype_backend chooses how columns are stored, not who does the parsing — that is engine=, and engine='pyarrow' is a genuinely faster reader on large files. The two combine, and appear together throughout recent solutions.
Dates: parse_dates, and date_format when that is not enough
parse_dates takes a list of columns and hands them to the datetime parser. When the format is unambiguous, that is all you need. When it is not, here is what happens:
from io import StringIO
csv = "when,value\n01/02/2024,1\n15/03/2024,2\n"
pd.read_csv(StringIO(csv), parse_dates=['when']).dtypes
when str
value int64
dtype: object
No exception. No warning at all — I checked, with warnings forced on. The when column is simply still text, and you find out three steps later when .dt.month raises an AttributeError. Say what you mean instead:
pd.read_csv(StringIO(csv), parse_dates=['when'],
date_format='%d/%m/%Y').dtypes
when datetime64[us]
value int64
dtype: object
Nobody remembers whether the month is %m or %M, and I do not either. I keep strfti.me open when I am writing one of these — it shows you the result as you type, which beats scrolling a table of directives. Do check your work, though: a date_format that does not match is exactly as silent as no date_format at all. Bamboo Weekly #65 walks through this failure on real data, landing on date_format='%m/%d/%Y %I:%M:%S %p'. For a column that is already loaded, see to_datetime.
When the file fights back
Real files are not always polite, and a handful of arguments handle most of the rudeness.
na_values and its companion keep_default_na decide what counts as missing, and the defaults have teeth. Bamboo Weekly #35 grouped countries by continent and got five back instead of six, because North America is abbreviated NA, which read_csv treats as missing. So is Namibia's country code:
csv = "country,continent\nCanada,NA\nBrazil,SA\nNamibia,NA\n"
pd.read_csv(StringIO(csv))
country continent
0 Canada NaN
1 Brazil SA
2 Namibia NaN
keep_default_na=False turns that off, after which you can name the strings you do want treated as missing with na_values.
skiprows, header and names deal with files that put something other than column names on line one. NASA's global temperature record opens with a title line, so a naive read produces a single column named after the title:
gistemp = 'https://data.giss.nasa.gov/gistemp/tabledata_v4/GLB.Ts+dSST.csv'
pd.read_csv(gistemp, nrows=2).columns.tolist()
['Land-Ocean: Global Means']
Step over it, declare what missing looks like, and set the index in the same call:
(
pd.read_csv(gistemp, skiprows=1, na_values=['***'], index_col='Year')
[['Jan', 'Jul', 'J-D']]
.tail(3)
)
Jan Jul J-D
Year
2024 1.25 1.20 1.28
2025 1.38 1.02 1.19
2026 1.08 1.23 NaN
sep handles files that are comma-separated in name only — tabs, semicolons, pipes. And storage_options handles the ones that refuse to talk to you at all. The Bureau of Labor Statistics, source of the JOLTS and inflation data behind several Bamboo Weekly issues, returns HTTP 403 to Pandas' default user agent and wants a contact address in the header:
url = 'https://download.bls.gov/pub/time.series/jt/jt.state'
pd.read_csv(url, sep='\t')
HTTPError: HTTP Error 403: Forbidden
(
pd.read_csv(url, sep='\t',
storage_options={'User-Agent': 'Pandas (you@example.com)'},
usecols=['state_code', 'state_text'])
.head(4)
)
state_code state_text
0 00 Total US
1 01 Alabama
2 02 Alaska
3 04 Arizona
Wikipedia behaves the same way, where a plain storage_options={'User-Agent': 'Mozilla/5.0'} is enough.
Two more by name: thousands=',' turns "1,234,567" into a number rather than a string, and encoding='Latin-1' rescues the older government files that predate universal UTF-8.
Five mistakes people make
Letting Pandas guess the dtype of an identifier column. Zip codes, FIPS codes, phone numbers and account IDs are labels made of digits. Pandas reads them as integers and 00501 becomes 501, silently and permanently. Pass dtype={'zip': str}.
Reading the whole file and then throwing most of it away. usecols skips columns at parse time; df.drop(columns=…) does not. If you need four columns out of forty, say so on the way in.
Assuming parse_dates worked. It fails quietly, leaving you a column of strings and no error message. Check df.dtypes after any read where dates matter — it costs one line.
Forgetting thousands. One comma inside "1,234" makes the entire column text. Your sums fail, your plots come out as a bar chart of every distinct string, and nothing anywhere says why.
Reaching for low_memory on stale advice. The internet is full of confident and contradictory claims here, so I checked. On Pandas 3.0.5 the argument still exists, still defaults to True, and still does something: under the default C engine, a column whose type changes partway through the file comes back as object, with DtypeWarning: Columns (0: a) have mixed types. Specify dtype option on import or set low_memory=False. Passing low_memory=False does fix that, at the cost of buffering the whole file — but note which remedy the warning names first. dtype= states your intent rather than hoping inference lands well. And with the pyarrow reader the argument is not merely useless; it raises ValueError: The 'low_memory' option is not supported with the 'pyarrow' engine. The name was always backwards: low_memory=False uses more memory, not less.
Where it shows up in Bamboo Weekly
read_csv appears in 98 Bamboo Weekly posts, across 97 issues — more than any other method on this site. These four are free to read, and each leans on a different argument.
Bamboo Weekly #3 is the earthquake data above, and the cleanest illustration of why parse_dates belongs in the read rather than in a follow-up step.
Bamboo Weekly #42 joins two USDA plant hardiness releases against a zip code database. Every read in it carries dtype={'zipcode': str} alongside usecols and index_col — without which the join would match nothing at all.
Bamboo Weekly #65 is the date_format post: it starts with parse_dates=['Date'], shows what Pandas does when it cannot infer a format, and fixes it with an explicit one.
Bamboo Weekly #35 is the NA-is-North-America story, and worth reading as a correction rather than a solution — a reader spotted the missing continent, and the updated post carries the keep_default_na=False fix. It also uses positional usecols=[0, 22] with skiprows=3 and names=, which is how you read one column out of a file whose headers you would rather not trust.
Practice it
Work through a read_csv exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/read-csv/
Go deeper
Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.
If your data is in a spreadsheet, see read_excel, which shares usecols, header, skiprows, nrows and na_values with this function and adds sheet_name. If it is in an HTML table, see read_html. And once it is loaded, memory_usage tells you what it cost you.
Related methods
.memory_usage()— when you want to see what the columns you did read are costingpd.read_excel()— when the file is a spreadsheet with sheets, headers and decorative rowspd.read_parquet()— when the data is columnar and typed, which is smaller and faster than CSVpd.read_html()— when the data is a table on a web page and nobody published a filepd.DataFrame()— when the data is small enough to type out, or you are building a lookup table.read_json()— when the response is JSON rather than delimited text
See it on real data
Below are the 100 Bamboo Weekly exercises that use read_csv on real-world data — try each one, then study the worked solution.
- Bamboo Weekly #188: Hurricane season
- Bamboo Weekly #186: Renaming places
- Bamboo Weekly #185: US-Canada trade
- Bamboo Weekly #184: Parmesan cheese
- Bamboo Weekly #183: Hiring
- Bamboo Weekly #182: Surveillance technology
- Bamboo Weekly #181: Housing costs
- Bamboo Weekly #180: Movies
- Bamboo Weekly #179: Krakow tourism
- Bamboo Weekly #176: Religious restrictions
- Bamboo Weekly #175: Inflation
- Bamboo Weekly #174: Vacation
- Bamboo Weekly #172: World Cup
- Bamboo Weekly #171: Hantavirus
- Bamboo Weekly #170: Port of Long Beach
- Bamboo Weekly #169: Press freedom
- Bamboo Weekly #166: Income tax
- Bamboo Weekly #165: Artemis II
- Bamboo Weekly #164: Fertilizer
- Bamboo Weekly #162: Spotify and car accidents
- Bamboo Weekly #161: Missiles in Israel
- Bamboo Weekly #160: Strait of Hormuz
- Bamboo Weekly #158: University endowments
- Bamboo Weekly #156: Winter Olympics
- Bamboo Weekly #155: Gold
- Bamboo Weekly #153: Venezuela
- Bamboo Weekly #152: Congestion pricing
- Bamboo Weekly #149: Flu season
- Bamboo Weekly #148: US Manufacturing
- Bamboo Weekly #142: Hurricanes
- Bamboo Weekly #141: Argentina
- Bamboo Weekly #140: Stack Overflow survey
- Bamboo Weekly #138: Federal workers
- Bamboo Weekly #137: UN Security Council
- Bamboo Weekly #136: Indian vehicles
- Bamboo Weekly #134: Taiwan weather
- Bamboo Weekly #133: Wind power
- Bamboo Weekly #132: JetBrains survey
- Bamboo Weekly #131: Canadian border crossings
- Bamboo Weekly #125: Shrinking dollars
- Bamboo Weekly #121: Research funding
- Bamboo Weekly #119: Python conferences
- Bamboo Weekly #118: Flight delays
- Bamboo Weekly #117: Electricity
- Bamboo Weekly #116: Philadelphia Fed survey
- Bamboo Weekly #108: Measles
- Bamboo Weekly #107: Consumer confidence
- Bamboo Weekly #106: Flu season
- Bamboo Weekly #105: Federal employees
- Bamboo Weekly #104: Aviation accidents
- Bamboo Weekly #103: CDC data
- Bamboo Weekly #102: WordPress
- Bamboo Weekly #101: Los Angeles Fires
- Bamboo Weekly #99: Literacy and numeracy
- Bamboo Weekly #93: Anti-politics
- Bamboo Weekly #87: Nuclear power
- Bamboo Weekly #85: PACs and parties
- Bamboo Weekly #84: Central banks
- Bamboo Weekly #81: School
- Bamboo Weekly #78: Stock markets
- Bamboo Weekly #75: Refugees
- Bamboo Weekly #73: Avocado hand
- Bamboo Weekly #72: City travel
- Bamboo Weekly #67: Electric cars
- Bamboo Weekly #66: Pittsburgh
- Bamboo Weekly #65: Microplastics
- Bamboo Weekly #60: Iceland
- Bamboo Weekly #59: Long covid
- Bamboo Weekly #58: NATO
- Bamboo Weekly #57: International arms trade
- Bamboo Weekly #56: Rent increases
- Bamboo Weekly #55: IVF
- Bamboo Weekly #52: Border encounters
- Bamboo Weekly #51: Academy Awards
- Bamboo Weekly #50: Red Sea shipping
- Bamboo Weekly #49: Campaign finance
- Bamboo Weekly #47: Minimum wage
- Bamboo Weekly #46: Pedestrians
- Bamboo Weekly #44: Global economics
- Bamboo Weekly #43: Financial protection
- Bamboo Weekly #42: Plant hardiness
- Bamboo Weekly #39: WeWork
- Bamboo Weekly #35: Terrorism
- Bamboo Weekly #34: House of Representatives
- Bamboo Weekly #33: Fracking
- Bamboo Weekly #29: Auto accidents
- Bamboo Weekly #25: Entrepreneurship
- Bamboo Weekly #24: Wildfire smoke
- Bamboo Weekly #22: Banana index
- Bamboo Weekly #21: Electric cars
- Bamboo Weekly #18: World population
- Bamboo Weekly #15: Eurovision
- Bamboo Weekly #14: JOLTS
- Bamboo Weekly #13: Python developers
- Bamboo Weekly #11: Software jobs
- Bamboo Weekly #10: Oil prices
- Bamboo Weekly #9: US house prices
- Bamboo Weekly #7: Bank failures
- Bamboo Weekly #4: Eating well
- Bamboo Weekly #3: Earthquake
Part of the Pandas Methods Index. See also practice by skill.