Calculate Estimated Gross Rental Yield for a Property Shortlist
A Python gross rental yield calculator for a property shortlist that labels every input: your assumed price and rent, or the source's asking price and rent Zestimate, with expenses excluded.
What gross rental yield measures
Gross rental yield is a year of rent as a percentage of the purchase price. It is a quick screening number for a shortlist: it tells you which homes deserve a closer look, not which ones are good investments. It is gross because it ignores every cost of owning the home, including vacancy, property tax, insurance, maintenance, management, HOA fees and financing.
Because it ignores costs, two homes with the same gross yield can produce very different results for an owner. A condo with a large HOA fee and a house with high property tax can look identical at this stage. Use the figure to sort the list, then do the cost work on the homes that survive.
gross rental yield (%) = monthly rent x 12 / purchase price x 100
# Arithmetic check, not a market observation:
# rent 2,000 a month, price 400,000 -> 24,000 / 400,000 x 100 = 6.00%Label every input as an assumption or a source value
A yield is only as honest as its two inputs, so the calculator records where each one came from. For the purchase price, your own assumption wins. Otherwise, for a home listed for sale, it uses the asking price, labelled as such. An asking price is not what the home will sell for, and a home that is not for sale has no purchase price to use.
For the rent, your own assumption also wins. Otherwise it uses the rent Zestimate, labelled as a source estimate: Zillow's monthly rent estimate, shown only when Zillow shows one. It is not an achieved rent and not a lease offer. When neither input is available, the row says which assumption to add instead of producing a number.
| Input | First choice | Fallback | If both are missing |
|---|---|---|---|
| Purchase price | Your assumed price | Asking price, for a home listed for sale | No yield; add an assumption. |
| Monthly rent | Your assumed rent | Rent Zestimate, labelled as an estimate | No yield; add an assumption. |
Prepare the shortlist
List the homes in shortlist.csv with optional assumption columns. Leave a cell blank to use the source value. Send the url column, in the same order, to the batch endpoint and save the response; the calculator reads that saved file and never calls the API, so changing an assumption costs nothing.
The example below lists one home twice: once with source values and once with your own price and rent. Comparing the two rows shows how much the answer depends on the inputs.
id,url,assumed_price,assumed_monthly_rent
a,https://www.zillow.com/homedetails/<home-a>_zpid/,,
b,https://www.zillow.com/homedetails/<home-a>_zpid/,500000,2400
c,https://www.zillow.com/homedetails/<home-c>_zpid/,,
d,<an address that does not match>,,
e,https://www.zillow.com/homedetails/<rental-e>_zpid/,,The calculator
The script uses Decimal arithmetic and rounds the percentage to two places. It writes one row per shortlist entry, including failed lookups, and keeps the price and rent bases next to the result.
"""Estimated gross rental yield for a shortlist, with every input labelled.
Usage: python gross_yield.py shortlist.csv response.json > yield.csv
shortlist.csv: id,url,assumed_price,assumed_monthly_rent (blank = use the source field),
in the same order as the items sent to POST /v1/zillow/batch.
Gross yield = monthly rent x 12 / purchase price x 100. It ignores every expense.
"""
import csv
import json
import sys
from decimal import Decimal, InvalidOperation
SALE_LISTED = {"FOR_SALE", "PENDING", "COMING_SOON", "FORECLOSURE"}
def number(text):
try:
value = Decimal(text.strip().replace(",", ""))
except (InvalidOperation, AttributeError):
return None
return value if value > 0 else None
def choose_price(row, data):
if number(row["assumed_price"]):
return number(row["assumed_price"]), "your assumed price"
if data.get("homeStatus") in SALE_LISTED and data.get("price"):
return Decimal(str(data["price"])), "asking price (source)"
return None, "no purchase price: add an assumption"
def choose_rent(row, data):
if number(row["assumed_monthly_rent"]):
return number(row["assumed_monthly_rent"]), "your assumed rent"
if data.get("rentZestimate"):
return Decimal(str(data["rentZestimate"])), "rent Zestimate (source estimate)"
return None, "no rent: add an assumption"
def gross_yield(monthly_rent, price):
return (monthly_rent * 12 / price * 100).quantize(Decimal("0.01"))
def main(shortlist_path, response_path):
with open(shortlist_path, newline="", encoding="utf-8") as f:
shortlist = list(csv.DictReader(f))
results = {r["index"]: r for r in json.load(open(response_path, encoding="utf-8"))["data"]}
out = csv.writer(sys.stdout)
out.writerow(["id", "address", "price_used", "price_basis", "monthly_rent_used",
"rent_basis", "gross_yield_pct", "source_gross_yield_pct",
"source_basis", "fetched_at"])
for i, row in enumerate(shortlist):
result = results.get(i, {})
if not result.get("ok"):
out.writerow([row["id"], "", "", "lookup failed", "", "", "", "", "", ""])
continue
data, meta = result["data"], result["meta"]
price, price_basis = choose_price(row, data)
rent, rent_basis = choose_rent(row, data)
calc = data.get("calculated") or {}
out.writerow([row["id"], data.get("address"), price, price_basis, rent, rent_basis,
gross_yield(rent, price) if price and rent else "",
calc.get("gross_yield_pct"), calc.get("rent_to_price_basis"),
meta.get("fetched_at")])
if __name__ == "__main__":
main(*sys.argv[1:3])Compare with the API's own calculated figure
Every property response also includes data.calculated.gross_yield_pct, calculated by PropertiesAPI from the returned fields; it is not a Zillow estimate. It uses the rent Zestimate for a year divided by the price, or by the Zestimate when there is no price, and always for a rental, whose price is a monthly rent. rent_to_price_basis says which denominator was used.
The calculator shows that figure in its own column so you can see when your assumptions move the answer. In the sample output, the first row matches the API figure because both use the asking price and the rent Zestimate. The second row differs because it uses your inputs. The rental row has an API figure based on the Zestimate but no yield of its own, because no purchase price was assumed.
id,address,price_used,price_basis,monthly_rent_used,rent_basis,gross_yield_pct,source_gross_yield_pct,source_basis,fetched_at
a,"10 Example Ave, Austin, TX 78701",525000,asking price (source),2650,rent Zestimate (source estimate),6.06,6.06,price,2026-10-20T14:00:00.000Z
b,"10 Example Ave, Austin, TX 78701",500000,your assumed price,2400,your assumed rent,5.76,6.06,price,2026-10-20T14:00:00.000Z
c,"8 Null Ln, Austin, TX 78704",525000,asking price (source),,no rent: add an assumption,,,,2026-10-20T14:00:00.000Z
d,,,lookup failed,,,,,,
e,"22 Sample St APT 4, Austin, TX 78702",,no purchase price: add an assumption,2500,rent Zestimate (source estimate),,7.32,zestimate,2026-10-20T14:00:00.000ZWatch units, buildings and duplicate rows
Yield depends on matching the rent to the same space as the price. For condos and apartments, include the unit in the address or use the unit's own link, and check the unit field in the response. An apartment-building link describes one available unit, its lowest-priced one available now, and its price is that unit's monthly rent, so it has no purchase price to divide by.
If you list the same home more than once with different assumptions, as in the example, keep the id column distinct and sort by it. The batch counts each row as a separate item, but repeats of the same home within 24 hours come from our cache and are free, so a scenario row does not cost another lookup when it repeats a home you just collected.
Use the result as a screen, not a forecast
Present the number as estimated gross yield, with its bases visible. Do not describe it as a return, a cash flow or an expected income, and do not compare it with net figures from elsewhere. A projected yield based on an estimate is not a result anyone has achieved.
For the homes that pass the screen, the next step is a cost-aware calculation with your own assumptions for vacancy, tax, insurance, maintenance and financing. The rent-vs-buy worksheet guide shows how to keep source fields and assumptions separate in that kind of calculation.
- Check that each returned home, including its unit, is the one you meant.
- Check fetched_at: an older collection may not reflect the current asking price.
- Compare like with like: a studio and a four-bedroom house are not interchangeable rentals.
- Keep excluded rows visible so the shortlist does not quietly shrink.
What was checked
Validation scope: the calculator was run against synthetic batch responses that cover a for-sale home, user assumptions, a missing rent estimate, a rental and a failed lookup, and its yield function was checked against hand arithmetic. No live properties were collected for this article, and no claim about typical yields in any market follows from it.
Sources and documentation
Product details checked against the linked documentation on October 22, 2026. Code and sample inputs are illustrative unless stated otherwise.
- PropertiesAPI property fields and calculated fields
- PropertiesAPI batch endpoint
- Decimal arithmetic in Python
Written by PropertiesAPI, an independent property data service. We are not affiliated with Zillow. Source data may be incomplete or change over time.