How to get property data by address in Python

Photo of Alex Wilkinson
Alex Wilkinson
CEO of Houski
2026-11-13

You have an address as a person would type it, and you want the facts of the home: its type, bedrooms, bathrooms, floor area, construction year and an estimated value. This post shows a complete Python program that does it with the Houski property data API and nothing outside the standard library.

Houski covers more than 220 million properties in Canada, the United States, Great Britain, much of Europe and South Korea under one schema. No license or Multiple Listing Service (MLS) membership is needed, only an API key.

Look up an address with the search endpoint

The search endpoint takes a typed address in the query parameter and returns the best matches first. The match is fuzzy, so a half-typed address or a typo still finds the home. The block below is a real call against the live API, run each time this page loads.

API request
TypeScript code
const houski_data = async (): Promise<SearchResponse> => {

    // You must copy the SearchResponse type declarations from the 
    // Houski API documentation to strongly type the response

    const url = new URL('https://api.houski.ca/search');
    url.searchParams.set('api_key', 'YOUR_API_KEY');
    url.searchParams.set('max_results', '5');
    url.searchParams.set('query', '100+main+calgary');

    const response = await fetch(url);
    const data = await response.json();

    return data;
}

(async () => {
let data: SearchResponse = await houski_data();

// Log the response
console.log(data);
})();
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 0.099999994,
  "data": [
    {
      "address": "100 Legacy Main Street SE",
      "property_id": "b6e696a369ce7c8d"
    },
    {
      "address": "100 Martin Crossing Court NE",
      "property_id": "121066ea9febfe6d"
    },
    {
      "address": "100 Martinglen Close NE",
      "property_id": "b3001564b82348b1"
    },
    {
      "address": "100 6100 MacLeod Trail SW",
      "property_id": "66c6009bf7798158"
    },
    {
      "address": "100 Marmot Way NW",
      "property_id": "6b0a08bb1a4eda6a"
    }
  ],
  "error": "",
  "match_meta": [
    {
      "match_value": 0.88235295,
      "property_id": "b6e696a369ce7c8d"
    },
    {
      "match_value": 0.88235295,
      "property_id": "121066ea9febfe6d"
    },
    {
      "match_value": 0.8235294,
      "property_id": "b3001564b82348b1"
    },
    {
      "match_value": 0.7647059,
      "property_id": "66c6009bf7798158"
    },
    {
      "match_value": 0.7647059,
      "property_id": "6b0a08bb1a4eda6a"
    }
  ],
  "price_quote": false,
  "result_total": 5,
  "time_ms": 101
}

With no select parameter, each match carries only its address and property_id. That is enough for an autocomplete box. To get the facts of the home, add select.

A complete Python program using urllib

Put your key in the HOUSKI_API_KEY environment variable. The program reads it from there, so the key is never written into source code.

Python code
"""Get property data by address with the Houski property data API.

Usage: python get_property_data_by_address.py "100 main calgary"
"""

import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request

API_URL = "https://api.houski.ca"
API_KEY = os.environ.get("HOUSKI_API_KEY") or sys.exit("Set HOUSKI_API_KEY first.")


def houski(endpoint, **parameters):
    """Send one GET request to the Houski API and return the parsed JSON answer."""
    query = urllib.parse.urlencode({"api_key": API_KEY, **parameters})
    try:
        with urllib.request.urlopen(f"{API_URL}/{endpoint}?{query}") as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        sys.exit(f"Houski answered {error.code}: {error.read().decode()}")


address = " ".join(sys.argv[1:]) or "100 main calgary"

# select names the fields to return. Only the values that come back are billed.
answer = houski(
    "search",
    query=address,
    max_results=1,
    select="property_type,bedroom,bathroom_full,interior_sq_m,construction_year,estimate_sale_price",
)

if not answer["data"]:
    sys.exit("No property matched that address.")

# A field with no value for this home is left out of the row, so print what came back.
for field, value in answer["data"][0].items():
    print(f"{field}: {value}")

print(f"Cost: {answer['cost_cents']} cents")

Every request is a GET that answers in JSON, so urllib and json are all the program needs. The same program, with versions in JavaScript, TypeScript, Rust and curl, is in the examples repository.

Choose the fields with select

The select parameter is a comma separated list of field names. There are more than 450 fields per property, listed in the field reference. Ask for the ones your code uses and nothing more, because select is what decides the cost of a request.

Here is the same kind of lookup with select, live:

API request
TypeScript code
const houski_data = async (): Promise<SearchResponse> => {

    // You must copy the SearchResponse type declarations from the 
    // Houski API documentation to strongly type the response

    const url = new URL('https://api.houski.ca/search');
    url.searchParams.set('api_key', 'YOUR_API_KEY');
    url.searchParams.set('max_results', '5');
    url.searchParams.set('query', '123+main+st+calgary');
    url.searchParams.set('select', 'address,property_type,estimate_list_price,bedroom,bathroom_half,bathroom_full');

    const response = await fetch(url);
    const data = await response.json();

    return data;
}

(async () => {
let data: SearchResponse = await houski_data();

// Log the response
console.log(data);
})();
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 2.1499999,
  "data": [
    {
      "address": "123 Main Street",
      "bathroom_full": 1,
      "bathroom_half": 0,
      "bedroom": 3,
      "estimate_list_price": 123919,
      "property_id": "aafadb43ded908a3",
      "property_type": "Townhouse"
    },
    {
      "address": "123 Main Street",
      "bathroom_full": 1,
      "bathroom_half": 0,
      "bedroom": 3,
      "estimate_list_price": 234224,
      "property_id": "ae06f5914a609e6d",
      "property_type": "House"
    },
    {
      "address": "123 Midcrest Crescent SE",
      "bathroom_full": 2,
      "bathroom_half": 0,
      "bedroom": 3,
      "estimate_list_price": 585563,
      "property_id": "d5f6e3fa9a720e57",
      "property_type": "House"
    },
    {
      "address": "123 Masters Street SE",
      "bathroom_full": 3,
      "bathroom_half": 1,
      "bedroom": 4,
      "estimate_list_price": 676479,
      "property_id": "4a7f48fcb67f69a4",
      "property_type": "House"
    },
    {
      "address": "123 Marquis Green SE",
      "bathroom_full": 4,
      "bathroom_half": 1,
      "bedroom": 3,
      "estimate_list_price": 770055,
      "property_id": "8708c4475c5f506c",
      "property_type": "House"
    }
  ],
  "error": "",
  "match_meta": [
    {
      "match_value": 0.7,
      "property_id": "aafadb43ded908a3"
    },
    {
      "match_value": 0.7,
      "property_id": "ae06f5914a609e6d"
    },
    {
      "match_value": 0.7,
      "property_id": "d5f6e3fa9a720e57"
    },
    {
      "match_value": 0.7,
      "property_id": "4a7f48fcb67f69a4"
    },
    {
      "match_value": 0.7,
      "property_id": "8708c4475c5f506c"
    }
  ],
  "price_quote": false,
  "result_total": 5,
  "time_ms": 58
}

The search endpoint also accepts max_results from 1 to 25, and an optional country_abbreviation that keeps the matches to one country.

Handle missing values

A field with no value for a home is left out of its row. It does not come back as null or as an empty string. In Python that means a direct index such as row["bedroom"] can raise a KeyError. Loop over the items of the row, as the program above does, or use row.get("bedroom") and handle None.

Values that no record states are worked out by Houski, and they are labelled Inferred. Many fields have a matching field ending in _source that says whether a value is a link to a public record, Observed, Inferred or User. If your use needs stated values only, add exclude=inferred and the inferred values are left out of the response.

Check the cost first with price_quote

Add price_quote=true to any request and the API returns what the request would cost, for free, with no data. In the program that is one more argument:

Python code
quote = houski("search", query=address, max_results=1, select="bedroom,bathroom_full", price_quote="true")
print(f"This request would cost {quote['cost_cents']} cents")

Every real answer also reports its own cost in cost_cents, next to data, error, result_total, time_ms, cache_hit and price_quote.

What a lookup costs

Billing is on the values returned, never on how much was searched. Most fields cost $0.001 per value returned, and a field with no value for a home is not billed. The program above selects six fields for one home, so one lookup costs a little over half a cent at most, and a thousand lookups cost about six dollars.

Pay as you go has a $99 monthly minimum in United States dollars, and every account sets its own maximum monthly spend. The pricing page has the full rates.

Next steps

Once you have a property_id, the properties endpoint filters and sorts whole cities, and the expand parameter attaches permits, listing history and assessments to a home.

Get a key and run the program: start with the quick start guide.