Python Write JSON to File With Pretty Print (Tested)
Save a Python dict as a readable JSON file with json.dump(indent=4), keep £ and é as they are, handle datetimes, overwrite safely and append the right way.
A simple UK postcode regex for JavaScript is /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i. It accepts every real postcode format, from M1 1AE to SW1A 1AA, with or without the space and in any case. If you also want to reject impossible postcodes such as Q1 1AA, use the stricter pattern below, which only allows the letters Royal Mail actually uses in each position. In this post I test both on 14 inputs, write a function that turns sw1a1aa into SW1A 1AA, and wire it into a form.
I wrote this after my post on email validation with regex, because UK address forms nearly always ask for a postcode next to the email. Every snippet was run with Node 20 or in headless Chrome, and the outputs are copied straight from the terminal.
Every UK postcode has two parts separated by a space:
SW1A, M1 or DN55. It's 2 to 4 characters long.1AA.That gives six shapes, where A is a letter and 9 is a digit:
A9 9AA, e.g. M1 1AEA99 9AA, e.g. B33 8THAA9 9AA, e.g. CR2 6XHAA99 9AA, e.g. DN55 1PTA9A 9AA, e.g. W1A 0AXAA9A 9AA, e.g. SW1A 1AA, Buckingham PalaceThe fact that the inward code is always three characters is the most useful rule here. It means you can always find where the space belongs, even if the user leaves it out.
Here are both patterns side by side, run against real postcodes, messy input and some that only look right:
// postcode.js: two UK postcode patterns compared
const simple = /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i;
// Stricter: only letters that are really used in each position
const strict = /^(GIR ?0AA|[A-PR-UWYZ]([0-9]{1,2}|[A-HK-Y][0-9]{1,2}|[0-9][A-HJKPSTUW]|[A-HK-Y][0-9][ABEHMNPRVWXY]) ?[0-9][ABD-HJLNP-UW-Z]{2})$/i;
const tests = [
'SW1A 1AA', // Buckingham Palace (A9A 9AA)
'EC1A 1BB', // A9A 9AA
'W1A 0AX', // A9A 9AA
'M1 1AE', // A9 9AA
'B33 8TH', // A99 9AA
'CR2 6XH', // AA9 9AA
'DN55 1PT', // AA99 9AA
'sw1a1aa', // lower case, no space
'GIR 0AA', // old Girobank code
'SW1A 1AA',// two spaces
'Q1 1AA', // Q never starts a postcode
'LS1 1CA', // C is never used in the last two letters
'SW1A 1AAA',// too long
'12345', // US ZIP
];
console.log('input'.padEnd(12), 'simple', 'strict');
for (const t of tests) {
console.log(JSON.stringify(t).padEnd(12), String(simple.test(t)).padEnd(6), strict.test(t));
}
Output:
input simple strict
"SW1A 1AA" true true
"EC1A 1BB" true true
"W1A 0AX" true true
"M1 1AE" true true
"B33 8TH" true true
"CR2 6XH" true true
"DN55 1PT" true true
"sw1a1aa" true true
"GIR 0AA" false true
"SW1A 1AA" false false
"Q1 1AA" true false
"LS1 1CA" true false
"SW1A 1AAA" false false
"12345" false false
/^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i reads as: one or two letters, a digit, an optional letter or digit, an optional space, then a digit and two letters. The i flag makes it case-insensitive, and ^ and $ make it match the whole string, not a postcode hidden inside other text.
It accepts all six real formats and rejects the obvious junk, like a US ZIP code. Its weakness is that it's too kind: Q1 1AA and LS1 1CA pass even though neither can exist. Q is never the first letter of a postcode, and C is never used in the last two letters. For most sign-up forms that's fine, because you only want to catch typos.
The strict one encodes Royal Mail's rules about which letters can appear where. [A-PR-UWYZ] is the first letter (no Q, V or X), [A-HK-Y] the second (no I, J or Z), and [ABD-HJLNP-UW-Z]{2} the final two letters (no C, I, K, M, O or V). It also has a special case for GIR 0AA, the old Girobank postcode, which breaks all the rules.
Neither pattern accepts two spaces in the middle, as the "SW1A 1AA" row shows. That's realistic: people paste postcodes with odd spacing all the time. Rather than making the regex more forgiving, it's better to clean the input first.
This function strips everything that isn't a letter or digit, upper-cases the rest, puts the space back before the last three characters, and only then runs the strict regex:
// normalise.js: clean up what people type, then check it
const strict = /^(GIR ?0AA|[A-PR-UWYZ]([0-9]{1,2}|[A-HK-Y][0-9]{1,2}|[0-9][A-HJKPSTUW]|[A-HK-Y][0-9][ABEHMNPRVWXY]) ?[0-9][ABD-HJLNP-UW-Z]{2})$/;
function normalisePostcode(input) {
const compact = String(input).toUpperCase().replace(/[^A-Z0-9]/g, '');
if (compact.length < 5 || compact.length > 7) return null;
// The inward code is always the last 3 characters
const formatted = compact.slice(0, -3) + ' ' + compact.slice(-3);
return strict.test(formatted) ? formatted : null;
}
for (const input of ['sw1a1aa', ' m1 1ae ', 'b33-8th', 'dn551pt', 'SW1A 1AAA', 'hello']) {
console.log(JSON.stringify(input).padEnd(15), '->', normalisePostcode(input));
}
Output:
"sw1a1aa" -> SW1A 1AA
" m1 1ae " -> M1 1AE
"b33-8th" -> B33 8TH
"dn551pt" -> DN55 1PT
"SW1A 1AAA" -> null
"hello" -> null
Because the inward code is always three characters, compact.slice(0, -3) + ' ' + compact.slice(-3) is all it takes to put the space in the right place. The length check (5 to 7 characters without the space) rejects anything that can't be a postcode before the regex runs. Returning the formatted value, not just true, means you save a consistent SW1A 1AA in your database instead of five different spellings of the same address.
Here's the function in a small form. It uses novalidate so the script controls the error message, autocomplete="postal-code" so browsers can fill it in, and aria-invalid plus role="alert" so screen readers hear the error:
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<title>UK postcode check</title>
</head>
<body>
<form id="address-form" novalidate>
<label for="postcode">Postcode</label>
<input id="postcode" name="postcode" autocomplete="postal-code" required>
<p id="postcode-error" role="alert"></p>
<button type="submit">Continue</button>
</form>
<script>
const strict = /^(GIR ?0AA|[A-PR-UWYZ]([0-9]{1,2}|[A-HK-Y][0-9]{1,2}|[0-9][A-HJKPSTUW]|[A-HK-Y][0-9][ABEHMNPRVWXY]) ?[0-9][ABD-HJLNP-UW-Z]{2})$/;
function normalisePostcode(input) {
const compact = String(input).toUpperCase().replace(/[^A-Z0-9]/g, '');
if (compact.length < 5 || compact.length > 7) return null;
const formatted = compact.slice(0, -3) + ' ' + compact.slice(-3);
return strict.test(formatted) ? formatted : null;
}
const form = document.getElementById('address-form');
const input = document.getElementById('postcode');
const error = document.getElementById('postcode-error');
form.addEventListener('submit', (event) => {
event.preventDefault();
const postcode = normalisePostcode(input.value);
if (!postcode) {
error.textContent = 'Enter a real UK postcode, like SW1A 1AA';
input.setAttribute('aria-invalid', 'true');
return;
}
input.value = postcode; // show the tidy version
input.removeAttribute('aria-invalid');
error.textContent = '';
console.log('OK:', postcode);
});
</script>
</body>
</html>
I loaded the page in headless Chrome and submitted four values. Here's what happened to the field, the error text and aria-invalid each time:
"" => value="" error="Enter a real UK postcode, like SW1A 1AA" aria-invalid=true
"Q1 1AA" => value="Q1 1AA" error="Enter a real UK postcode, like SW1A 1AA" aria-invalid=true
OK: SW1A 1AA
"sw1a1aa" => value="SW1A 1AA" error="" aria-invalid=null
OK: LS1 4AP
" ls1 4ap " => value="LS1 4AP" error="" aria-invalid=null
An empty box and Q1 1AA both show the error. sw1a1aa is accepted and the field is rewritten to SW1A 1AA, which also shows the user that you understood them. If you want to know more about the built-in checks I switched off with novalidate, see my guide to HTML form input types and validation.
A regex can only tell you that a postcode is the right shape. B1 1ZZ passes the strict pattern, but no such postcode exists. If you need to know a postcode is real, for deliveries for example, ask a lookup service. The free postcodes.io API, built on open data from the ONS and Ordnance Survey, has a validate endpoint:
// exists.mjs: format is fine, but does the postcode actually exist?
async function postcodeExists(postcode) {
const url = 'https://api.postcodes.io/postcodes/' + encodeURIComponent(postcode) + '/validate';
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
return data.result; // true or false
}
for (const pc of ['SW1A 1AA', 'ZZ99 9ZZ', 'B1 1ZZ']) {
console.log(pc.padEnd(9), await postcodeExists(pc));
}
Output:
SW1A 1AA true
ZZ99 9ZZ false
B1 1ZZ false
I'd still run the regex first. It's instant, works offline and saves an API call for obvious typos. Then call the API only when the shape is right, and treat a network error as "can't check right now" rather than "invalid", so a slow connection doesn't lock people out of your form. If you're new to calling APIs, my post on async/await in JavaScript explains the await fetch() pattern used here.
Browser validation is for the user's convenience. Anyone can switch off JavaScript or send a request straight to your server, so validate the postcode again there. The same regex works in PHP's preg_match() with the /i flag, and the normalising steps are one line each with strtoupper() and preg_replace(). My PHP form validation and sanitisation guide shows where that check fits in a full form handler.
For most forms: /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i. For strict checking of which letters are allowed in each position, use the longer pattern in the strict variable above.
SW1A 1AA (Buckingham Palace), M1 1AE, B33 8TH, CR2 6XH and DN55 1PT are all valid examples, and between them they cover the main formats.
Yes. Many people type postcodes without the space. Accept it either way, then save the tidy version with exactly one space before the last three characters.
Not all of them. BFPO addresses (BFPO 123) and the Falkland Islands (FIQQ 1ZZ) fail both patterns, while Gibraltar's GX11 1AA happens to pass because it follows the normal shape. If your users need BFPO or other overseas codes, add them as extra alternatives with |.
No. A regex checks the format only. Use a lookup such as postcodes.io, or Royal Mail's Postcode Address File for commercial use, to check that a postcode is real.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Save a Python dict as a readable JSON file with json.dump(indent=4), keep £ and é as they are, handle datetimes, overwrite safely and append the right way.
Hash passwords in PHP with password_hash(), check them with password_verify(), and build a PDO register and login that upgrades old hashes. Real output.
Stop tracking a file in Git without deleting it: git rm --cached, .gitignore, whole folders, a dry run, undoing it, and the catch for teammates and secrets.