How to format a number with thousands separators
Short answer: Rust’s format spec has no thousands-separator flag — there is no {:,} to reach for, and nothing to enable. {:,} is what other languages use, and the Rust compiler names that habit in its error. You get separators by formatting the digits and inserting them yourself: a ten-line helper that also composes with the ordinary width and alignment flags.
From a fresh project — cargo new sep-demo, then replace src/main.rs with each block below.
There is no {:,} flag
Section titled “There is no {:,} flag”fn main() { println!("{:,}", 1234567);}error: invalid format string: python's numeric grouping `,` is not supported in rust format strings --> src/main.rs:2:17 |2 | println!("{:,}", 1234567); | - ^ expected `}` in format string | | | because of this opening brace | = note: to print `{`, you can escape it using `{{`
error: aborting due to 1 previous errorThe diagnostic is worth reading twice, because it settles the question completely: the comma is not part of the format grammar at all — the string fails to parse, before any type is involved. Python’s format(x, ",") and f-strings like f"{x:,}" are where most people learn the flag, and the Rust compiler recognises the muscle memory by name. There is no alternative spelling either: no grouping mode on Display, no option on the numeric types, and no locale machinery anywhere in the standard library. Formatting a number with separators means building that string yourself.
Format the digits, then split
Section titled “Format the digits, then split”The shape of the problem is easier to see on integers. Take the digits as a string, cut off the short head group that the first comma needs, then walk the rest three characters at a time:
fn group(digits: &str) -> String { let split = digits.len() % 3; let mut out: String = digits[..split].into(); for (i, c) in digits[split..].chars().enumerate() { if i % 3 == 0 && !out.is_empty() { out.push(','); } out.push(c); } out}
fn separated(n: i128) -> String { let grouped = group(&n.unsigned_abs().to_string()); if n < 0 { format!("-{grouped}") } else { grouped }}
fn main() { println!("{}", separated(999)); println!("{}", separated(1234567)); println!("{}", separated(-9876543)); println!("{}", separated(123456789012345678901234567890_i128));}9991,234,567-9,876,543123,456,789,012,345,678,901,234,567,890Two details carry the correctness. split_at(digits.len() % 3) splits off the head group — "1234567" has seven digits, so the head is one digit and the rest falls into complete threes; when the length is already a multiple of three the head is empty and the loop starts fresh. And unsigned_abs() keeps the minus sign out of the digits, so it never lands in the middle of a group — the sign is reattached as a single character at the end. i128 is wide enough that the function covers every integer type in the language; the last line above is larger than u64 can hold.
The separator is whatever you push: a space for European-style 1 234 567, an underscore matching Rust’s own 1_048_576 literal, one call away.
Floats: group the integer part, keep the fraction
Section titled “Floats: group the integer part, keep the fraction”The same group works for floats once the number is in fixed-point form — format it, then treat the two sides of the decimal point differently:
fn group(digits: &str) -> String { let split = digits.len() % 3; let mut out: String = digits[..split].into(); for (i, c) in digits[split..].chars().enumerate() { if i % 3 == 0 && !out.is_empty() { out.push(','); } out.push(c); } out}
fn separated(n: f64, places: usize) -> String { let sign = if n < 0.0 { "-" } else { "" }; let fixed = format!("{:.*}", places, n.abs()); match fixed.split_once('.') { Some((int, frac)) => format!("{sign}{}.{frac}", group(int)), None => format!("{sign}{}", group(&fixed)), }}
fn main() { println!("{}", separated(1234567.891, 2)); println!("{}", separated(-1234567.5, 1)); println!("{}", separated(42.0, 2));}1,234,567.89-1,234,567.542.00The {:.*} spec takes the decimal count from an argument, which is why one function handles any number of places; How to round a float to a number of decimals covers what that formatting does to the stored value. Comma grouping applies to the integer side only — nobody reads 1,234,567.891 with commas in the fraction. places: 0 produces no decimal point at all, and the split_once returning None is the branch that handles it.
Width and alignment still work
Section titled “Width and alignment still work”Because the helper returns a String, every ordinary format flag applies to the finished string — this is a right-aligned column:
fn group(digits: &str) -> String { let split = digits.len() % 3; let mut out: String = digits[..split].into(); for (i, c) in digits[split..].chars().enumerate() { if i % 3 == 0 && !out.is_empty() { out.push(','); } out.push(c); } out}
fn main() { let rows = [7, 42, 1999, 1_048_576]; for r in rows { println!("{:>12}", group(&r.to_string())); }} 7 42 1,999 1,048,576The commas count toward the width, so a column of grouped numbers lines up the same way an ungrouped one does. Fill, alignment and precision for other cases are in How to format strings with println and format — that page covers the rest of the spec; the comma is simply not in it.
If the grouping you need is not fixed comma-and-three — locale-specific separators, Indian digit grouping — the standard library has nothing, and the num-format crate is where that lives. For the common case, the helper above is the whole answer.
- How to compare floats without surprises — what
{:.2}above is actually printing; the previous article in this chapter. - How to round a float to a number of decimals — choosing the digits a formatted float shows.
- Working with numbers — the rest of the chapter.