Displaying numbers well is one of those things that looks easy until you have to support multiple locales, currencies, and display contexts at once. Does 1000000 render as 1,000,000 or 1.000.000 or 1 000 000? Does 0.05 mean 5% or 0.05%? Is 1234567 better shown as $1.23M or $1,234,567.00?
This guide covers every common formatting scenario in JavaScript, Python, Go, and PHP — from simple comma insertion to locale-aware currency display.
Quick reference
| Goal | JS | Python | Go | PHP |
|---|---|---|---|---|
| Thousands separator | Intl.NumberFormat |
f"{n:,}" |
message.NewPrinter |
number_format() |
| Currency | Intl.NumberFormat + style: "currency" |
locale.currency() / Babel |
golang.org/x/text |
number_format + symbol |
| Percentage | Intl.NumberFormat + style: "percent" |
f"{n:.1%}" |
fmt.Sprintf("%.1f%%", n*100) |
number_format($n*100, 1).'%' |
| Fixed decimals | .toFixed(2) |
f"{n:.2f}" |
fmt.Sprintf("%.2f", n) |
number_format($n, 2) |
| Compact (1.2M) | Intl.NumberFormat + notation: "compact" |
no stdlib, manual | manual | manual |
| Ordinal (1st/2nd) | manual | manual | manual | manual |
Thousands separator
The most common formatting need — adding commas (or dots, or spaces) every three digits.
JavaScript
// Locale-aware — recommended
const n = 1234567.89;
const formatted = new Intl.NumberFormat('en-US').format(n);
// → "1,234,567.89"
// German locale uses dots as thousands separator, comma as decimal
new Intl.NumberFormat('de-DE').format(n);
// → "1.234.567,89"
// Swiss French: apostrophe as thousands separator
new Intl.NumberFormat('fr-CH').format(n);
// → "1 234 567,89"
Python
n = 1234567.89
# f-string with comma grouping (always US-style)
print(f"{n:,.2f}") # → "1,234,567.89"
print(f"{n:_}") # → "1_234_567.89" (underscore separator)
# Locale-aware (must set locale first)
import locale
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
print(locale.format_string("%.2f", n, grouping=True)) # → "1,234,567.89"
# Without locale dependency — use Babel for production
from babel.numbers import format_number
print(format_number(n, locale='de_DE')) # → "1.234.567,89"
Go
package main
import (
"golang.org/x/text/language"
"golang.org/x/text/message"
"fmt"
)
func main() {
n := 1234567.89
// Using golang.org/x/text (locale-aware)
p := message.NewPrinter(language.English)
p.Printf("%.2f\n", n) // → "1,234,567.89"
// Manual implementation for simple cases
fmt.Printf("%s\n", addCommas(1234567))
}
func addCommas(n int64) string {
s := fmt.Sprintf("%d", n)
out := make([]byte, 0, len(s)+(len(s)-1)/3)
for i, c := range s {
if i > 0 && (len(s)-i)%3 == 0 {
out = append(out, ',')
}
out = append(out, byte(c))
}
return string(out)
}
PHP
<?php
$n = 1234567.89;
// number_format(number, decimals, decimal_point, thousands_sep)
echo number_format($n, 2); // → "1,234,567.89"
echo number_format($n, 2, ',', '.'); // → "1.234.567,89" (German)
echo number_format($n, 2, ',', ' '); // → "1 234 567,89" (French)
// PHP 8.0+: NumberFormatter (locale-aware, requires intl extension)
$fmt = new NumberFormatter('de_DE', NumberFormatter::DECIMAL);
echo $fmt->format($n); // → "1.234.567,89"
Currency formatting
JavaScript
const amount = 1234.5;
// USD
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
// → "$1,234.50"
// EUR in German locale
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(amount);
// → "1.234,50 €"
// Accounting format (negative in parentheses)
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencySign: 'accounting',
}).format(-amount);
// → "($1,234.50)"
Python
from babel.numbers import format_currency
amount = 1234.5
print(format_currency(amount, 'USD', locale='en_US')) # → "$1,234.50"
print(format_currency(amount, 'EUR', locale='de_DE')) # → "1.234,50 €"
print(format_currency(amount, 'GBP', locale='en_GB')) # → "£1,234.50"
Go
import (
"golang.org/x/text/currency"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
p := message.NewPrinter(language.English)
unit := currency.USD.Amount(1234.50)
p.Printf("%v\n", unit) // → "$1,234.50"
PHP
<?php
$amount = 1234.50;
// With intl extension
$fmt = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
echo $fmt->formatCurrency($amount, 'USD'); // → "$1,234.50"
$fmt2 = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
echo $fmt2->formatCurrency($amount, 'EUR'); // → "1.234,50 €"
Percentage formatting
JavaScript
const ratio = 0.1234; // 12.34%
// Automatic multiplication by 100
new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 1,
maximumFractionDigits: 2,
}).format(ratio);
// → "12.34%"
// Without Intl — manual
`${(ratio * 100).toFixed(1)}%` // → "12.3%"
Python
ratio = 0.1234
# f-string shorthand (no Babel needed)
print(f"{ratio:.1%}") # → "12.3%"
print(f"{ratio:.2%}") # → "12.34%"
# Locale-aware
from babel.numbers import format_percent
print(format_percent(ratio, locale='en_US')) # → "12%"
print(format_percent(ratio, format='#,##0.00%', locale='en_US')) # → "12.34%"
Go
ratio := 0.1234
fmt.Sprintf("%.1f%%", ratio*100) // → "12.3%"
fmt.Sprintf("%.2f%%", ratio*100) // → "12.34%"
PHP
<?php
$ratio = 0.1234;
echo number_format($ratio * 100, 2) . '%'; // → "12.34%"
echo round($ratio * 100, 1) . '%'; // → "12.3%"
// With intl extension
$fmt = new NumberFormatter('en_US', NumberFormatter::PERCENT);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 2);
echo $fmt->format($ratio); // → "12.34%"
Compact notation (1.2M, 5.6K)
Compact numbers are essential for dashboards, analytics, and social share counts where space is limited.
JavaScript
const formatter = new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short',
});
formatter.format(1234); // → "1.2K"
formatter.format(1234567); // → "1.2M"
formatter.format(1234567890); // → "1.2B"
// With max digits
new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumSignificantDigits: 3,
}).format(1234567); // → "1.23M"
Python
def compact_number(n: float) -> str:
if abs(n) >= 1_000_000_000:
return f"{n/1_000_000_000:.1f}B"
elif abs(n) >= 1_000_000:
return f"{n/1_000_000:.1f}M"
elif abs(n) >= 1_000:
return f"{n/1_000:.1f}K"
return str(n)
print(compact_number(1234)) # → "1.2K"
print(compact_number(1234567)) # → "1.2M"
print(compact_number(1234567890)) # → "1.2B"
Go
func compactNumber(n float64) string {
switch {
case math.Abs(n) >= 1_000_000_000:
return fmt.Sprintf("%.1fB", n/1_000_000_000)
case math.Abs(n) >= 1_000_000:
return fmt.Sprintf("%.1fM", n/1_000_000)
case math.Abs(n) >= 1_000:
return fmt.Sprintf("%.1fK", n/1_000)
default:
return fmt.Sprintf("%.0f", n)
}
}
PHP
<?php
function compactNumber(float $n): string {
$abs = abs($n);
if ($abs >= 1_000_000_000) return round($n / 1_000_000_000, 1) . 'B';
if ($abs >= 1_000_000) return round($n / 1_000_000, 1) . 'M';
if ($abs >= 1_000) return round($n / 1_000, 1) . 'K';
return (string)$n;
}
echo compactNumber(1234567); // → "1.2M"
Fixed decimal places
Sometimes you just need exactly N decimal places — no locale, no symbols.
// JavaScript
(1.5).toFixed(2); // → "1.50" (returns string)
parseFloat((1.567).toFixed(2)); // → 1.57 (number)
# Python
f"{1.5:.2f}" # → "1.50"
round(1.567, 2) # → 1.57
// Go
fmt.Sprintf("%.2f", 1.5) // → "1.50"
<?php
number_format(1.5, 2); // → "1.50"
sprintf("%.2f", 1.5); // → "1.50"
6 common mistakes
1. Using .toFixed() for financial math
toFixed() returns a string, and floating-point rounding can surprise you: (1.005).toFixed(2) returns "1.00" in some JS engines, not "1.01". Use a decimal library (decimal.js, Python's Decimal) for money.
2. Hardcoding US-style separators
Inserting commas and periods manually with string manipulation breaks for non-US locales. Use Intl.NumberFormat, Babel, or the intl PHP extension.
3. Forgetting that Intl.NumberFormat multiplies percentages
If you format 0.5 with style: 'percent', you get 50%. Pass the ratio (0–1), not the percentage (0–100).
4. PHP number_format() silently truncates, not rounds
Actually it rounds — but the rounding uses the C round() function, which has quirks at the midpoint for even numbers. Verify your banker's rounding requirements explicitly.
5. Mixing notation: 'compact' with maximumFractionDigits: 0
1234 becomes "1K" instead of "1.2K". Be explicit about maximumSignificantDigits or maximumFractionDigits when using compact notation.
6. Python locale.setlocale() is global and not thread-safe
Setting locale at the module level affects every thread. Prefer Babel's format_number() / format_currency() for locale-aware formatting in web apps and APIs.
FAQ
Should I use Intl.NumberFormat or a library like numeral.js?
Prefer Intl.NumberFormat for modern apps — it's built in, covers 500+ locales, and is maintained by the browser. numeral.js is no longer actively maintained. Only add a library if you need features Intl doesn't support (like custom compact suffixes).
How do I format numbers in React?
Call Intl.NumberFormat (or toLocaleString) in your component or a utility function. Don't format in JSX directly — keep formatting logic in a formatters.ts helper for reuse and testability.
What's the difference between toLocaleString() and Intl.NumberFormat?
Number.prototype.toLocaleString() is a convenience wrapper around Intl.NumberFormat. For one-off calls they're equivalent. For repeated formatting, create a single Intl.NumberFormat instance and reuse it — it's 2–10× faster.
How do I handle very large numbers (BigInt)?
Intl.NumberFormat supports BigInt directly in modern environments. Pass the BigInt value the same way you'd pass a regular number.
How do I format negative numbers?
All the methods above handle negatives automatically. For accounting style (parentheses instead of minus sign), use currencySign: 'accounting' in Intl.NumberFormat.
How do I strip trailing zeros (1.50 → 1.5)?
In JS: parseFloat(n.toFixed(2)).toString(). In Python: f"{n:.2f}".rstrip('0').rstrip('.'). Or set minimumFractionDigits: 0 and maximumFractionDigits: 2 in Intl.NumberFormat.