Building a School Finder: Ofsted Ratings + Distance from Any UK Postcode
Build a school finder that returns nearby schools sorted by Ofsted rating and distance — in under 30 lines. Covers primary, secondary, and special schools. Full Python and JavaScript examples using the Homedata Schools API.
Why schools data matters in property
School proximity is one of the strongest predictors of house prices in the UK. Properties within the catchment area of an "Outstanding" Ofsted-rated primary school command a 6–8% premium over equivalent properties near "Requires Improvement" schools. For secondary schools, the premium can hit 10–15%.
If you're building a property portal, estate agent CRM, or neighbourhood analysis tool, school data isn't optional — it's a core feature. Buyers with children filter by schools before they filter by price.
The data: GIAS and Ofsted
School data in England comes from two sources:
- GIAS (Get Information About Schools) — the DfE's official register. Contains ~27,000 open establishments with names, addresses, types, age ranges, pupil counts, and URNs (unique reference numbers).
- Ofsted — inspection outcomes. Ratings: Outstanding, Good, Requires Improvement, Inadequate. Plus the date of last inspection.
The Homedata Schools API combines both datasets. One call to /api/schools/{postcode}/ returns nearby schools with GIAS metadata and Ofsted ratings, sorted by distance.
Step 1: Query schools near a postcode
import requests
API_KEY = "your_api_key_here"
POSTCODE = "SW1A 1AA"
response = requests.get(
f"https://api.homedata.co.uk/api/schools/{POSTCODE}/",
headers={"Authorization": f"Api-Key {API_KEY}"}
).json()
schools = response['schools']
print(f"Found {len(schools)} schools near {POSTCODE}\n")
for s in schools[:5]:
print(f" {s['name']}")
print(f" Type: {s['type']} | Phase: {s['phase']}")
print(f" Ofsted: {s['ofsted_rating']} ({s['ofsted_date']})")
print(f" Pupils: {s['pupil_count']} | Age: {s['age_low']}–{s['age_high']}")
print(f" Distance: {s['distance_m']}m")
print()
Step 2: Filter by phase and rating
Most school finders let users filter by phase (primary/secondary) and minimum Ofsted rating. Here's a practical filter:
def filter_schools(schools, phase=None, min_rating=None, max_distance_m=None):
"""
Filter schools by phase, Ofsted rating, and distance.
Args:
phase: 'Primary', 'Secondary', 'All through', etc.
min_rating: minimum Ofsted rating (1=Outstanding, 4=Inadequate)
max_distance_m: maximum distance in metres
"""
RATING_MAP = {
'Outstanding': 1,
'Good': 2,
'Requires improvement': 3,
'Inadequate': 4,
}
filtered = schools
if phase:
filtered = [s for s in filtered if s['phase'] == phase]
if min_rating:
filtered = [s for s in filtered
if RATING_MAP.get(s.get('ofsted_rating'), 99) <= min_rating]
if max_distance_m:
filtered = [s for s in filtered if s['distance_m'] <= max_distance_m]
return filtered
# Outstanding + Good primaries within 1km
good_primaries = filter_schools(
schools,
phase='Primary',
min_rating=2, # Outstanding or Good
max_distance_m=1000 # Within 1km
)
print(f"Good+ primaries within 1km: {len(good_primaries)}")
for s in good_primaries:
print(f" {s['name']} — {s['ofsted_rating']} — {s['distance_m']}m")
Step 3: Build a school score for a property
For property portals, a single "school score" is more useful than a raw list. Here's a simple scoring algorithm that weights Ofsted rating and distance:
def school_score(schools, max_radius_m=2000):
"""
Calculate a 0–100 school score for a location.
Weights:
- Outstanding within 500m = maximum points
- Distance decay: linear to max_radius_m
- Separate primary and secondary scores, averaged
"""
def phase_score(phase_schools):
if not phase_schools:
return 50 # Neutral if no schools found
RATING_SCORES = {
'Outstanding': 100,
'Good': 75,
'Requires improvement': 35,
'Inadequate': 10,
}
total_weight = 0
weighted_score = 0
for s in phase_schools[:5]: # Top 5 nearest
rating_score = RATING_SCORES.get(s.get('ofsted_rating'), 50)
distance_factor = max(0, 1 - (s['distance_m'] / max_radius_m))
weight = distance_factor ** 0.5 # Square root for gentler decay
weighted_score += rating_score * weight
total_weight += weight
return round(weighted_score / total_weight) if total_weight > 0 else 50
primaries = [s for s in schools if s['phase'] == 'Primary']
secondaries = [s for s in schools if s['phase'] == 'Secondary']
primary_score = phase_score(primaries)
secondary_score = phase_score(secondaries)
# Weighted average (primary slightly more important for family buyers)
overall = round(primary_score * 0.55 + secondary_score * 0.45)
return {
'overall': overall,
'primary': primary_score,
'secondary': secondary_score,
'label': 'Excellent' if overall >= 80 else 'Good' if overall >= 60 else 'Average' if overall >= 40 else 'Below average',
'nearest_outstanding': next(
(s['name'] for s in schools if s.get('ofsted_rating') == 'Outstanding'),
None
),
}
score = school_score(schools)
print(f"School score: {score['overall']}/100 ({score['label']})")
print(f"Primary: {score['primary']}, Secondary: {score['secondary']}")
if score['nearest_outstanding']:
print(f"Nearest Outstanding: {score['nearest_outstanding']}")
JavaScript version
const API_KEY = 'your_api_key_here';
const BASE = 'https://api.homedata.co.uk/api';
const headers = { Authorization: `Api-Key ${API_KEY}` };
async function getSchoolScore(postcode) {
const { schools } = await fetch(
`${BASE}/schools/${encodeURIComponent(postcode)}/`,
{ headers }
).then(r => r.json());
const ratingScores = {
'Outstanding': 100, 'Good': 75,
'Requires improvement': 35, 'Inadequate': 10
};
function phaseScore(list) {
if (!list.length) return 50;
let tw = 0, ws = 0;
for (const s of list.slice(0, 5)) {
const rs = ratingScores[s.ofsted_rating] ?? 50;
const df = Math.max(0, 1 - s.distance_m / 2000);
const w = Math.sqrt(df);
ws += rs * w;
tw += w;
}
return Math.round(ws / tw);
}
const primaries = schools.filter(s => s.phase === 'Primary');
const secondaries = schools.filter(s => s.phase === 'Secondary');
return {
overall: Math.round(phaseScore(primaries) * 0.55 + phaseScore(secondaries) * 0.45),
schools: schools.length,
outstanding: schools.filter(s => s.ofsted_rating === 'Outstanding').length,
};
}
getSchoolScore('SW1A 1AA').then(console.log);
Combining with property data
The real power comes from combining schools data with other Homedata endpoints. Here's how you'd enrich a property listing:
# Full property enrichment: schools + crime + broadband
uprn = "10023456789"
postcode = "SW1A 1AA"
property_data = requests.get(
f"{BASE}/property/{uprn}/", headers=headers
).json()
schools_data = requests.get(
f"{BASE}/schools/{postcode}/", headers=headers
).json()
crime_data = requests.get(
f"{BASE}/crime/{postcode}/", headers=headers
).json()
broadband_data = requests.get(
f"{BASE}/broadband/{postcode}/", headers=headers
).json()
# Now you have everything a buyer needs:
# - Property details (beds, type, EPC)
# - Nearby schools with Ofsted ratings
# - Local crime rates by category
# - Broadband speed availability
Or use the Postcode Profile API (/api/postcode-profile/{postcode}/) to get all of this in a single call — including a school count and average Ofsted score for the area.
Use cases
| Use Case | How Schools Data Helps |
|---|---|
| Property portals | School score badges on listings — "3 Outstanding schools within 1km" |
| Estate agent CRMs | Match family buyers to properties near good schools |
| Relocation tools | Compare areas by school quality alongside transport and amenities |
| Investment analysis | School quality correlates with price resilience and rental demand |
| Local authority dashboards | Map school capacity vs. new housing development |
Try it now: Get a free API key — the Schools endpoint is included from Starter (£49/month). Start free with 100 calls/month on the Core Property module, no credit card needed.
Schools data for every UK postcode
Ofsted ratings, pupil counts, age ranges — one API call.
Get free API key →