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). 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
requestslibrary (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) |
|---|---|---|
| A | 92–100 | ✅ Compliant |
| B | 81–91 | ✅ Compliant |
| C | 69–80 | ✅ Compliant |
| D | 55–68 | ✅ Compliant |
| E | 39–54 | ✅ Compliant (minimum) |
| F | 21–38 | ❌ Non-compliant |
| G | 1–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_areafield 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 band —
construction_age_bandtells 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
Start checking EPC compliance now
Free API key — 100 calls/month, no credit card required.