Read JSON and HTML Tables
Read JSON and HTML Tables with pandas — Data Analysis with Python.
Focus: read json and html tables with pandas
When your data arrives as a nested JSON API response or a dozen HTML tables scraped from a webpage, pandas can transform it into clean DataFrames with almost no effort. The pain: manually flattening JSON dicts or listing every <td> tag wastes hours and invites subtle bugs. This lesson shows you how pd.read_json() and pd.read_html() do that heavy lifting, so you can spend your time analyzing, not parsing.
The problem this lesson solves
Real-world data doesn't always come in neat CSV files. APIs return JSON with nested objects and arrays, and publications publish statistics as HTML tables. Trying to read those with custom Python loops is slow, error-prone, and you'll reinvent the wheel. Without pandas' built-in readers, you end up writing hundreds of lines of dictionary-key-checking or BeautifulSoup soup.select() logic just to get tabular data into a DataFrame.
The pain it eliminates: - Flattening nested JSON by hand using loops or ugly list comprehensions. - Handling malformed JSON with try/except blocks before you even see the data. - Scraping HTML tables with regex or manual string splitting — fragile and slow. - Losing the type inference and convenient indexing that DataFrame offers.
Pandas gives you read_json() and read_html() — two high-level, battle-tested functions that parse these formats and return DataFrame (or list of DataFrames) ready for analysis.
Core concept / mental model
Think of read_json() and read_html() as adapter functions. They take a file-like source or raw string in one format and convert it into pandas' central tabular structure, the DataFrame. You tell them what format you have; they handle the messy details.
pd.read_json()– Parses JSON (string, file, or URL) and creates a DataFrame. It can interpret the JSON structure in several orientations (split,records,index,columns,table,values) depending on how your data is organized.pd.read_html()– Reads all<table>elements from an HTML document (URL, file, or raw HTML string) and returns a list of DataFrames (one per table). It uses parser libraries (like lxml or html5lib) under the hood.
A helpful analogy: JSON is like a messy physical inbox — sometimes letters are nested in boxes within boxes. read_json is your assistant who knows how to flatten certain inbox layouts. HTML tables are like a catalog with multiple product pages; read_html reads each page (table) and gives you a stack of tidy spreadsheets.
Definitions to keep in your pocket
- DataFrame: A 2D tabular data structure with labeled rows and columns — pandas' heart.
- Orientation (JSON): The layout of the JSON relative to how pandas should map it to rows and columns.
- List of DataFrames:
read_htmlreturns a list because a single HTML page can contain multiple tables.
How it works step by step
1. Reading JSON with pd.read_json()
Step one: understand your JSON's structure. Is it a flat array of objects? A dict of lists? Nested objects? That determines the orient parameter.
orient='records'– Each JSON object is a row. Most common for API responses.orient='split'– JSON has keys likeindex,columns, anddata— good for round-tripping DataFrames.orient='index'– JSON keys become DataFrame index labels.orient='table'– A full pandas table schema with index and columns metadata.orient='values'– Just a flat array of arrays; DataFrame gets default integer columns.
Step two: call pd.read_json() with your source (string, path, or URL). pandas tries to infer orientation automatically, but it pays to be explicit.
Step three: inspect the result with df.head() or df.info().
2. Reading HTML tables with pd.read_html()
Step one: pass the HTML source — a URL (HTTP request handled internally), an HTML string, or a file handle.
Step two: parse all tables. Since multiple tables may exist, read_html returns a list of DataFrames. You often need to select the right one.
Step three: use df.iloc[2] or loop to check each table. Optionally, use match (regex) or attrs to target a specific table.
3. Clean up and analyze
Once you have a DataFrame, the world of pandas is open: you can handle missing values, rename columns, or merge with other frames — all without returning to the raw file.
Hands-on walkthrough
Let's put theory into action with two complete examples you can run yourself.
Example 1: Reading a JSON API response
import pandas as pd
import json
# Sample JSON as a string (emulating an API response)
json_data = '''
[
{"name": "Alice", "score": 88},
{"name": "Bob", "score": 72},
{"name": "Charlie", "score": 93}
]
'''
# Read JSON with explicit orientation for array of objects
df = pd.read_json(json_data, orient='records')
print(df)
print('\nData types:')
print(df.dtypes)
Expected output:
name score
0 Alice 88
1 Bob 72
2 Charlie 93
Data types:
name object
score int64
dtype: object
Example 2: Reading HTML tables from a webpage
import pandas as pd
# Sample HTML with a simple table
html_string = '''
<table>
<tr><th>Product</th><th>Price</th></tr>
<tr><td>Laptop</td><td>1200</td></tr>
<tr><td>Mouse</td><td>25</td></tr>
</table>
'''
# read_html returns a list of DataFrames
table_list = pd.read_html(html_string)
print(f"Number of tables found: {len(table_list)}")
df_table = table_list[0] # select the first table
print(df_table)
Expected output:
Number of tables found: 1
Product Price
0 Laptop 1200
1 Mouse 25
Pro tip: If you're working with an online HTML page, you can pass the URL directly:
pd.read_html('https://example.com/data_table.html'). pandas will fetch and parse it.
Example 3: Flattening nested JSON
Let's tackle a common messy API response — nested objects.
import pandas as pd
nested_json = '''
{
"report": "Q1 sales",
"region": "North",
"items": [
{"id": 1, "details": {"product": "Widget", "qty": 10}},
{"id": 2, "details": {"product": "Gadget", "qty": 5}}
]
}
'''
# read_json with default orient won't flatten nested dicts automatically
raw = pd.read_json(nested_json)
print("Raw DataFrame:")
print(raw[['id', 'details']])
# Use json_normalize to flatten nested 'items'
norm = pd.json_normalize(nested_json, 'items', ['report', 'region'])
print("\nFlattened DataFrame:")
print(norm)
Expected output:
Raw DataFrame:
id ...
0 1 ...
1 2 ...
Flattened DataFrame:
id details.product details.qty report region
0 1 Widget 10 Q1 sales North
1 2 Gadget 5 Q1 sales North
Compare options / when to choose what
The table below compares the main read_json orientations and how they fit common data layouts.
| Orientation | JSON structure | Best for when... | Rows become | Columns become |
|---|---|---|---|---|
'records' |
Array of objects | API responses, records-style data | Each object | Object keys |
'split' |
Dict with index, columns, data |
Round-tripping DataFrames | Provided in data |
Provided in columns |
'index' |
Dict: keys as index labels | Data keyed by row label | Dict values | Inner keys |
'columns' |
Dict: keys as column names | Data keyed by column label | Inner keys | Dict keys |
'values' |
Nested array (list of lists) | Matrix/array data | Sub-lists | Integer 0..n |
'table' |
Full pandas table schema | When you need to preserve index/columns from a saved DataFrame | Provided | Provided |
For HTML, your choice is mainly about which parser to use (lxml vs html5lib) and whether to filter with match or attrs.
Key insight: Use orient='records' for most API data. Use orient='split' when you save DataFrames to JSON and want to restore them exactly.
When reading HTML, if the page has multiple tables, you must select the right one. That selection is a mini-decision: by index, by match regex, or by attrs (like {'class': 'data'}).
Troubleshooting & edge cases
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
ValueError: Expected object or value |
Malformed JSON (extra comma, missing quote) | Validate with json.loads() first or use assert |
KeyError when accessing column |
Orientation doesn't match | Print df.columns to inspect; set orient explicitly |
EmptyDataError from read_html |
No <table> found or HTML is malformed |
Check URL/string; use attrs to target; try a different parser (html5lib) |
ParserError: Error tokenizing data |
Truly malformed content, not JSON | Pre-process or use error_bad_lines=False (deprecated) — better to clean source |
| Duplicate column names | JSON/HTML table headers repeat | Use df.columns = ... after reading to rename |
Edge cases
- Missing values: JSON nulls appear as
NaNin DataFrame; HTML empty rows become rows of NaN—drop them withdropna()if needed. - Type inference: Numeric strings stay as text. Convert with
pd.to_numeric()after reading. - Nested JSON:
read_jsonwon't flatten; you needjson_normalize(orpd.json_normalize) as shown earlier. - Header rows in HTML tables: Sometimes the first row is not a header; set
header=Noneand assign column names manually. - Mixed JSON structures: When JSON varies from record to record, pandas might create many columns or a lot of NaNs — consider
json_normalizewithmax_levelto flatten deeper. - HTTP errors: When reading from a URL, you may get
HTTPError— check response status first or userequests.get()for more control.
Pro tips
- Before reading JSON, always inspect a sample with
json.loads()to understand structure — it saves debugging time.- For HTML tables, if you get a
LookupErrorabout lxml, install it:pip install lxmlor useengine='html5lib'.- Use
df.head()right after reading to sanity-check your columns and rows.
What you learned & what's next
You've mastered the core idea behind reading JSON and HTML tables with pandas, and you applied it with hands-on examples. Specifically you learned:
- The fundamental role of
pd.read_json()andpd.read_html()as adapters. - Common JSON orientations and how to choose the right one.
- How to extract tables from a webpage and select the one you need.
- How to troubleshoot typical parsing and orientation errors.
This skill removes the biggest barrier between you and the data locked in APIs or web pages. Now that you can load JSON and HTML into DataFrames, the natural next step is cleaning and reshaping that data — handling missing values, renaming columns, and aggregating. That's exactly where the next lesson in this track leads you, enabling you to turn raw parsed data into analysis-ready datasets.
Practice recap
Now it's your turn: find a small JSON API (like a fake user API) and a webpage with an HTML table, then load both into DataFrames using the methods above. Try changing the orient parameter for the JSON and selecting different tables with read_html to see how the output changes. This hands-on repetition will lock in the pattern.
Common mistakes
- Assuming
read_jsonauto-flattens nested objects — it doesn't; you needjson_normalizeorpd.json_normalize. - Forgetting that
read_htmlreturns a list of DataFrames, not a single DataFrame; tryingdf.columnsimmediately will error. - Not specifying
orientwhen the JSON is an array; pandas might misinterpret it ascolumnsorientation, producing a transposed DataFrame. - Passing a URL that returns a 404 or non-HTML content — check the HTTP status first.
Variations
- Use
pd.json_normalize(orjson_normalize) to flatten deeply nested JSON beyond whatread_jsonsupports. - Use the
matchorattrsparameters inread_htmlto select specific tables instead of picking by index. - For streaming or massive JSON files, consider
json.loadwith manual iterators orpandas.read_jsonwithchunksize.
Real-world use cases
- Pull structured data from a REST API (like a list of users or orders) and load it directly into a DataFrame for analysis.
- Scrape multiple sports league standings tables from a stats website, parse them all with
read_html, and combine or compare them. - Load a DataFrame-backed JSON export (using
orient='split'or'table') from a previous analysis step and restore it for continued processing.
Key takeaways
pd.read_json()andpd.read_html()turn JSON strings/files and HTML tables into pandas DataFrames with minimal code.- Always understand your JSON structure to choose the correct
orientparameter —'records'is typical for APIs. pd.read_html()returns a list of DataFrames; select the right table using indexing,match, orattrs.- Nested JSON flattening is done with
pd.json_normalize, not withread_jsonalone. - Troubleshooting parsing errors starts with inspecting the exact source data and knowing the common failure modes.