Build a project

Build a Rent-vs-Buy Worksheet with Property Data and Your Own Assumptions

A rent-vs-buy worksheet that keeps listing data, such as asking price, tax and HOA fee, apart from the financing, insurance and maintenance assumptions only you can supply.

7 min read

A rent-vs-buy sheet has two kinds of input

Every rent-vs-buy comparison mixes facts about a property with assumptions about your situation. The asking price, the last annual property tax and the HOA fee describe the home as listed. The interest rate, the down payment, insurance, maintenance and how long you will stay describe you. Spreadsheets go wrong when those two kinds of input sit in the same column and nobody can tell which numbers came from where.

This worksheet keeps them apart. Property fields come from one saved API response and are labelled source. Everything else lives in an ASSUMPTIONS block you edit, labelled assumption. The output shows a basis next to every line, so a reader can see at a glance which numbers would change if your circumstances did.

What the property data can supply

A single property lookup returns several fields that are useful here, each with a caveat. Use them as starting values and confirm them on the listing and with the professionals involved in a purchase.

FieldUse in the worksheetCaveat
price with a for-sale statusStarting purchase priceAn asking price, not a sale price.
taxAnnualAmountProperty tax per monthThe last tax shown; it can change after a sale.
monthlyHoaFeeHOA per monthMissing when not shown; check the listing.
rentZestimateStarting rent alternativeZillow's estimate, not an offer to lease.
zestimateContext onlyAn estimate; not used as the price here.

What only you can supply

The financing and running-cost assumptions depend on your credit, your lender, your insurer and the condition of the home. The values in the script are placeholders that show the shape of the calculation. Replace every one of them; a worksheet that runs on someone else's assumptions answers someone else's question.

The Consumer Financial Protection Bureau's homebuying resources explain loan estimates, interest rates and closing costs, and are a good place to check the financing inputs before you rely on them.

  • Purchase price, if you expect to pay something other than the asking price.
  • Down payment share, interest rate and loan term.
  • Insurance per year, from a quote rather than a rule of thumb.
  • Maintenance per year as a share of the price, adjusted for the age and condition of the home.
  • The monthly rent of the home you would otherwise rent, which may not be this one.

The worksheet script

The loan payment uses the standard fixed-rate amortization formula. The script also reports how much principal the first year of payments repays, because that part of the payment becomes equity rather than cost, and a sheet that hides it overstates the cost of owning. Money is handled with Decimal and rounded to cents.

If a line has no value, such as an HOA fee the listing does not show, the monthly total says it is incomplete and names the missing line rather than treating it as zero.

rent_vs_buy.py
"""Monthly rent-vs-buy worksheet for one property. Source fields and your assumptions
stay in separate columns. Usage: python rent_vs_buy.py property.json
property.json is a saved GET /v1/zillow/property response. Edit ASSUMPTIONS first.
"""
import json
import sys
from decimal import Decimal, ROUND_HALF_UP

ASSUMPTIONS = {  # all yours: replace every value before using the output
    "purchase_price": None,          # None = use the asking price, if the home is for sale
    "down_payment_share": Decimal("0.20"),
    "annual_interest_rate": Decimal("0.065"),
    "loan_years": 30,
    "insurance_per_year": Decimal("2400"),
    "maintenance_share_per_year": Decimal("0.01"),   # of the purchase price
    "monthly_rent_alternative": None,  # None = use the rent Zestimate, labelled as an estimate
}
CENT = Decimal("0.01")


def monthly_payment(principal, annual_rate, years):
    n, r = years * 12, annual_rate / 12
    if r == 0:
        return (principal / n).quantize(CENT, ROUND_HALF_UP)
    return (principal * r / (1 - (1 + r) ** -n)).quantize(CENT, ROUND_HALF_UP)


def first_year_principal(principal, annual_rate, payment):
    balance, r = principal, annual_rate / 12
    for _ in range(12):
        balance -= payment - balance * r
    return (principal - balance).quantize(CENT, ROUND_HALF_UP)


def source(data, field):
    value = data.get(field)
    return Decimal(str(value)) if value is not None else None


def worksheet(data, a):
    rows, price = [], a["purchase_price"]
    if price is None and data.get("homeStatus") in {"FOR_SALE", "PENDING", "COMING_SOON"}:
        price, price_label = source(data, "price"), "source: asking price"
    else:
        price_label = "assumption"
    if not price:
        raise SystemExit("No purchase price: set ASSUMPTIONS['purchase_price']")
    loan = price * (1 - a["down_payment_share"])
    payment = monthly_payment(loan, a["annual_interest_rate"], a["loan_years"])
    tax = source(data, "taxAnnualAmount")
    hoa = source(data, "monthlyHoaFee")
    rent = a["monthly_rent_alternative"]
    rent_label = "assumption"
    if rent is None and data.get("rentZestimate") is not None:
        rent, rent_label = source(data, "rentZestimate"), "source: rent Zestimate (estimate)"
    rows.append(("Purchase price", price, price_label))
    rows.append(("Loan payment (principal and interest)", payment, "calculated from assumptions"))
    rows.append(("Property tax per month", (tax / 12).quantize(CENT) if tax else None,
                 "source: last annual tax" if tax else "missing: add an assumption"))
    rows.append(("HOA per month", hoa, "source" if hoa is not None else "not shown: check the listing"))
    rows.append(("Insurance per month", (a["insurance_per_year"] / 12).quantize(CENT), "assumption"))
    rows.append(("Maintenance per month", (price * a["maintenance_share_per_year"] / 12).quantize(CENT), "assumption"))
    missing = [label for label, value, _ in rows[1:] if value is None]
    own = sum(value for _, value, _ in rows[1:] if value is not None)
    rows.append(("Monthly cost to own", own, "incomplete: missing " + ", ".join(missing)
                 if missing else "sum of the five rows above"))
    rows.append(("Principal repaid in year one", first_year_principal(loan, a["annual_interest_rate"], payment), "calculated: part of the payment that becomes equity"))
    rows.append(("Cash at closing (down payment only)", (price * a["down_payment_share"]).quantize(CENT), "assumption; add closing costs"))
    rows.append(("Monthly rent alternative", rent, rent_label if rent is not None else "missing: add an assumption"))
    return rows


if __name__ == "__main__":
    body = json.load(open(sys.argv[1], encoding="utf-8"))
    print(f"{body['data'].get('address')}  collected {body['meta'].get('fetched_at')}")
    for label, value, basis in worksheet(body["data"], ASSUMPTIONS):
        shown = f"{value:,.2f}" if value is not None else "missing"
        print(f"{label:<40} {shown:>14}  {basis}")

Save one property response and run the sheet

Look the home up once and save the response. The worksheet reads the file, so you can change assumptions and rerun it as often as you like without another lookup. The output below comes from the documentation's illustrative example record, not a real home.

Collect and run
curl -sG "https://api.propertiesapi.com/v1/zillow/property" \
  -H "x-api-key: $PROPERTIESAPI_KEY" \
  --data-urlencode "url=https://www.zillow.com/homedetails/<the-home>_zpid/" \
  -o property.json --max-time 130

python rent_vs_buy.py property.json

# Output with the placeholder assumptions (illustrative record):
10 Example Ave, Austin, TX 78701  collected 2026-10-20T14:00:00.000Z
Purchase price                               525,000.00  source: asking price
Loan payment (principal and interest)          2,654.69  calculated from assumptions
Property tax per month                           820.00  source: last annual tax
HOA per month                                   missing  not shown: check the listing
Insurance per month                              200.00  assumption
Maintenance per month                            437.50  assumption
Monthly cost to own                            4,112.19  incomplete: missing HOA per month
Principal repaid in year one                   4,694.50  calculated: part of the payment that becomes equity
Cash at closing (down payment only)          105,000.00  assumption; add closing costs
Monthly rent alternative                       2,650.00  source: rent Zestimate (estimate)

Read the result without turning it into a verdict

The worksheet prints the monthly cost to own, the part of it that is principal, the cash needed at closing and the rent alternative side by side. It does not print a recommendation, and it should not. Whether to rent or buy depends on how long you will stay, what else you could do with the down payment, how prices and rents move, tax treatment and personal circumstances that no property record contains.

In particular, do not infer a decision from the Zestimate alone. A home listed below its Zestimate is not automatically a good purchase, and one listed above it is not automatically a bad one. The estimate is context for your research, not an input to the monthly cost and not a prediction of resale value.

  • Add closing costs and moving costs to the cash line.
  • Run the sheet for more than one rate and down payment to see how sensitive the result is.
  • Check the collection time of the saved response before you rely on the asking price.
  • Revisit the tax line with the local assessor's rules if a sale triggers reassessment.

What was checked

Validation scope: the payment function was checked against hand calculations, including a zero-rate loan, and the worksheet was run on the documentation's illustrative record and on a record with missing estimates and fees. No live properties were collected for this article. Nothing here is financial advice.

Sources and documentation

Product details checked against the linked documentation on October 22, 2026. Code and sample inputs are illustrative unless stated otherwise.

Written by PropertiesAPI, an independent property data service. We are not affiliated with Zillow. Source data may be incomplete or change over time.