Zillow Scraper with Python: A Guide to Public Listing Data
TL;DR
A Zillow scraper turns listing information into records, but page visibility does not grant permission to collect or republish it.
This Python walkthrough parses a local, synthetic HTML fixture; it sends no requests to Zillow.
Keep asking price, sale price, and Zestimate in separate fields and retain the source timestamp.
Use approved Zillow APIs, MLS/IDX agreements, licensed feeds, or public records when their terms cover the intended use.
Treat 403 responses, missing fields, and layout changes as reasons to review access and data quality, not to evade controls.
What Is a Zillow Scraper and What Can It Do?
A Zillow scraper is a program that reads real-estate listing information and converts it into records for research. Developers may want to compare asking prices, bedroom counts, property types, or listing status across a market. For a production project, confirm that the source and intended use are authorized. Zillow's API terms describe use by approved API consumers, and its consumer terms govern use of the service.
This article demonstrates parsing a local sample page so you can test Python logic without making automated Zillow access the tutorial's premise. For another real-estate source that permits page retrieval, Nstdata Crawl can return page content for downstream parsing; it does not grant rights to a target site's data. See the Crawl documentation and automated data collection guide.
Experience Nstproxy Crawl - Start Your Free Trial Today
What Zillow Listing Data Can You Collect?
A listing record can contain these fields when visible in a permitted source and allowed by its license:
Field
Meaning
Common mistake
Property address
Displayed address or approximate location
Treating a hidden address as parser failure
Listing price
Current advertised asking price
Calling it a sale price
Bedrooms and bathrooms
Advertised room counts
Mapping studio to zero bedrooms without documenting it
Square footage
Advertised area
Comparing before normalizing units and definitions
Property type
House, condo, townhouse, rental, and similar labels
Assuming labels map identically across sources
Listing status
For sale, pending, sold, or rental status
Assuming the status means the page was checked recently
Listing URL
Source evidence and re-check location
Dropping parameters without checking listing identity
Observed time
When your pipeline saw the values
Reusing the page's updated label as your collection time
A Zestimate is a model-generated valuation, not the asking price or a confirmed transaction. A historical sale price is another fact with its own date and source. Keep separate columns for each concept and include a price type field rather than combining everything into one generic price.
How Zillow Listing Pages Are Structured
Search results, listing detail pages, maps, and account features can be generated differently. A browser may display content loaded after JavaScript runs, while saved initial HTML contains only a shell. A map marker can represent approximate coordinates rather than a public street address.
When inspecting an authorized sample, look for stable evidence: semantic labels, data attributes, structured JSON-LD, or a documented API response. Avoid relying on generated class names that change during front-end builds. Keep the URL, page title, retrieval time, and a short content hash with each fixture so parser behavior is reproducible.
Can You Scrape Zillow with Python?
Python can parse HTML, make HTTP requests, and drive a browser, but those capabilities do not establish that a particular source permits automated collection. Zillow's developer site lists APIs and data products, and access is subject to approval and product terms. Do not treat a browser-visible listing or a working request as programmatic authorization.
Use this decision order:
Check whether an official API, MLS/IDX agreement, public-record dataset, or licensed vendor covers your fields and reuse rights.
If an authorized partner provides a feed, use its schema and refresh terms.
If a page source is explicitly approved for collection, test against a small representative fixture and document the permitted scope.
If the site rejects access or terms do not cover the use, stop and select another source.
A Python request returning 403 is a server decision. Do not disguise the request or evade an access control. The Zillow Data & APIs terms are the starting point for approved API use; public-record datasets have separate licenses and constraints.
Prerequisites: Python and a Test Data Source
The example uses Python's standard library, so no scraping package is required. Save a listing fragment supplied by a licensed partner or created specifically as a fixture. This snippet contains no live Zillow content:
<articleclass="listing"><spandata-field="address">42 Sample Street, Example City</span><spandata-field="price">$525,000</span><spandata-field="beds">3</span><spandata-field="baths">2.5</span><spandata-field="sqft">1,840</span><adata-field="url"href="/listings/42">View listing</a></article>
The parser is designed for these explicit data-field attributes. It is not a universal Zillow selector and should not be copied against a live Zillow page.
Step 1: Inspect a Real Estate Listing Page
Use an approved sample in a local browser or text editor. Identify which values are present and whether each comes from visible text, an attribute, or structured data. Decide how missing values will be represented before writing parsing code. A missing price should produce an explicit null and a diagnostic; it should not silently reuse yesterday's value.
Step 2: Extract Listing Fields with Python
The following function reads one local HTML snippet, selects the known fixture fields, and normalizes price and square footage. It keeps raw strings so downstream rules can be reviewed:
from html.parser import HTMLParser
from decimal import Decimal
import re
classListingParser(HTMLParser):def__init__(self):super().__init__() self.fields ={} self.active =Nonedefhandle_starttag(self, tag, attrs): attrs =dict(attrs) field = attrs.get("data-field")if field: self.active = field
if tag =="a": self.fields[field]= attrs.get("href","")defhandle_data(self, data):if self.active and data.strip(): self.fields.setdefault(self.active,"") self.fields[self.active]+= data.strip()defhandle_endtag(self, tag):if self.active: self.active =Nonedefparse_price(raw):match= re.search(r"\d[\d,]*(?:\.\d{1,2})?", raw or"")return Decimal(match.group().replace(",",""))ifmatchelseNonedefparse_integer(raw):match= re.search(r"\d[\d,]*", raw or"")returnint(match.group().replace(",",""))ifmatchelseNonehtml ="""<article>
<span data-field="address">42 Sample Street, Example City</span>
<span data-field="price">$525,000</span>
<span data-field="beds">3</span>
<span data-field="baths">2.5</span>
<span data-field="sqft">1,840</span>
<a data-field="url" href="/listings/42">View listing</a>
</article>"""parser = ListingParser()parser.feed(html)listing ={"address_raw": parser.fields.get("address"),"listing_price_raw": parser.fields.get("price"),"listing_price":str(parse_price(parser.fields.get("price"))or""),"bedrooms": parse_integer(parser.fields.get("beds")),"bathrooms_raw": parser.fields.get("baths"),"square_feet": parse_integer(parser.fields.get("sqft")),"source_url": parser.fields.get("url"),}assert listing["listing_price"]=="525000"assert listing["square_feet"]==1840print(listing)
The sample verifies parsing against the local fixture. A partner feed may use different field names, nested JSON, locale-specific currency, or explicit nulls. Add source-specific tests before accepting a record.
Parse Prices, Addresses, and Property Features
Use Decimal or store a normalized price as a decimal string instead of a binary float. Keep the formatted original because “$525,000” does not mean the same thing as “525000 EUR,” and the page may display monthly rent rather than sale price. Store currency, frequency, source label, and observation time.
Preserve the address exactly as displayed, then normalize geocoding or unit information separately using an authorized reference. Do not infer a precise address from an approximate marker.
Handle Missing and Inconsistent Listing Data
A missing price should remain null. Do not fill a hidden address from another page unless the match is verified. If square footage is not shown, leave it blank rather than estimating it from bed count. Keep a parse state such as complete, partial, or failed, with a reason.
Step 3: Save Property Listings to CSV
A useful export includes stable identity and provenance fields: property ID, source ID, source URL, listing price, currency, bedrooms, square feet, status, observed time, and parse state. Use a source's stable listing identifier when its agreement and response include one. A URL hash can help with duplicate detection but is not a reliable property identity when relisting changes the URL.
Step 4: Handle Pagination and Duplicate Properties
Search pages may return the same property under different filters, localities, or sort orders. De-duplicate first by source ID. If none exists, use a conservative match key that combines normalized address, unit, source, and listing status; a fuzzy title alone can merge different units in one building.
Pagination needs a checkpoint. Save the cursor or page token after each successful batch, record query filters, and make jobs idempotent so retries do not create duplicate rows. Keep collection bounded to permitted URLs and a reasonable page limit.
Zillow Data Alternatives: APIs, Licensed Datasets, and Public Records
A property-data API or licensed MLS/IDX feed can provide a contractual path to listing fields. Public records can support parcel and assessment research, but often lag market listings and use different identifiers. Zillow Group also publishes public data products; check their license and eligible uses before integrating them. Its public data catalog describes the current portfolio, while the American Community Survey provides aggregate housing context rather than individual active listings.
Choose by update frequency, fields, geographic coverage, and redistribution rights. For a price-trend study, an authorized recurring feed may be more useful than attempting to reconstruct missing observations from consumer pages.
Practical Use Cases for Zillow Listing Data
Permitted listing data can help compare asking-price bands by area, observe inventory counts, or study how bedroom mix changes over time. It does not establish market value, transaction volume, or housing affordability by itself. Listings are a selected population: not every home is for sale, and stale or duplicated pages change the apparent supply.
Use transaction records and statistical housing datasets to validate conclusions about realized sales. Label asking-price trends as asking-price trends.
How to Build a Real Estate Price Monitoring Pipeline
The pipeline is authorized source → bounded retrieval → identity resolution → field validation → historical snapshot → change detection → analyst review. Store each observation rather than overwriting the previous value. Before alerting on a price movement, confirm that listing identity and price type are unchanged.
A schema-first approach helps when records arrive from several providers. The website-to-JSON guide explains source provenance and field validation; the URL-to-Markdown API guide covers conversion when permitted page retrieval is part of the workflow.
Cost and Maintenance Considerations
A local parser has a low runtime cost but an ongoing maintenance burden: source layout changes, license audits, parser tests, and update scheduling. A licensed feed or API has a contract price but may reduce source adapters you own. A managed crawl service adds request-based collection and may bill proxy bandwidth separately; check Nstdata Crawl pricing. Cost per listing that passes identity and field checks is more informative than cost per fetched page.
Legal and Responsible Use of Zillow Data
Follow the terms attached to the specific page, API, feed, or dataset. Listing photos, broker remarks, and property facts may have separate rights. Do not republish source content or build a commercial database without confirming the license. Avoid personal data that is unnecessary for the research question, and set retention and deletion rules.
Use Nstdata Crawl with Authorized Real Estate Sources
Nstdata Crawl can retrieve and render public listing pages when the source’s terms or your data agreement permit automated collection. For Zillow data, use an approved API or an authorized MLS/IDX feed; Zillow’s Data & APIs terms describe access for approved licensees. Nstdata Crawl provides a collection API; it does not grant permission to collect Zillow data.
For an approved source with listing detail pages, the Python SDK can fetch a page as Markdown. Replace the example URL with a page covered by your source agreement. This code demonstrates the documented SDK flow; it does not send a request to Zillow.
import os
from datetime import datetime, timezone
from nstdata_ai_crawl import( Format, NstDataClient, ScrapeRequestDto,)listing_url ="https://licensed-provider.example/listings/123"with NstDataClient(os.environ["NSTDATA_API_TOKEN"])as client: result = client.submit_scrape_task_sync( ScrapeRequestDto( url=listing_url, formats=[Format.MARKDOWN], timeout=60_000, onlyMainContent=True,))markdown = result.data.get_markdown()ifnot markdown ornot markdown.strip():raise RuntimeError(f"No page content returned for {listing_url}")snapshot ={"source_url": listing_url,"observed_at": datetime.now(timezone.utc).isoformat(),"page_markdown": markdown,}print(snapshot["observed_at"],len(snapshot["page_markdown"]))
The Nstdata Crawl documentation also describes bounded site crawls for discovering linked pages. Set a maximum depth and page count, then review the task results before adding records to your dataset.
Treat Markdown as the captured source text, not as a normalized property record. Write a parser for the specific provider’s page structure, preserve the original listing URL and observation time, and leave missing values empty. Keep asking price, estimated value, and historical sale price in separate fields; they represent different data.
Conclusion: Test the Data Contract Before Scaling
Python can parse real-estate listing data, but choose an authorized source before automating collection. This fixture tests parsing and normalization without pretending to be a live Zillow extraction. For production, secure the feed or API agreement first, preserve raw evidence, and validate identity and price meaning at every stage.
Python can parse a local or authorized listing dataset. Whether automated access to a particular Zillow page is permitted depends on applicable terms and agreements; this example does not request Zillow pages.
Q: Does Zillow have an official API?
Zillow Group publishes APIs and data offerings, but access and allowed use depend on the specific product and approval terms. Check its current developer portal and agreement.
Q: What data can a listing parser extract?
It can extract fields present in an authorized source, such as displayed address, asking price, room counts, property type, status, URL, and update time. Missing values should remain explicit, not guessed.
Q: Why might a request return 403?
The source may deny the request or require a permitted access path. Review the terms and choose an authorized source; do not evade the restriction.
Q: What are alternatives to Zillow page scraping?
Consider approved real-estate APIs, licensed MLS/IDX feeds, county public records, government housing statistics, or a provider whose contract covers your research and redistribution.
Marcus Chen
Oct. 21st 2026
Crawl entire websites with a single API request
99.8% success rate with JavaScript rendering
Get clean, LLM-ready data in multiple formats
Turn any website into Markdown, HTML, JSON, links, PDFs and more — without managing crawling infrastructure.