Skip to main content
Free UK property data API Start free →
Back to Blog
Investor & Landlord 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.

Homedata Team · Updated

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). 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 can't 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 isn't a viable workflow.

May 2026 update — MEES Band C consultation

The government published a revised consultation in late 2025 on raising the minimum EPC for rented homes to Band C — currently proposed for new tenancies by 2028, all tenancies by 2030. As of May 2026, this hasn't been legislated, but the direction is firm enough that portfolio managers are already stress-testing against the C threshold. See the current MHCLG consultations page for the live status.

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's consultation proposes raising the MEES minimum to Band C (score 69) for new tenancies by 2028, with all tenancies following by 2030. Not law yet — but given how slowly EPCs are improved in practice, you want this pipeline running now, not when the final legislation drops.

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 building with the free API →

100 calls/month · EPC, Land Registry, council tax, schools · No credit card

Get free API key

Start checking EPC compliance now

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

Related posts

May 7, 2026

UK Stamp Duty Calculator 2026: How Much SDLT Will You Pay?

May 7, 2026

Selling a Property After Probate: The Complete UK Guide for 2026

May 7, 2026

What Property Can I Afford in the UK? The 2026 Buyer Guide

May 7, 2026

How Do UK Mortgages Work? A Complete 2026 Guide

May 7, 2026

How Long Does It Take to Buy a House in the UK? 2026 Timeline

May 7, 2026

How Much Does Moving House Cost in the UK? 2026 Breakdown

May 7, 2026

What Is Conveyancing? The UK Step-by-Step Guide for 2026

May 7, 2026

How Do Property Auctions Work in the UK? The 2026 Investor Guide

May 4, 2026

Mansion Tax 2028? The High Value Council Tax Surcharge Explained

Apr 29, 2026

Holiday Let Rules 2026: Registration, Planning and Tax Changes for Airbnb Owners

Apr 24, 2026

Ground Rent Cap and Commonhold: What the 2026 Leasehold Reform Bill Really Changes

Apr 17, 2026

Planning Rules 2026: What Developers Need to Know After the Planning and Infrastructure Act

Apr 10, 2026

Scotland Rent Controls 2026: What Has Started, What Comes Later, and Who Is Exempt

Apr 3, 2026

Are Property Packs Coming Back? Material Information and Home Buying Reform Explained

May 1, 2026

Renters Rights Act 2026: What Landlords Must Do Now

May 6, 2026

31 May 2026 Landlord Deadline: The Renters Rights Act Information Sheet Explained

May 3, 2026

Section 21 Is Gone: How Landlords Can Repossess Property After 1 May 2026

Apr 22, 2026

EPC C by 2030: What the 2026 MEES Decision Means for Landlords

Apr 15, 2026

Building Safety Levy October 2026: What Residential Developers Must Budget For

Apr 8, 2026

Making Tax Digital for Landlords: April 2026 Start Date, Thresholds and Software Checklist

May 6, 2026

How to Scrape UK Property Listings (And Why You Probably Shouldn t)

Apr 4, 2026

UK Postcode Geocoding API: Convert Postcodes to Coordinates (Python & JS)

Apr 4, 2026

How to Bulk Export UK Property Data: Python & Node.js Guide

Mar 17, 2026

Tracking UK Property Market Activity via API: Listing Events, Price Reductions, and Deal Flow

Mar 3, 2026

UK House Price Growth by Postcode: Tracking Capital Appreciation via API

Feb 17, 2026

How to Build an Automated Property Valuation (AVM) with the Homedata API

Feb 3, 2026

Building a School Finder: Ofsted Ratings + Distance from Any UK Postcode

Jan 20, 2026

How to Calculate Property Development Feasibility in the UK

Jan 6, 2026

How to Build a Rental Yield Calculator with the Homedata API

Dec 22, 2025

How Much Is My Council Tax? The Exact Formula (and Why Postcode Estimates Are Wrong)

Dec 8, 2025

Address Lookup Tutorial: Typeahead → Resolve → Enriched Property in Under 50 Lines

Nov 24, 2025

What Is a UPRN? The Complete Guide to UK Property Reference Numbers

Nov 10, 2025

EPC Ratings Explained: A Developer s Guide to Energy Performance Certificates

Oct 27, 2025

UK Flood Risk Data Explained: NAFRA2, Risk Bands, and the EA Flood Risk API

Oct 13, 2025

Council Tax Bands API: VOA Data, Band Lookup, and Use Cases

Apr 6, 2026

UK Census 2021 Demographics API: Get Area Data by Postcode (Python & JS)