Postcodes
The kindtech.postcodes module turns UK postcodes (and postcode outcodes) into
the ONS geography codes the rest of KindTech joins on. It wraps
postcodes.io — no API key required.
It's the layer that connects point-level address data to the common geography
types: any list of postcodes becomes a geography_code column — at LSOA, MSOA,
OA, LAD, ward, ICB or TTWA — that joins straight to boundaries
(load_geodata) and statistics (load_ons).
Functions
postcodes_to_geography()
Map postcodes to a single geography level, ready to join.
from kindtech import postcodes_to_geography
located = postcodes_to_geography(["SE13 7HX", "SE6 4RU"], geography_type="LSOA")
# postcode geography_code geography_name
# 0 SE13 7HX E01034394 Lewisham 040C
# 1 SE6 4RU E01003318 Lewisham 020B
Returns columns postcode, geography_code, geography_name. The
geography_code aligns with geodata_to_properties() and load_ons(), so the
result merges directly:
import pandas as pd
from kindtech import postcodes_to_geography, load_geodata, load_ons, geodata_to_properties
# 1. Postcodes -> counts per LSOA
located = postcodes_to_geography(postcodes, "LSOA")
per_lsoa = located.groupby("geography_code").size().reset_index(name="count")
# 2. Population for per-capita, and boundaries for the map — same join key
pop = load_ons("population_lsoa", geography_type="LSOA", time="latest")
geo = pd.DataFrame(geodata_to_properties(load_geodata("LSOA"), "LSOA", 2021))
merged = geo.merge(per_lsoa, on="geography_code", how="left").merge(
pop, on="geography_code", how="left"
)
Supported geography_type values: LSOA, MSOA, OA, LAD, WD, ICB,
TTWA (string or GeographyType enum). An unsupported level raises
ValueError.
lookup_postcodes()
The full lookup — one row per postcode with every geography level at once.
from kindtech import lookup_postcodes
df = lookup_postcodes(["SE13 7HX", "M1 1AE", "NOTAPC"])
Columns: postcode, valid, lsoa_code/lsoa_name, msoa_code/msoa_name,
oa_code, lad_code/lad_name, ward_code, icb_code, ttwa_code,
latitude, longitude.
Invalid postcodes are flagged, not dropped
Unrecognised postcodes return a row with valid=False and None codes, so
the output always lines up with the input. Filter with
df[df["valid"]] when you only want matches.
lookup_outcodes()
Look up postcode outcodes — the prefix before the space (e.g. SE13). An
outcode spans many areas, so this returns the list of Local Authorities it
touches plus the outcode's geometric centroid.
from kindtech import lookup_outcodes
lookup_outcodes(["SE13", "M1"])
# outcode valid admin_districts latitude longitude
# 0 SE13 True Greenwich, Lewisham 51.459641 -0.009665
Columns: outcode, valid, admin_districts (comma-separated LAD names),
latitude, longitude.
outcode_to_geography()
Approximate an outcode to a single geography via its centroid.
from kindtech import outcode_to_geography
outcode_to_geography(["SE13"], geography_type="LSOA")
# outcode geography_code geography_name
# 0 SE13 E01033327 Lewisham 041B
Outcode mapping is approximate
An outcode covers many LSOAs/wards. This returns the geography containing
the outcode's geometric centroid — a rough stand-in, useful when your data
is only tagged by postcode prefix. For per-area analysis (e.g. counts
per capita by LSOA), prefer full postcodes via postcodes_to_geography(),
or split an outcode's records across its constituent areas weighted by
population.
Design
kindtech.postcodes is a thin runtime wrapper — unlike the geo and ons
modules it ships no catalog and has no ingestion step, because postcodes.io
serves the lookup directly. Requests are batched (100 postcodes per call, the
postcodes.io limit) and results preserve input order. DataFrames come back in
whatever backend you have installed (pandas or polars), like the rest of
KindTech.
See Data Sources for the underlying datasets, licensing, and limits.