format! and format specifications
In Rust println! is a macro around a format string, and the same language inside that string builds a String with format!. The placeholders are braces, the arguments follow the string, and the colon after the name opens a specification for width, precision, alignment and radix.
This is one system, not several: the same specification works in format!, println!, write! and the formatting implementation of a type. The lesson covers the parts a manual and a report need, and ends with the error a missing argument produces.
Placeholders and format!
Empty braces take the next argument, and braces with a name read a variable or a named argument. The two forms can be mixed, and a named argument is repeated by writing its name twice. The value is always rendered by the type, so an integer prints as digits and a string as text.
The program prints a line with three positional arguments and a line with two named ones. The output is the same shape as the template, with the values substituted in order.
fn main() {
let name = "Rust";
let version = 1;
println!("{} {} {}", name, version, 2024);
println!("{name} {version}");
}
rustc --edition 2024 placeholders.rs
./placeholders
Rust 1 2024
Rust 1
format! returns a String instead of printing, so the same template can be stored, sent or tested. The lesson on String uses it to build text.
Width, fill and alignment
{:>5} right-aligns in five columns, {:<5} left-aligns and {:^5} centres. The character before the arrow is the fill, so {:*^5} pads with asterisks instead of spaces. The brackets in the program make the padding visible.
The four lines show the same two letters in five columns with four different specifications. Width counts characters, not bytes, which is one of the places where the formatting system does the UTF-8 work for you.
fn main() {
println!("[{:>5}]", "ab");
println!("[{:<5}]", "ab");
println!("[{:^5}]", "ab");
println!("[{:*^5}]", "ab");
}
rustc --edition 2024 width.rs
./width
[ ab]
[ab ]
[ ab ]
[*ab**]
A width may also come from an argument: {:1$} takes it from the second argument, which is how a table with a computed column size is built without building the format string at run time.
Numbers in a format string
A precision after a dot rounds a float, a zero before the width pads with zeros, and a plus sign forces the sign on a positive number. The letters x, b and o print an integer in hexadecimal, binary and octal, in lower case unless the letter is upper case.
The program prints a rounded float, a padded integer, a signed integer and three radices of the same byte. The output is the four lines below, which is enough to see how each flag changes the text.
fn main() {
println!("{:.2}", 3.14159);
println!("{:05}", 42);
println!("{:+}", 42);
println!("{:x} {:b} {:o}", 255, 5, 8);
}
rustc --edition 2024 numbers.rs
./numbers
3.14
00042
+42
ff 101 10
A float with no precision prints the shortest representation that round-trips, which the lesson on numbers explains. A precision pins the digits for a report.
Debug and pretty debug
{:?} prints the debug form of a value, which a type gets from #[derive(Debug)]. {:#?} is the same information with line breaks and indentation, which is what a nested struct needs to be readable.
The program prints a point both ways and prints its fields normally first, so the derived form is not the only output. The pretty form spans several lines, and each field sits on its own.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{} {}", p.x, p.y);
println!("{:?}", p);
println!("{:#?}", p);
}
rustc --edition 2024 debug.rs
./debug
1 2
Point { x: 1, y: 2 }
Point {
x: 1,
y: 2,
}
The derive is an attribute, and the lesson on derive lists the traits that work the same way. Without it, the debug placeholder is E0277, which the first program in this course showed.
A text table
A table is a loop over rows and one format string with two widths: the name left-aligned in eight columns and the score right-aligned in four. The columns line up because the numbers are padded on the left and the names on the right.
The program formats three rows from an array of tuples. The same loop would write a file with write!, which the next lesson shows.
fn main() {
let rows = [("ada", 36), ("grace", 85), ("alan", 41)];
for (name, score) in rows {
println!("{:<8} {:>4}", name, score);
}
}
rustc --edition 2024 table.rs
./table
ada 36
grace 85
alan 41
The widths are constants in the format string, so a longer name would push the score column. A real report computes the width from the data, which is a loop over the rows before the loop that prints them.
A name that is not there
Braces with a name are an implicit capture from the enclosing scope. When no such variable exists, the compiler reports E0425 at the name inside the string, which looks odd until you remember that the macro expands to code that reads a variable.
The program below writes {name} without declaring it. The error points at the name inside the literal, and the fix is an argument or a variable with that name.
fn main() {
println!("{name}");
}
rustc --edition 2024 unknown_arg_err.rs
error[E0425]: cannot find value `name` in this scope
--> unknown_arg_err.rs:2:16
|
2 | println!("{name}");
| ^^^^ not found in this scope
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0425`.
The same message appears for a positional placeholder with too few arguments, with a different note that counts them. Both are caught at compile time, which is the advantage of a format string over a concatenation of values.
Exercises
- Print a price list with the name in ten columns and the price in eight, two decimals. Which specification does each column need?
- Print the same number in decimal, hexadecimal and binary in one line, each in eight columns.
- Use format! to build the line instead of printing it, and print the length of the result. How many characters did the padding add?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What do empty braces in a format string do?
Answer
Take the next argument in order
Each empty placeholder consumes the next positional argument.
-
What does
{:>5}do?Answer
Right-aligns the value in five columns
The arrow points right, so the padding goes on the left.
-
What does
{:.2}do to a float?Answer
Prints two digits after the decimal point
A precision after the dot fixes the number of fractional digits.
-
What does
{:#?}change compared with{:?}?Answer
It prints the value with line breaks and indentation
The hash flag selects the pretty debug form of the Debug implementation.
-
What does this program print?
fn main() { let name = "Rust"; let version = 1; println!("{} {} {}", name, version, 2024); }Answer
Rust 1 2024
The three positional placeholders take the three arguments in order.
-
What does this program print?
fn main() { println!("[{:>5}]", "ab"); println!("[{:<5}]", "ab"); println!("[{:^5}]", "ab"); }Answer
[ ab] / [ab ] / [ ab ]
Right alignment pads on the left, left alignment on the right, and centring splits the padding.
-
What does this program print?
fn main() { println!("{:.2}", 3.14159); println!("{:05}", 42); println!("{:x} {:b} {:o}", 255, 5, 8); }Answer
3.14 / 00042 / ff 101 10
The precision rounds, the zero pads to five, and the radix letters print in lower case.
-
What does this program print?
#[derive(Debug)] struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 1, y: 2 }; println!("{:?}", p); }Answer
Point { x: 1, y: 2 }
The derived Debug implementation prints the type name and each field with its name.
-
What does this program print?
fn main() { let rows = [("ada", 36), ("grace", 85)]; for (name, score) in rows { println!("{:<8} {:>4}", name, score); } }Answer
ada 36 / grace 85
Names occupy eight columns on the left and scores four on the right, which lines the two rows up.
-
What is the first line of the compiler output for this program?
fn main() { println!("{name}"); }Answer
error[E0425]: cannot find value
namein this scopeThe named placeholder reads a variable, and none with that name exists in the scope.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev