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.
To format a number as currency in JavaScript, create a formatter with new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }) and call its format() method. It turns 1234.5 into £1,234.50, adding the pound sign, the comma between thousands and exactly two decimal places. It's built into every modern browser and Node, so you don't need a library. Below are the options I actually use, such as no symbol and whole pounds, the real output from running each one, and the money mistakes that toFixed() won't save you from.
I needed this for my UK student loan repayment calculator, which shows a lot of pound amounts on one screen. Writing my own comma-adding function felt clever for about five minutes, until negative numbers broke it. Every snippet here was run with Node 20 and the outputs are copied from the terminal.
// currency.js: format numbers as pounds with Intl.NumberFormat
const gbp = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
console.log(gbp.format(1234.5));
console.log(gbp.format(0.5));
console.log(gbp.format(1000000));
console.log(gbp.format(-42.1));
console.log(gbp.format(19.999));
Output:
£1,234.50
£0.50
£1,000,000.00
-£42.10
£20.00
That one formatter handles the parts people usually get wrong by hand:
£1,000,000.00.0.5 becomes £0.50, not £0.5.19.999 rounds up to £20.00.-£42.10, which is how British banks and spreadsheets show it.The two arguments matter. 'en-GB' is the locale: it decides how the number looks (commas, a full stop for decimals, symbol in front). 'GBP' is the currency: it decides which symbol and how many decimal places. They're separate on purpose, as you'll see when we format euros below.
These are the variations I've needed most:
const n = 1234.5;
// No symbol, but still commas and 2 decimal places
const plain = new Intl.NumberFormat('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
console.log('no symbol: ', plain.format(n));
// Whole pounds only
const whole = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', maximumFractionDigits: 0 });
console.log('whole pounds:', whole.format(n));
// Short form for dashboards
const compact = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', notation: 'compact' });
console.log('compact: ', compact.format(1234567));
// Accounting style negatives in brackets
const accounting = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', currencySign: 'accounting' });
console.log('accounting: ', accounting.format(-42.1));
// Show the code instead of the symbol
const code = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', currencyDisplay: 'code' });
console.log('code: ', code.format(n));
// Always show + or -
const signed = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', signDisplay: 'exceptZero' });
console.log('signed: ', signed.format(25), signed.format(-25), signed.format(0));
Output:
no symbol: 1,234.50
whole pounds: £1,235
compact: £1.2M
accounting: (£42.10)
code: GBP 1,234.50
signed: +£25.00 -£25.00 £0.00
style: 'currency' and set both fraction digit options to 2. Useful for a table where the £ is in the column heading.maximumFractionDigits: 0 rounds to the nearest pound, so 1234.5 becomes £1,235. In some older browsers this throws a RangeError unless you also set minimumFractionDigits: 0, so set both if you support them.notation: 'compact' gives £1.2M, which is good for dashboards and charts, but not for anything someone has to pay.currencySign: 'accounting' puts negatives in brackets, (£42.10), like a balance sheet.currencyDisplay: 'code' shows GBP 1,234.50, which is clearer on sites that mix several currencies.signDisplay: 'exceptZero' adds a + to positive values, handy for showing a change, such as "+£25.00 since last month".Keep the locale fixed to your audience and only change the currency. Here's what that looks like, with a few other locales for comparison:
const amount = 1234.5;
const rows = [
['en-GB', 'GBP'],
['en-GB', 'EUR'],
['en-GB', 'USD'],
['en-US', 'USD'],
['de-DE', 'EUR'],
['en-IN', 'INR'],
['bn-BD', 'BDT'],
];
for (const [locale, currency] of rows) {
const f = new Intl.NumberFormat(locale, { style: 'currency', currency });
console.log(locale, currency, f.format(amount));
}
Output:
en-GB GBP £1,234.50
en-GB EUR €1,234.50
en-GB USD US$1,234.50
en-US USD $1,234.50
de-DE EUR 1.234,50 €
en-IN INR ₹1,234.50
bn-BD BDT ১,২৩৪.৫০৳
Look at the third line. For a UK reader, en-GB writes US dollars as US$1,234.50, because a bare $ could be Australian or Canadian. An American locale just writes $1,234.50. Germany puts the euro sign after the number and swaps the comma and full stop. Bangladesh (my home) uses Bengali digits. You get all of that free by picking the right locale, which is a good reason not to build currency strings by gluing a symbol onto toFixed(2).
If you don't know your visitor's locale, navigator.language gives you the browser's setting. For a UK-focused site, I'd just hard-code 'en-GB' so prices look the same for everyone.
Formatting is the last step. If the number is already wrong, it will just print the wrong number beautifully:
// 1. Floating point: add money as pence (integers), not pounds
console.log(0.1 + 0.2);
console.log((10 + 20) / 100);
// 2. toFixed() rounds the binary value, not the decimal you typed
console.log((1.005).toFixed(2));
console.log((2.675).toFixed(2));
// 3. Intl rounds "half expand" on the decimal string it sees
const gbp = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
console.log(gbp.format(1.005), gbp.format(2.675));
// 4. Format pence safely
function formatPence(pence) {
return gbp.format(pence / 100);
}
const basket = [199, 250, 1099]; // £1.99, £2.50, £10.99
const total = basket.reduce((sum, p) => sum + p, 0);
console.log(total, formatPence(total));
Output:
0.30000000000000004
0.3
1.00
2.67
£1.01 £2.68
1548 £15.48
0.1 + 0.2 isn't 0.3 in JavaScript, because numbers are stored in binary. Add up a basket of prices in pounds and you'll eventually be a penny out. The fix is to store and add money as whole pence (integers), and divide by 100 only when you format, like formatPence() does. I used the same rule in the student loan calculator, where HMRC's rounding has to be exact. My post on how UK student loan repayments are calculated shows a real case where floating point would take a pound too much.toFixed() rounding. (1.005).toFixed(2) gives "1.00", not "1.01", because 1.005 is really stored as 1.00499999…. Intl.NumberFormat rounds based on the decimal value, so it gives £1.01. Either way, the real answer is to avoid fractions of a penny in the first place.The basket example uses reduce() to add up the pence. If that method is new to you, see JavaScript map, filter and reduce explained.
Number.prototype.toLocaleString() takes the same arguments and is fine for a one-off. In a loop, create the formatter once and reuse it:
const price = 49.99;
console.log(price.toLocaleString('en-GB', { style: 'currency', currency: 'GBP' }));
// Reuse one formatter in loops; creating one per call is slower
const gbp = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
let t = performance.now();
for (let i = 0; i < 100000; i++) gbp.format(i / 100);
const reuse = performance.now() - t;
t = performance.now();
for (let i = 0; i < 100000; i++) (i / 100).toLocaleString('en-GB', { style: 'currency', currency: 'GBP' });
const each = performance.now() - t;
console.log(`reused formatter: ${reuse.toFixed(0)} ms, toLocaleString each time: ${each.toFixed(0)} ms`);
// formatToParts lets you style the symbol separately
console.log(gbp.formatToParts(1234.5).map(p => `${p.type}:${p.value}`).join(' | '));
Output (timings will vary on your machine):
£49.99
reused formatter: 59 ms, toLocaleString each time: 2728 ms
currency:£ | integer:1 | group:, | integer:234 | decimal:. | fraction:50
On my machine, formatting 100,000 prices took about 60 ms with one reused formatter and over 2.7 seconds with toLocaleString(), because each call builds a new formatter behind the scenes. That's the difference between a smooth table and a frozen tab. I keep one formatter at the top of the file and import it wherever I need it.
formatToParts() splits the result into labelled pieces. That lets you wrap the £ in a <span> to make it smaller, or style the pence differently, without parsing the string yourself.
Putting it together, this is the helper I copy into projects:
// money.js: one formatter, whole pence in, pounds out
const gbp = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
export function formatGBP(pence) {
if (!Number.isInteger(pence)) throw new TypeError('Pass whole pence, e.g. 1999');
return gbp.format(pence / 100);
}
console.log(formatGBP(1999));
console.log(formatGBP(123450));
try { formatGBP(19.99); } catch (e) { console.log(e.name + ': ' + e.message); }
Output:
£19.99
£1,234.50
TypeError: Pass whole pence, e.g. 1999
It accepts pence, so the rest of the app is forced to do money maths in integers. If you also show dates next to prices, such as on an invoice, my post on formatting a date as dd/mm/yyyy in JavaScript uses the same Intl API.
Use new Intl.NumberFormat('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n). Add style: 'currency', currency: 'GBP' if you also want the £ sign.
Let Intl.NumberFormat add it with currency: 'GBP'. If you need the character on its own, '£' works directly in a UTF-8 file, or use '\u00A3'.
Leave out style: 'currency' and set the fraction digits to 2, as in the "no symbol" example. You keep the commas and decimals without the £.
Yes, basic currency formatting works in every modern browser and in Node. Some newer options, such as notation: 'compact' and signDisplay, need browsers from around 2020 onwards.
Store them as whole pence (integers) in JavaScript, or as a DECIMAL column in your database. Never store the formatted string; format only when you display it.
MDN's Intl.NumberFormat reference lists every option.
// 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.