Replacing getAddress.io? Free drop-in replacement →
← Back to Blog
Tutorials Sep 29, 2025 · 10 min read

Build an EPC Compliance Checker with Python and the Homedata API

A practical tutorial: check MEES compliance across a property portfolio using the EPC API. Includes a full Python script with CSV export and Band C stress-testing.

What you're building — and why it matters

By the end of this tutorial, you'll have a Python script that takes a list of property UPRNs, looks up the EPC rating for each, and flags any that don't meet the Minimum Energy Efficiency Standards (MEES). You can run it on a spreadsheet export, a database query, or any list of rental properties.

This is genuinely useful. Since April 2020, landlords in England and Wales cannot legally rent out a property with an EPC rating below Band E (score 38 or below). Properties rated F or G face fines of up to £5,000 per breach. For property managers or software teams maintaining portfolios, manual checking is not a viable workflow.

What you need to follow along:

  • A free Homedata API key (100 calls/month on the free tier)
  • Python 3.8+
  • The requests library (pip install requests)
  • A list of UPRNs — even a handful of test ones works

Understanding the EPC score

Before writing code, it helps to understand what you're checking. The EPC API returns two scores:

  • current_energy_efficiency — the property's actual score right now (1–100)
  • potential_energy_efficiency — what it could score with recommended improvements

Both scores map to letter bands:

Band Score range MEES status (rental)
A92–100✅ Compliant
B81–91✅ Compliant
C69–80✅ Compliant
D55–68✅ Compliant
E39–54✅ Compliant (minimum)
F21–38❌ Non-compliant
G1–20❌ Non-compliant

For MEES compliance, you need current_energy_efficiency >= 39. That's Band E or above. Score of 38 or below means the property cannot legally be rented out without an exemption.

Step 1: Make your first API call

The endpoint is GET /api/epc-checker/{uprn}/. Pass your UPRN as a path parameter — not a query string.

curl "https://api.homedata.co.uk/api/epc-checker/100023336956/" \
  -H "Authorization: Api-Key YOUR_API_KEY"

You'll get back something like this:

{
  "uprn": 100023336956,
  "current_energy_efficiency": 72,
  "potential_energy_efficiency": 85,
  "last_epc_date": "2022-04-15",
  "epc_floor_area": 68,
  "construction_age_band": "1967-1975",
  "epc_id": "1234567890512022041514144822200478"
}

A score of 72 = Band C. This property is compliant and well above the E minimum. Potential score of 85 = Band B — achievable with improvements.

Step 2: Convert score to band

The API returns the numeric score. You'll often want the letter band alongside it — for display, reporting, or logic. Here's a helper function:

def score_to_band(score: int) -> str:
    """Convert EPC efficiency score (1-100) to letter band."""
    if score >= 92:
        return "A"
    elif score >= 81:
        return "B"
    elif score >= 69:
        return "C"
    elif score >= 55:
        return "D"
    elif score >= 39:
        return "E"
    elif score >= 21:
        return "F"
    else:
        return "G"

def is_mees_compliant(score: int) -> bool:
    """Returns True if the property meets the E-band minimum."""
    return score >= 39

Step 3: Build the compliance checker

Now let's put it together into a script that checks a list of UPRNs and produces a clear compliance report:

import requests
import time

API_KEY = "your_api_key_here"
BASE_URL = "https://api.homedata.co.uk"

def score_to_band(score: int) -> str:
    if score >= 92: return "A"
    elif score >= 81: return "B"
    elif score >= 69: return "C"
    elif score >= 55: return "D"
    elif score >= 39: return "E"
    elif score >= 21: return "F"
    else: return "G"

def check_epc(uprn: int) -> dict:
    """Fetch EPC data for a single UPRN."""
    resp = requests.get(
        f"{BASE_URL}/api/epc-checker/{uprn}/",
        headers={"Authorization": f"Api-Key {API_KEY}"},
        timeout=10,
    )

    if resp.status_code == 404:
        return {"uprn": uprn, "status": "not_found", "error": "No EPC record"}
    if not resp.ok:
        return {"uprn": uprn, "status": "error", "error": resp.text}

    data = resp.json()
    score = data.get("current_energy_efficiency", 0)
    potential = data.get("potential_energy_efficiency", 0)

    return {
        "uprn": uprn,
        "status": "ok",
        "current_score": score,
        "current_band": score_to_band(score),
        "potential_score": potential,
        "potential_band": score_to_band(potential),
        "last_epc_date": data.get("last_epc_date"),
        "construction_age_band": data.get("construction_age_band"),
        "compliant": score >= 39,
        "gap_to_e": max(0, 39 - score),  # 0 if already compliant
        "gap_to_c": max(0, 69 - score),  # 0 if already Band C or better
    }

def run_compliance_check(uprns: list[int]) -> None:
    """Check EPC compliance for a list of UPRNs and print a report."""
    results = []

    print(f"Checking {len(uprns)} properties...\n")

    for uprn in uprns:
        result = check_epc(uprn)
        results.append(result)
        time.sleep(0.1)  # gentle rate limiting

    # Separate results
    ok = [r for r in results if r["status"] == "ok"]
    not_found = [r for r in results if r["status"] == "not_found"]
    errors = [r for r in results if r["status"] == "error"]
    non_compliant = [r for r in ok if not r["compliant"]]
    compliant = [r for r in ok if r["compliant"]]

    # Print report
    print("=" * 60)
    print(f"MEES COMPLIANCE REPORT — {len(uprns)} properties checked")
    print("=" * 60)
    print(f"✅ Compliant (E or above):  {len(compliant)}")
    print(f"❌ Non-compliant (F or G):  {len(non_compliant)}")
    print(f"⚠️  No EPC record:          {len(not_found)}")
    print(f"💥 API errors:             {len(errors)}")
    print()

    if non_compliant:
        print("NON-COMPLIANT PROPERTIES:")
        print("-" * 60)
        for r in non_compliant:
            print(
                f"  UPRN {r['uprn']}: "
                f"Band {r['current_band']} ({r['current_score']}) — "
                f"needs +{r['gap_to_e']} points to reach E | "
                f"potential: Band {r['potential_band']} ({r['potential_score']})"
            )
        print()

    if not_found:
        print("NO EPC RECORD (may need inspection):")
        print("-" * 60)
        for r in not_found:
            print(f"  UPRN {r['uprn']}: {r['error']}")
        print()

# --- Run it ---
portfolio = [
    100023336956,
    10090067699,
    200003553960,
    100021421083,
    # Add your UPRNs here
]

run_compliance_check(portfolio)

Example output:

Checking 4 properties...

============================================================
MEES COMPLIANCE REPORT — 4 properties checked
============================================================
✅ Compliant (E or above):  3
❌ Non-compliant (F or G):  1
⚠️  No EPC record:          0
💥 API errors:             0

NON-COMPLIANT PROPERTIES:
------------------------------------------------------------
  UPRN 200003553960: Band F (34) — needs +5 points to reach E | potential: Band D (61)

Step 4: Export to CSV

For anything beyond a handful of properties, you'll want the results in a format you can share. Add this to your script:

import csv

def export_to_csv(results: list[dict], filename: str = "epc_report.csv") -> None:
    """Export compliance results to CSV."""
    fields = [
        "uprn", "status", "current_score", "current_band",
        "potential_score", "potential_band", "compliant",
        "gap_to_e", "gap_to_c", "last_epc_date", "construction_age_band",
    ]
    with open(filename, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(results)
    print(f"Report saved to {filename}")

Then call it after running the check:

results = [check_epc(uprn) for uprn in portfolio]
export_to_csv(results, "portfolio_epc_report.csv")

Step 5: Planning ahead for Band C

The government has proposed raising the MEES minimum to Band C (score 69) for new tenancies by 2028, with all tenancies following by 2030. This hasn't been legislated yet, but smart portfolio managers are already stress-testing their properties against the C threshold.

The gap_to_c field in the script above already calculates this. Add a section to your report:

below_c = [r for r in ok if r["current_score"] < 69]

if below_c:
    print(f"\nPROPERTIES BELOW BAND C ({len(below_c)} of {len(ok)}):")
    print("(Not currently non-compliant, but at risk under proposed 2028 rules)")
    print("-" * 60)
    for r in sorted(below_c, key=lambda x: x["current_score"]):
        print(
            f"  UPRN {r['uprn']}: "
            f"Band {r['current_band']} ({r['current_score']}) — "
            f"needs +{r['gap_to_c']} points for C | "
            f"potential: Band {r['potential_band']}"
        )

Reading the potential score

The potential_energy_efficiency score represents what the property could achieve with the improvements recommended on the EPC certificate — typically a combination of insulation upgrades, better heating controls, draught-proofing, and glazing improvements.

If a non-compliant property has a potential score of 45+ (Band E), it means the assessor believes it can be brought up to compliance without major structural work. If the potential score is still below 39, you're looking at a property that may qualify for a MEES exemption — register one with the PRS Exemptions Register if improvements aren't feasible.

for r in non_compliant:
    gap_potential = r["potential_score"] - r["current_score"]
    if r["potential_score"] >= 39:
        print(
            f"UPRN {r['uprn']}: improvable — "
            f"potential Band {r['potential_band']} (+{gap_potential} points achievable)"
        )
    else:
        print(
            f"UPRN {r['uprn']}: may need exemption — "
            f"potential Band {r['potential_band']} still below E"
        )

What about properties with no EPC?

A 404 response means there's no EPC record in the register for that UPRN. This doesn't necessarily mean the property is exempt — it may mean:

  • The property was built before EPC requirements (pre-2008) and hasn't been sold or let recently
  • The UPRN in your system doesn't match the one in the EPC register
  • The certificate was lodged under a different UPRN or address variant

For rental properties with no EPC record, the safest approach is to commission a new assessment — landlords are required to have a valid EPC before marketing a property for rent.

Next steps

The script above is a solid foundation. Here's where to take it next:

  • Schedule it — run monthly via a cron job and email a diff of any properties that have changed band or lapsed
  • Add floor area — the epc_floor_area field lets you calculate improvement cost estimates (e.g., £/m² for insulation)
  • Combine with solar data — the Solar Assessment API shows estimated generation, payback period, and post-panel EPC score for each property
  • Triage by age bandconstruction_age_band tells you when the property was built. Pre-1930 solid-wall properties need different interventions than 1970s cavity-wall ones

See the full EPC API reference for all available fields and query options.

Start checking EPC compliance now

Free API key — 100 calls/month, no credit card required.