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
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 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_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 checking EPC compliance now
Free API key — 100 calls/month, no credit card required.