Win 1 TB Residential Traffic, iPhone 18 Pro Max + 33 More Prizes$50 Top-Up = 1 TicketJoin the GiveAway
Enterprise

Zillow scraper: how to scrape property data with Python

Mo

November 06, 2026

Scraping & Automation

Zillow scraper: how to scrape property data with Python

Summarize this article with AI:

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.

Yozh Scraper

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:

python
python3 -m venv .venv

source .venv/bin/activate

python -m pip install -r requirements.txt

The 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:

html
<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.gdpClientCache

These 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:

python
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.text

The 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:

python
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 props

Extract 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:

python
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 value

Include 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:

shell
python zillow_sample.py sample.json --output run-01

The output directory must be new. The tested sample produces:

Saved 2 unique listings to run-01

One resulting record looks like this. All property information below is fictional:

json
{

  "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:

shell
python -m unittest -v

Passing 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

json
{

  "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.   

Static ISP

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. 

Datacenter

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.

429 response

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.

Helpful?

Share article

Zillow Scraper FAQs