~ / guides / How to Scrape Zillow With Python (Requests & BeautifulSoup)

How to Scrape Zillow With Python (Requests & BeautifulSoup)

DC
Dana Cole
Zillow data engineer · about the author
the short version
  • A plain requests + BeautifulSoup call to a Zillow /homedetails/ page returns HTTP 403 with no property data. I ran it in June 2026 and a full Chrome User-Agent changed nothing.
  • Zillow blocks on IP reputation and browser fingerprint through PerimeterX (now HUMAN Security). The 403 body is a px-captcha Press & Hold page, so there is no HTML to parse.
  • When a page does load, the listing data sits outside the visible HTML. It lives in a <script id="__NEXT_DATA__"> JSON blob (and gdpClientCache), so the job is to parse JSON.
  • The routes that work: residential proxies with slow pacing, the open-source pyzill library, or a scraper API that returns parsed JSON and handles the blocking for you.

I tried to scrape Zillow with Python the lazy way first: one requests.get against a property page, ready to hand the HTML to BeautifulSoup. It came back 403 before BeautifulSoup had a single tag to chew on. That failure is the entire subject of this guide, because it is what almost everyone hits on their first run, and the usual fix people reach for (set a browser User-Agent) did nothing for me.

Below is exactly what I ran in June 2026, what Zillow returned, where the property data actually lives when a page does load, and the three setups that get clean data back. The goal throughout is web scraping Zillow real estate data with Python: prices, addresses, beds and baths, and the Zestimate.

Why does a Python requests scraper get blocked by Zillow?

A Python requests scraper gets blocked by Zillow because Zillow scores the request on IP reputation and browser fingerprint, and a datacenter IP fails that check before any HTML is served. When I sent a GET to a live /homedetails/ page, Zillow answered with an HTTP 403 and a roughly 6 KB px-captcha “Press & Hold” page, regardless of the User-Agent I set.

I ran the same request two ways against a real Zillow property URL:

RequestUser-AgentResultBody contains
GET /homedetails/...2092-zpid/none403, 5,952 bytesPerimeterX, px-captcha, Press & Hold
GET /homedetails/...2092-zpid/full Chrome desktop string403, 5,952 bytesPerimeterX, px-captcha, Press & Hold

The two responses were byte-for-byte identical. That is the tell: the User-Agent is not the lever. The block decision happens on the IP and the TLS fingerprint, so swapping the header just sends a different label on the same flagged connection. The body was not a listing at all. It was the PerimeterX challenge page, which means BeautifulSoup had no property HTML to parse even if I had pointed it at the response.

Zillow’s anti-bot layer is PerimeterX, now branded HUMAN Security after its 2022 merger. It profiles IP reputation, JA3 TLS fingerprints, and JavaScript and behavioral signals, then issues the Press & Hold captcha to anything that scores like a bot. Datacenter ranges are pre-flagged. The fix has to change where the request comes from, which the working approaches below all do. First, it helps to know where Zillow keeps the data once you are past the gate.

Where does Zillow store property data in the page?

Zillow stores property data in a JSON object inside a <script id="__NEXT_DATA__"> tag, which sits apart from the visible HTML you see in the browser. Zillow runs on Next.js, so the price, address, bedrooms, bathrooms, square footage, and Zestimate are serialized into that script block and hydrated into the page by JavaScript on load.

This is the single most important fact for scraping Zillow with Python, and it is why CSS-selector tutorials age badly. The visible markup (class names like ListItem-c11n-...) is generated client-side and churns constantly. The __NEXT_DATA__ payload is the stable source. On property detail pages the listing object has historically lived under keys such as gdpClientCache and hdpApolloPreloadedData, the latter being an Apollo GraphQL cache, as Scrapfly’s Zillow teardown documents.

Here is the shape of what you are actually after, once a page returns real HTML:

WhereWhat it holdsHow you read it
<script id="__NEXT_DATA__">Full Next.js props: listing, photos, price historyjson.loads the tag text, then walk the dict
gdpClientCache (inside that JSON)Property detail object (the “GDP” page)Index into the cache, take the first value
hdpApolloPreloadedDataApollo GraphQL cache for the same detailFallback when the cache key differs
Search-card HTMLdata-test="property-card-price" on a spanA backup selector for list pages only

So the parsing job on Zillow is a JSON job. You locate one script tag, load it, and traverse the object for the fields you want. That is far more durable than chasing rotating CSS classes. The catch is still getting that HTML in the first place, which is where the request setup matters.

How do you scrape Zillow with Python, requests, and BeautifulSoup?

The way to scrape Zillow with Python is to fetch the page through a residential IP, use BeautifulSoup to grab the __NEXT_DATA__ script tag, and parse the JSON inside it. BeautifulSoup is the right tool for locating the script element. The actual data extraction happens in json.loads once you have that tag.

First, the naive version that fails, so you can recognize the failure when you see it:

import requests
from bs4 import BeautifulSoup

# The request that returns 403 from a datacenter IP. Do not ship it.
ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
     "(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
url = "https://www.zillow.com/homedetails/2092-zpid/"

r = requests.get(url, headers={"User-Agent": ua}, timeout=20)
print(r.status_code)              # -> 403
print("PerimeterX" in r.text)     # -> True  (the Press & Hold page)

soup = BeautifulSoup(r.text, "html.parser")
print(soup.find("script", id="__NEXT_DATA__"))  # -> None, there is no data

When that request succeeds through a residential IP, the parsing logic is short, because everything you need is in one script tag:

import json
import requests
from bs4 import BeautifulSoup

ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
     "(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
url = "https://www.zillow.com/homedetails/2092-zpid/"

# proxies = {"http": "http://USER:PASS@residential-host:port",
#            "https": "http://USER:PASS@residential-host:port"}
r = requests.get(url, headers={"User-Agent": ua}, timeout=20)  # add proxies=proxies
r.raise_for_status()

soup = BeautifulSoup(r.text, "html.parser")
raw = soup.find("script", id="__NEXT_DATA__").string
data = json.loads(raw)

# Walk the Apollo/GDP cache to the property object.
cache = data["props"]["pageProps"]["componentProps"]["gdpClientCache"]
prop = json.loads(cache)[next(iter(json.loads(cache)))]["property"]

print(prop["address"]["streetAddress"], prop["price"])
print(prop["bedrooms"], "bd /", prop["bathrooms"], "ba /", prop["livingArea"], "sqft")
print("Zestimate:", prop.get("zestimate"))

The exact key path inside gdpClientCache shifts as Zillow ships changes, so in practice you defensively search the loaded JSON for the keys you need and avoid hardcoding one path. The principle holds across versions: find __NEXT_DATA__, json.loads it, then read fields like streetAddress, price, bedrooms, bathrooms, livingArea, and zestimate out of the object. BeautifulSoup’s only job here is find("script", id="__NEXT_DATA__").

For Zillow search result pages, the visible cards do expose a data-test="property-card-price" span you can select with BeautifulSoup, as shown in Scrapingdog’s walkthrough. I treat that as a backup. The search results are also driven by a searchQueryState object and a paginated API that caps at 500 properties per query, so for list scraping the JSON route again wins on durability. None of this runs, though, until the 403 is solved, so the next section is the part that actually matters.

How do you avoid getting blocked when scraping Zillow?

You avoid the Zillow 403 by changing the IP reputation and the request rate, because those are the signals PerimeterX scores first. The User-Agent is not the variable that moved the result in my testing. These are the levers that do, in rough order of impact.

The honest tradeoff is maintenance. Doing all of this yourself means buying a residential proxy pool, rotating it, pacing requests, and handling the captcha pages when a session still trips. That is a real, ongoing project once you pass a few thousand listings, which is why most teams hand the blocking problem to a scraper API. Before that, it is worth knowing the open-source option that already wraps the proxy advice.

Is there a ready-made Python Zillow scraper on GitHub?

Yes, the most current ready-made Python Zillow scraper on GitHub is pyzill, an open-source library (around 100 stars) that returns Zillow search and property data as JSON. If you search “zillow scraper python github” this is the repo worth starting from. You install it with pip install pyzill and call methods for the workflows you need.

import pyzill

# Single property by its Zillow detail URL
prop = pyzill.get_from_home_url(
    "https://www.zillow.com/homedetails/2092-zpid/",
    proxy_url="http://USER:PASS@residential-host:port",
)
print(prop["price"], prop["address"])

pyzill exposes for_sale(), for_rent(), and sold() for search with geographic bounds, plus get_from_home_url() and get_from_home_id() for individual listings. It still needs you to supply the residential proxy. The library handles Zillow’s request structure; it does not own an IP pool, so the blocking problem is yours to feed it through.

Most other repos you will find are dated. ChrisMuir/Zillow, a Selenium scraper that extracts 11 variables per listing, carries a notice that it has not worked for most users since 2019 because, in the author’s words, Zillow “will display an unlimited number of CAPTCHA’s” and the script pauses indefinitely waiting for a human to clear them. That single line is the clearest case for not driving a headless browser at Zillow yourself. The alternative is to let a managed API absorb the proxies, fingerprinting, and captcha entirely.

How do you scrape Zillow at scale without managing proxies?

A scraper API removes the blocking work by accepting a Zillow URL and returning parsed JSON, with the residential proxy rotation, fingerprinting, and captcha handling done on the server side. You send one request and get the property object back, with no 403 to debug. In my runs against ChocoData, a single GET returned the same listing fields I was digging out of __NEXT_DATA__ by hand.

curl "https://chocodata.com/api/v1/zillow/property?url=https://www.zillow.com/homedetails/2092-zpid/&api_key=$CHOCO_API_KEY"

The Python version drops straight into the workflow above, except there is no proxy, no script-tag walking, and no captcha branch to write:

import os
import requests

resp = requests.get(
    "https://chocodata.com/api/v1/zillow/property",
    params={
        "url": "https://www.zillow.com/homedetails/2092-zpid/",
        "api_key": os.environ["CHOCO_API_KEY"],
    },
    timeout=60,
)
prop = resp.json()
print(prop["address"], prop["price"])
print(prop["bedrooms"], "bd /", prop["bathrooms"], "ba")
print("Zestimate:", prop.get("zestimate"))

This returns the same data the manual route does (price, address, bedrooms, bathrooms, living area, Zestimate, zpid) without registering proxies or solving Press & Hold. For a one-off pull of a handful of listings, the residential-proxy DIY route is fine. For continuous collection across many ZIP codes, rentals, or agent and lead records, offloading the blocking is usually the cheaper path once you price in your own time. The same approach scales to listing and property feeds and home price and sales history through their matching endpoints.

A summary of the four routes, so you can pick by volume:

RouteHandles the 403OutputBest for
Raw requests + BeautifulSoupNoNone (returns 403)Learning what the block looks like
Residential proxies + __NEXT_DATA__ parseYou manage itJSON you extractA few hundred listings, hands-on
pyzill + your proxyYou supply IPsJSONScripted pulls, comfortable with upkeep
ChocoData scraper APIYes, server-sideParsed JSONContinuous or high-volume collection

Before you collect anything at scale, it is worth knowing where the legal line sits, because the federal-access question and Zillow’s contract do not point the same way. I cover that in full in is scraping Zillow legal, and the wider method comparison lives in my complete guide to scraping Zillow.

FAQ

Can you scrape Zillow with Python and BeautifulSoup?

You can write the code, but a raw requests call to a Zillow listing page returns HTTP 403 from a datacenter IP, so BeautifulSoup has nothing to parse. In my June 2026 test the 403 body was a PerimeterX Press & Hold page. BeautifulSoup works only once you get a real 200 HTML response, which needs residential proxies or a scraper API.

Where is Zillow's property data in the page source?

Zillow renders most listing fields from a JSON object inside a <script id="__NEXT_DATA__"> tag, separate from the visible HTML. Older pages cached the same data under gdpClientCache and hdpApolloPreloadedData (an Apollo cache). You extract that script tag, run json.loads on it, and walk the object instead of using CSS selectors.

Why does my Zillow scraper get a 403 or a captcha?

Zillow uses PerimeterX (HUMAN Security) plus a Cloudflare layer to score every request on IP reputation, TLS fingerprint, and behavior. Datacenter IPs and fast request rates get a 403 with a 'Press & Hold' captcha. A better User-Agent does not fix it because the User-Agent is not what triggered the block.

Is there a Python Zillow scraper on GitHub?

Yes. pyzill is an actively maintained, open-source Zillow scraper (install with pip install pyzill) that returns search and property JSON. Its own README tells you to use rotating residential proxies. Older repos like ChrisMuir/Zillow (Selenium) have not worked since 2019 because of Zillow's captcha.

Is scraping Zillow with Python legal?

Scraping publicly visible pages is generally treated as legal in the US after hiQ v. LinkedIn, but Zillow's Terms of Use prohibit automated access by name, so it is a contract matter. Personal data such as agent details adds privacy-law exposure. I cover the detail in my guide on whether scraping Zillow is legal.

DC
Dana Cole
I've built Zillow data pipelines for years. On zillowscraperapi.com I run Zillow scraping methods against live pages and publish what actually holds up.