Zillow scraper: how to scrape property data with Python

Comparing listings across a city gets tedious when prices change and yesterday’s spreadsheet is already outdated. A Zillow scraper collects property information from listing pages and organizes it into records you can compare, update, and export without rebuilding the spreadsheet manually.
TL;DR
Check access first: Zillow’s published terms prohibit automated queries, including scraping. Choose an approved data program or another source you are authorized to collect and reuse before planning live extraction.
Test a small sample. Check extracted prices, addresses, and property details against their sources.
Handle additional results carefully. Follow pagination, remove duplicates, and investigate missing values.
Match proxy settings to the task. Consider rotating residential proxies for location and session control, or mobile proxies for unlimited traffic. Neither guarantees Zillow access.
Validate before exporting. Keep asking prices separate from estimates and retain collection dates.
CSV column preview
zpid,address,listing_status,asking_price,bedrooms,bathrooms,source_url,collected_at
What a Zillow scraper can extract
A Zillow data scraper can collect different information from search results, individual property pages, and professional profiles. Search results may provide listing summaries, while property pages may add descriptions, estimates, or history. Contact fields need separate verification because an agent or brokerage contact is different from a homeowner’s information.
Before you scrape Zillow data, define the output. The Python example below uses this sale-only schema:
Field | Format | Meaning in this example |
zpid | Required string | Property identifier, preserved as text and used for deduplication. |
address | Required string | Address text supplied by the search record. |
listing_status | Required string | FOR_SALE; the example excludes other listing statuses. |
asking_price | Number or null | Sale asking price, assumed to be USD in this example. |
bedrooms | Number or null | Reported bedroom count; zero remains a valid value. |
bathrooms | Number or null | Reported bathroom count, including fractional values where supplied. |
zestimate | Number or null | Estimated property value under the example’s USD assumption, kept separate from asking price. |
description | String or null | Description extracted from the selected property detail document. |
source_url | Required string | Property URL associated with the search record. |
search_source_url | Required string | URL identifying the search document that supplied the record. |
collected_at | Timestamp with time zone | Search-document observation time. |
detail_collected_at | Timestamp or null | Separate detail-document observation time; null when no detail document was processed. |
synthetic | Boolean | Marks records derived from inputs declared synthetic. A false value is not independent proof of authenticity. |
For saved documents, observation times come from the input manifest; the optional live fetcher records fetch-completion times. The supplied demonstration uses fictional timestamps.
Optional missing values remain null in JSON and blank in CSV. Missing required fields or malformed numeric values stop the example rather than producing guessed values.
Rent, last sale price, area, history, and professional contact fields require additional verified mappings. They are not exported by this example. Keep asking prices, sale prices, rental amounts, and estimates separate when extending the schema.
Choose between Python, scraping tools, and Zillow data access
Choose the route around the dataset you need and the maintenance you can handle. A one-time spreadsheet export may suit a hosted tool, while repeated collection with custom fields requires more control over extraction, validation, and updates.
Approach | Best suited to | Main consideration |
Python script | Custom extraction, validation, and storage | You maintain the code and verify source access. |
Reusable browser extraction through a local service | Requires deployment and an extraction preset tested against the intended pages. | |
Browser extension or hosted scraper | Exports without maintaining the collector | Check available fields, collection limits, privacy, and total cost. |
Managed scraping API | Outsourcing page fetching or extraction | Confirm whether the response contains HTML or structured property records. |
Official data program or research download | Approved integrations or regional market analysis | Check dataset coverage, access conditions, and permitted uses. |
Before committing to a tool, test a small sample containing your required fields. Check whether it retrieves property details or only search summaries, how it handles missing values, and whether its export preserves identifiers and dates.
A third-party product marketed as a “Zillow API” is not necessarily an official Zillow service. It may collect website content and deliver it through its own endpoint; the name alone does not establish affiliation, licensing, or collection permission.
Zillow Research provides regional housing metrics, including home values, rents, and inventory. Zillow Group’s developer programs cover specific products with their own access conditions. These options serve different purposes and should not be treated as unrestricted access to every listing or contact field.
Read more about No-Code Web Scraper.
How to scrape Zillow with Python
This example turns saved search and property documents into CSV and JSON records. It was tested against synthetic HTML samples and mocked HTTP responses, with no live Zillow pages fetched during validation.
The script reads one search document, retains up to five unique FOR_SALE properties, and can enrich one selected property from a matching detail document. The five-record limit belongs to this example, and not a Zillow search-result cap.
Test environment: Linux x86_64, Python 3.12.14, Requests 2.34.2, and Beautiful Soup 4.15.0. Dependencies are pinned in requirements.txt. The example was tested on November 4, 2026, and rechecked on November 5, 2026. Download the Python example, pinned dependencies, tested example, automated tests, and matching CSV and JSON exports.
Set up Python and define the output
Extract the accompanying example files, open their directory, and create a Python environment:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txtThe output contains property identifiers, addresses, listing status, asking prices, bedrooms, bathrooms, source URLs, and observation timestamps. Optional property details include a Zestimate and description.
Keep credentials outside the script. Its optional HTTP proxy configuration reads ZILLOW_PROXY_URL from the environment; leaving it unset uses a direct connection. This Python client does not inherit desktop Chrome’s proxy settings.
The supplied sample.json identifies two synthetic HTML files and their example capture times. Saved-page processing preserves those timestamps instead of presenting the parsing date as the collection date.
Inspect one search page and one property page
Before deciding how to scrape data from Zillow, inspect the pages covered by your authorization. Compare the original HTML response with the browser’s rendered page to establish where the required information appears.
The sample parser looks for embedded JSON in:
<script id="__NEXT_DATA__" type="application/json">It then checks these specific structures:
Search records:
props.pageProps.searchPageState.cat1.searchResults.listResults
Property details:
props.pageProps.componentProps.gdpClientCacheThese are assumptions exercised by the supplied samples, not permanent Zillow interfaces. Verify them against your permitted source documents before using the parser.
If fields appear only after JavaScript runs, an HTTP request and HTML parser will not reproduce that rendering. Browser automation may be necessary, but rendering alone does not establish permission or guarantee access.
Fetch and validate the response
The complete script’s fetch_html() function checks the HTTP status, final URL, and content type before returning HTML:
def fetch_html(url):
url = source_url(url)
proxy = os.getenv("ZILLOW_PROXY_URL")
if proxy and urlsplit(proxy).scheme != "http":
raise ValueError("This example supports an HTTP proxy endpoint only")
with requests.Session() as session:
session.trust_env = False
if proxy:
session.proxies = {"https": proxy}
with session.get(
url,
timeout=(5, 20),
allow_redirects=False,
) as response:
if response.status_code != 200:
raise ValueError(
f"Stopped: HTTP {response.status_code}; no automatic retry"
)
if response.url != response.request.url:
raise ValueError("Unexpected final URL")
source_url(response.url)
media_type = (
response.headers.get("Content-Type", "")
.split(";")[0]
.lower()
.strip()
)
if media_type not in {"text/html", "application/xhtml+xml"}:
raise ValueError("Expected an HTML response")
return response.textThe timeout values allow five seconds for connection establishment and twenty seconds of read inactivity; they are not a total execution deadline. Requests documents this distinction in its timeout guidance.
An HTTP 200 response still needs content validation. A challenge page, unexpected document, or changed data structure must not become a successful export containing zero records. The next function checks for the expected JSON container:
def page_props(html):
soup = BeautifulSoup(html, "html.parser")
node = soup.find("script", id="__NEXT_DATA__")
if node is None:
raise ValueError(
"Expected JSON block absent: inspect the returned page"
)
try:
props = json.loads(node.get_text())["props"]["pageProps"]
except (KeyError, TypeError, json.JSONDecodeError) as exc:
raise ValueError("Invalid or changed page-data structure") from exc
if not isinstance(props, dict):
raise ValueError("Expected a pageProps object")
return propsExtract listing records and selected property details
The example retains records explicitly marked FOR_SALE, requires an identifier and address, and removes duplicate ZPIDs before applying its five-record limit. Rental and sold records are excluded to prevent their prices from being labeled asking prices.
Optional numeric fields remain None when absent. Unexpected strings, negative values, and invalid numbers stop processing instead of being silently converted:
def number(value, positive=False):
if value is None:
return None
if (
isinstance(value, bool)
or not isinstance(value, (int, float))
or not math.isfinite(value)
or value < 0
or (positive and value == 0)
):
raise ValueError(
"Unexpected numeric value; review the field mapping"
)
return valueInclude the optional detail page only when additional fields are needed. Its URL and property identifier must match a selected search record. The parser stores its Zestimate separately from the asking price and records a separate detail-page timestamp.
This is central to learning how to scrape Zillow data accurately: preserve what each field means, and never manufacture missing values.
Run a small sample and inspect the result
Run the complete script against the supplied synthetic pages:
python zillow_sample.py sample.json --output run-01The output directory must be new. The tested sample produces:
Saved 2 unique listings to run-01One resulting record looks like this. All property information below is fictional:
{
"zpid": "9000000001",
"address": "100 Example Lane, Sample City, TX 00000",
"listing_status": "FOR_SALE",
"asking_price": 395000,
"bedrooms": 3,
"bathrooms": 2,
"source_url": "https://www.zillow.com/homedetails/example-property/9000000001_zpid/",
"search_source_url": "https://www.zillow.com/synthetic-test-city/",
"collected_at": "2026-11-04T10:00:00+00:00",
"zestimate": 400000,
"description": "Synthetic property description for parser testing only.",
"detail_collected_at": "2026-11-04T10:01:00+00:00",
"synthetic": true
}Selected columns from the CSV export show how missing values are preserved:
zpid,listing_status,asking_price,bedrooms,bathrooms,zestimate
9000000001,FOR_SALE,395000,3,2,400000
9000000002,FOR_SALE,,0,1,Check identifiers, prices, missing fields, and duplicate counts against the inputs. The second record deliberately preserves a missing price while retaining zero bedrooms.
For authorized live collection, supply a separate manifest containing verified URLs and use --live; the script rejects synthetic inputs in that mode. The included tests can be rerun with:
python -m unittest -vPassing these tests confirms the demonstrated behavior, not current Zillow access. Validate the real page structure and a permitted sample before expanding collection.
Validation note
All 13 automated tests passed on November 4, 2026, and passed again on November 5. The checks used synthetic HTML samples and mocked HTTP responses.
Check | Result |
Sample scope | One search document and one matching detail document. |
Raw entries | Three search entries before deduplication. |
Exported records | Two unique properties after removing one duplicate. |
Rejected records | None in the supplied demonstration sample. |
Incomplete records | One retained record contains missing optional values. |
Missing values | asking_price, zestimate, description, and detail_collected_at are null in that record. |
Export comparison | Regenerated CSV and JSON matched the supplied sample files. |
The missing detail timestamp means no detail document was processed for that property; it does not indicate a failed detail request.
Separate tests checked expected failures, including denied responses, unexpected URLs or content types, missing identifiers, malformed JSON, and changed structures. Live Zillow compatibility, pagination, and connections through a proxy provider were not validated.
Use Yozh Scraper for reusable extraction
Yozh Scraper on GitHub provides a self-hosted service for browser rendering, field extraction, and asynchronous jobs. You can reuse extraction rules across requests while keeping Python responsible for validation and storage.
Yozh Scraper provides browser rendering, extraction rules, and reusable presets for building a custom configuration. This guide does not include a Zillow preset verified against live pages. Validate the required fields against permitted source documents before using a custom preset for recurring property collection whilst using it as a Zillow web scraper.
Start the service and check its health. After cloning the repository, copy .env.example to .env and run docker compose up --build. In another terminal, check http://localhost:8000/api/v1/health. A healthy response confirms service availability, not successful property extraction.
Create and verify a custom preset. Open http://localhost:8000/docs and create a preset using verified CSS or XPath rules. Give it a name beginning with user_, such as user_zillow_property. Test it through POST /api/v1/presets/{name}/test using a permitted sample. Check another property page before reusing the rules.
Run one small job. Submit a single property URL through POST /api/v1/scrape/preset/page using the saved preset’s configuration. Retain the returned job_id; it identifies the background task rather than confirming that extraction succeeded.
Validate the property fields. Map results to the same schema used in the Python example: property identifier, address, listing status, asking price, bedrooms, bathrooms, and source URL. Add the collection timestamp, check numeric types, and preserve missing optional values. Compare the output with the source before increasing volume.
Retrieve results from Python. Poll GET /api/v1/scrape/{job_id} with a delay and overall timeout, handling failed or canceled jobs. After completion, request GET /api/v1/scrape/{job_id}/results, inspect errors and warnings, then validate the records before exporting them.
For broader installation instructions and the optional visual tester, follow the screen scraping guide.
Handle pagination and incomplete search results
One Zillow search should be treated as a defined sample, not a complete regional inventory. Geographic boundaries, listing filters, pagination, and collection failures can all affect which records appear, even when the search displays a larger total.
Before expanding collection, define what one record represents: a property, an individual unit, a building, or a listing. An apartment building and its available units should not be merged simply because they share a street address.
The Python example above reads one search page and does not implement pagination. To extend it, inspect the permitted search interface and verify how the next page is requested. Preserve the original geographic bounds, filters, and sorting settings throughout the run, recording them alongside the dataset.
Stop when a page repeats, returns no new identifiers, or reaches your configured page, record, or runtime limit. Record the stopping reason so a deliberately limited sample is distinguishable from a failed collection.
Deduplicate overlapping results using ZPID where it identifies the intended entity, or another verified property, unit, or listing identifier. Avoid relying on the address alone.
For each run, record:
Raw records: entries returned before deduplication and validation.
Unique records: retained entries after duplicate removal.
Rejected records: entries excluded, with reasons.
Incomplete records: retained entries missing optional fields; these also count as unique.
Report the coverage demonstrated by the tested method. The reviewed guides describe different search limits and approaches, so avoid presenting one fixed number as a universal Zillow result cap.
Clean Zillow data and export CSV or JSON
Extracted records become useful when their meaning survives sorting, filtering, and comparison. Before exporting, normalize each field consistently and check a few records against the source.
Preserve ZIP codes as strings. Leading zeros matter, so import the column as text when opening a CSV in Excel.
Separate price components. Store the amount, currency, price type, and rent period independently. An asking price, last sale price, and Zestimate belong in separate fields or clearly labeled records.
Retain area units. Save the measurement alongside its unit, such as square feet, so later calculations use comparable values.
Keep missing values distinct from zero. Use JSON null or an empty CSV cell for unavailable information. Zero bedrooms can describe a studio; it should not automatically become a missing value.
Preserve provenance. Include the source URL and collection timestamp so each observation can be traced and checked.
CSV works well for flat property records. Keep nested price or listing histories in JSON, or export them to a separate table linked by property identifier.
For monitoring, retain dated observations instead of overwriting previous values. A listing disappearing from a later run does not prove the property sold: changed filters, incomplete collection, or listing removal may explain its absence.
Sample JSON record from the supplied test files
This is the second record generated by the Python example. Its property identifier, address, URLs, and observation timestamp are fictional. The missing asking price remains null, while zero bedrooms remains numeric zero.
{
"zpid": "9000000002",
"address": "200 Example Lane, Sample City, TX 00000",
"listing_status": "FOR_SALE",
"asking_price": null,
"bedrooms": 0,
"bathrooms": 1,
"source_url": "https://www.zillow.com/homedetails/second-example/9000000002_zpid/",
"search_source_url": "https://www.zillow.com/synthetic-test-city/",
"collected_at": "2026-11-04T10:00:00+00:00",
"zestimate": null,
"description": null,
"detail_collected_at": null,
"synthetic": true
}The checks below also cover fields you may add later; the supplied script uses the narrower sale-only schema defined earlier.
Choose proxies and session settings for the job
A proxy controls the network route and exit IP used by your scraper. Choose the connection around traffic volume, location, and session continuity; no proxy type guarantees Zillow access.
Connection | Workload fit | Pricing |
Rotating residential | Location targeting, with sticky sessions up to 24 hours for connected steps, best for web scraping, AI data collection, automation, SEO and market research. | Standard pool: $2/GB with free geotargeting |
Mobile | Clean IPs with Unlimited traffic helps control bandwidth costs when rendering downloads, scripts and images repeatedly, best for web scraping, automation, SEO and market research. | LTE/5G $1.7/day with HTTP, SOCKS5 and UDP protocol support. |
A fixed address suits repeated observations that need consistent routing, with unlimited bandwidth, best for web scraping, QA testing, automation and region-specific access. | $5.29/month with 99.9% uptime. | |
High-speed dedicated IPv4/IPv6 datacenter proxies with unlimited bandwidth; best for web scraping, automation, monitoring, API integration and large-scale data collection. | $2.18/month with HTTP and SOCKS5 protocol support. |
For a browser-based collector, one documented configuration is Ubuntu 24.04, Playwright Chromium, and an HTTP proxy with username/password authentication. Set server, username, and password through browser.new_context(proxy=...), loading credentials from environment variables. Playwright documents HTTP proxy credentials separately from SOCKSv5 support, so do not assume authenticated SOCKS5 works identically.
Open an IP-check page inside that same browser context and verify the exit IP and location. Then test representative, permitted extraction and inspect the returned records before increasing the workload.
Get CyberYozh App IP address check with real assessment, using leading anti-fraud systems.
How to scrape Zillow with phone numbers
Contact-field availability depends on the listing, source, and extraction method. Before adding phone numbers to a Zillow scraper, verify that the permitted source exposes the contact field and identifies whom it belongs to.
Agent and brokerage contacts are different from homeowner details. A number displayed beside a listing should not automatically become owner_phone in your dataset. Inspect its accompanying name and label, preserving the stated relationship rather than guessing who answers.
For each permitted professional contact, retain:
Phone number: stored as text, including any displayed extension.
Name and role: exactly as identified by the source.
Source URL: the page containing that contact information.
Collection timestamp: when the information was observed.
Keep multiple contacts separate so an agent’s number does not become associated with the brokerage’s name. Missing numbers should remain null in JSON or blank in CSV; record extraction errors separately instead of filling gaps with guessed details.
Public display does not establish permission for every collection, reuse, or outreach purpose. Review the source’s access conditions and applicable privacy requirements before collecting or using professional contacts.
Rent a virtual mobile number or a residential number compatible with popular online services.
Fix common Zillow scraping problems
Identify the failed stage before changing the setup. Connection problems involve routing, authentication, or transport; access restrictions deny or limit requests; extraction errors occur when the collector cannot correctly interpret the returned content.
Symptom | Likely cause | Next check |
403 response or challenge page | Access refusal or automated-traffic check | Inspect the response body and identify which service returned it. Pause repeated attempts and verify the permitted access method. |
Request rate limit | Honor Retry-After when present. Reduce request frequency and concurrency, use increasing retry delays, and cap attempts. | |
HTTP 200, no records | Unexpected content or extraction mismatch | Check the final URL, content type, and saved document. Confirm the expected fields exist before changing selectors. |
Incomplete listing coverage | Filters, pagination, collection limits, or failed requests | Compare geographic bounds and filters; review page counts, rejected records, and the stopping reason. |
Duplicate properties | Overlapping searches or repeated pages | Deduplicate using a verified identifier while preserving distinct units and dated observations. |
Missing phone numbers | Field unavailable or incorrectly mapped | Inspect the source and contact label. Preserve missing values instead of guessing. |
Incorrect or missing prices | Wrong price field or conversion error | Verify asking price, rent, sale price, and Zestimate separately. Check currency and rent period. |
Parser stopped after a page change | Changed HTML or embedded JSON structure | Compare saved samples, update affected selectors or paths, and validate a small sample again. |
For connection failures, verify the endpoint, authentication, and effective routing in the requesting client. After any correction, compare extracted values with the source before restarting a larger run.
Build a dataset you can verify
Select a permitted source, validate a small sample, and preserve each field’s meaning when exporting. Expand only after comparing the output with the source. Test your verified extraction rules in Yozh Scraper before setting up recurring collection.
If collection requires a specific exit location or consistent IP, choose a suitable connection from the proxy catalog and test it with your verified extraction setup.
