Skip to content
Learn Rust
Theme

Printing and Formatting

Standard library · 5 of 5

println!, format! and write! share one mini-language inside the {}: width, alignment, precision, hex. Two traits decide how a value prints. Display ({}) is for people, and you write it yourself. Debug ({:?}) is for developers, and #[derive(Debug)] writes it for you.

Coming from Go{} uses Display, close to your String() method, and {:?} uses Debug, which is what %+v prints for a struct with no String(). Go has one hook (%v, %+v and %s all call String()); Rust has two traits, and {:?} never uses your Display. Go prints any struct by default, but Rust only prints types that implement one of these traits.

Coming from KotlinDisplay is your toString() override, and #[derive(Debug)] is like the toString a data class writes for you. A Rust struct has neither until you add one, and width and precision go inside {} ({n:>6}) instead of "%6d".format(n).

Coming from PythonThe mini-language is close to Python's: {n:>6}, {n:06}, {n:#x}, with differences the page lists. Display is __str__ and Debug is __repr__, but Rust has no default for either, and {:.2} means two decimal places, like Python's .2f, not two significant digits.

Coming from TypeScriptformat! is a template literal, and {n:>6} does what padStart(6) does. Display is your toString(), and {:?} is like console.log(obj), but only for types with #[derive(Debug)].

Coming from Javaimpl Display is your toString(), and #[derive(Debug)] is like the toString a record writes for you. The format string is checked at compile time, where String.format checks it at run time.

Coming from C#impl Display is overriding ToString(). #[derive(Debug)] is close to the ToString a record gives you, but only for {:?}: Display has no default. {n:>6} is {n,6}. C# checks the arguments of $"..." at compile time but not the letter after the colon ($"{n:Q}" fails at run time); Rust checks both.

Coming from C{:>6}, {:.2} and {:#x} cover what %6d, %.2f and %#x do, but the argument type comes from the value, so a letter that disagrees with the argument is a compile error. impl Display is your money_str function, and derive(Debug) writes the field-by-field print for you.

Coming from C++The {} mini-language is almost the same as C++20 std::format, and impl Display is a std::formatter specialization. The difference is #[derive(Debug)]: Rust writes a developer print for your struct, where C++ makes you write operator<< by hand.

The quick comparison

Rustsrc/main.rsRun ↗
use std::fmt;
struct Money {
cents: i64,
}
// Display: how Money looks with {}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.cents / 100, self.cents % 100)
}
}
#[derive(Debug)]
struct Item {
name: String,
qty: u32,
}
fn main() {
let n = 42;
println!("[{n:>6}] [{n:<6}] [{n:^6}] [{n:06}]");
println!("{:.2} {:e} {n:#x} {n:#b}", 3.14159, 1234.5);
let price = Money { cents: 1999 };
println!("total: {price}");
let label = format!("{price} each"); // format! builds a String
println!("{label} ({} chars)", label.len());
let item = Item {
name: "pen".to_string(),
qty: 3,
};
println!("{} x {}", item.qty, item.name);
println!("{item:?}"); // Debug, written by derive
println!("{item:#?}"); // pretty Debug
}
$ cargo run
[    42] [42    ] [  42  ] [000042]
3.14 1.2345e3 0x2a 0b101010
total: $19.99
$19.99 each (11 chars)
3 x pen
Item { name: "pen", qty: 3 }
Item {
    name: "pen",
    qty: 3,
}
package main
import "fmt"
type Money struct {
cents int64
}
// String() is Go's Display
func (m Money) String() string {
return fmt.Sprintf("$%d.%02d", m.cents/100, m.cents%100)
}
type Item struct {
Name string
Qty int
}
func main() {
n := 42
fmt.Printf("[%6d] [%-6d] [%06d]\n", n, n, n) // no centering verb in Go
fmt.Printf("%.2f %e %#x %#b\n", 3.14159, 1234.5, n, n)
price := Money{1999}
fmt.Println("total:", price)
label := fmt.Sprintf("%v each", price)
fmt.Printf("%s (%d chars)\n", label, len(label))
item := Item{"pen", 3}
fmt.Printf("%d x %s\n", item.Qty, item.Name)
fmt.Printf("%+v\n", item)
fmt.Printf("%#v\n", item)
}
$ go run main.go
[    42] [42    ] [000042]
3.14 1.234500e+03 0x2a 0b101010
total: $19.99
$19.99 each (11 chars)
3 x pen
{Name:pen Qty:3}
main.Item{Name:"pen", Qty:3}
class Money(val cents: Long) {
override fun toString() = "\$${cents / 100}.${"%02d".format(cents % 100)}"
}
data class Item(val name: String, val qty: Int)
fun main() {
val n = 42
println("[%6d] [%-6d] [%06d]".format(n, n, n))
println("%.2f %e %#x".format(3.14159, 1234.5, n) + " 0b" + n.toString(2))
val price = Money(1999)
println("total: $price")
val label = "$price each"
println("$label (${label.length} chars)")
val item = Item("pen", 3)
println("${item.qty} x ${item.name}")
println(item)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
[    42] [42    ] [000042]
3.14 1.234500e+03 0x2a 0b101010
total: $19.99
$19.99 each (11 chars)
3 x pen
Item(name=pen, qty=3)
from dataclasses import dataclass
from pprint import pprint
class Money:
def __init__(self, cents: int):
self.cents = cents
# __str__: how Money looks with print and f"{}"
def __str__(self) -> str:
return f"${self.cents // 100}.{self.cents % 100:02}"
@dataclass # writes __repr__ for you, like derive(Debug)
class Item:
name: str
qty: int
n = 42
print(f"[{n:>6}] [{n:<6}] [{n:^6}] [{n:06}]")
print(f"{3.14159:.2f} {1234.5:e} {n:#x} {n:#b}")
price = Money(1999)
print(f"total: {price}")
label = f"{price} each" # an f-string builds a str
print(f"{label} ({len(label)} chars)")
item = Item("pen", 3)
print(f"{item.qty} x {item.name}")
print(f"{item!r}") # repr, written by @dataclass
pprint(item, width=20) # pretty print, splits when it doesn't fit
$ python3 main.py
[    42] [42    ] [  42  ] [000042]
3.14 1.234500e+03 0x2a 0b101010
total: $19.99
$19.99 each (11 chars)
3 x pen
Item(name='pen', qty=3)
Item(name='pen',
     qty=3)
class Money {
constructor(public cents: number) {}
// toString: how Money looks inside `${}`
toString(): string {
return `$${Math.trunc(this.cents / 100)}.${String(this.cents % 100).padStart(2, "0")}`;
}
}
interface Item {
name: string;
qty: number;
}
// no format mini-language: padStart and padEnd do the work, centering is by hand
function center(s: string, width: number): string {
const left = Math.floor((width - s.length) / 2);
return s.padStart(s.length + left).padEnd(width);
}
const n = 42;
const s = String(n);
console.log(`[${s.padStart(6)}] [${s.padEnd(6)}] [${center(s, 6)}] [${s.padStart(6, "0")}]`);
console.log(`${(3.14159).toFixed(2)} ${(1234.5).toExponential()} 0x${n.toString(16)} 0b${n.toString(2)}`);
const price = new Money(1999);
console.log(`total: ${price}`);
const label = `${price} each`; // a template literal builds a string
console.log(`${label} (${label.length} chars)`);
const item: Item = { name: "pen", qty: 3 };
console.log(`${item.qty} x ${item.name}`);
console.log(item); // console.log shows the object's fields
console.log(JSON.stringify(item, null, 2)); // pretty, as JSON
$ tsc --strict --module nodenext --types node main.ts && node main.js
[    42] [42    ] [  42  ] [000042]
3.14 1.2345e+3 0x2a 0b101010
total: $19.99
$19.99 each (11 chars)
3 x pen
{ name: 'pen', qty: 3 }
{
  "name": "pen",
  "qty": 3
}
public class Main {
// toString is Java's Display
record Money(long cents) {
@Override
public String toString() {
return String.format("$%d.%02d", cents / 100, cents % 100);
}
}
// a record writes toString for you, like derive(Debug)
record Item(String name, int qty) {}
public static void main(String[] args) {
int n = 42;
System.out.printf("[%6d] [%-6d] [%06d]%n", n, n, n); // no centering flag
System.out.printf("%.2f %e %#x 0b%s%n", 3.14159, 1234.5, n, Integer.toBinaryString(n));
Money price = new Money(1999);
System.out.println("total: " + price);
String label = price + " each";
System.out.println(label + " (" + label.length() + " chars)");
Item item = new Item("pen", 3);
System.out.println(item.qty() + " x " + item.name());
System.out.println(item);
}
}
$ javac Main.java && java Main
[    42] [42    ] [000042]
3.14 1.234500e+03 0x2a 0b101010
total: $19.99
$19.99 each (11 chars)
3 x pen
Item[name=pen, qty=3]
var n = 42;
Console.WriteLine($"[{n,6}] [{n,-6}] [{n:D6}]"); // no centering
Console.WriteLine($"{3.14159:F2} {1234.5:e} 0x{n:x} 0b{n:b}");
var price = new Money(1999);
Console.WriteLine($"total: {price}");
var label = $"{price} each";
Console.WriteLine($"{label} ({label.Length} chars)");
var item = new Item("pen", 3);
Console.WriteLine($"{item.Qty} x {item.Name}");
Console.WriteLine(item); // records write ToString for you
// ToString is C#'s Display
record Money(long Cents)
{
public override string ToString() => $"${Cents / 100}.{Cents % 100:D2}";
}
record Item(string Name, int Qty);
$ dotnet run
[    42] [42    ] [000042]
3.14 1.234500e+003 0x2a 0b101010
total: $19.99
$19.99 each (11 chars)
3 x pen
Item { Name = pen, Qty = 3 }
#include <stdio.h>
struct Money {
long cents;
};
// C has no Display: write the text into a buffer the caller gives you
void money_str(char *buf, size_t size, struct Money m) {
snprintf(buf, size, "$%ld.%02ld", m.cents / 100, m.cents % 100);
}
struct Item {
const char *name;
unsigned qty;
};
int main(void) {
int n = 42;
printf("[%6d] [%-6d] [%06d]\n", n, n, n); // no centering in printf
// no %b for binary before C23
printf("%.2f %e %#x\n", 3.14159, 1234.5, n);
struct Money price = {1999};
char money[32];
money_str(money, sizeof money, price);
printf("total: %s\n", money);
// snprintf builds a string, and returns how long it is
char label[64];
int len = snprintf(label, sizeof label, "%s each", money);
printf("%s (%d chars)\n", label, len);
// no Debug either: print each field by hand
struct Item item = {"pen", 3};
printf("%u x %s\n", item.qty, item.name);
printf("Item { name: \"%s\", qty: %u }\n", item.name, item.qty);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
[    42] [42    ] [000042]
3.14 1.234500e+03 0x2a
total: $19.99
$19.99 each (11 chars)
3 x pen
Item { name: "pen", qty: 3 }
#include <format>
#include <iostream>
#include <string>
struct Money {
long cents;
};
// a std::formatter specialization: how Money looks with {}
template <>
struct std::formatter<Money> : std::formatter<std::string> {
auto format(const Money &m, std::format_context &ctx) const {
return std::format_to(ctx.out(), "${}.{:02}", m.cents / 100, m.cents % 100);
}
};
struct Item {
std::string name;
unsigned qty;
};
// no derive: operator<< is written by hand
std::ostream &operator<<(std::ostream &os, const Item &item) {
return os << "Item { name: \"" << item.name << "\", qty: " << item.qty << " }";
}
int main() {
int n = 42;
std::cout << std::format("[{:>6}] [{:<6}] [{:^6}] [{:06}]\n", n, n, n, n);
std::cout << std::format("{:.2f} {:e} {:#x} {:#b}\n", 3.14159, 1234.5, n, n);
Money price{1999};
std::cout << std::format("total: {}\n", price);
std::string label = std::format("{} each", price); // std::format builds a std::string
std::cout << std::format("{} ({} chars)\n", label, label.size());
Item item{"pen", 3};
std::cout << std::format("{} x {}\n", item.qty, item.name);
std::cout << item << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
[    42] [42    ] [  42  ] [000042]
3.14 1.234500e+03 0x2a 0b101010
total: $19.99
$19.99 each (11 chars)
3 x pen
Item { name: "pen", qty: 3 }

What's different

  • Inside {}: >6 right-aligns in 6 columns, <6 left, ^6 centers, 06 pads with zeros, .2 is decimal places, x/b/e are hex, binary and scientific, and # adds the 0x/0b prefix.
  • You can name variables right in the braces: {n:>6}, {price}. Expressions like {a + b} aren't allowed; pass them as arguments instead.
  • Implementing Display decides how your type prints with {}, much like toString() or __str__ in other languages. You also get .to_string() for free once Display exists. A width such as {:>8} reaches your fmt as data, and it is ignored unless your code uses it (f.pad).
  • write!(f, ...) inside fmt writes into the Formatter, which wraps the real destination. Display takes a Formatter instead of returning a String, so the text can go into stdout, a String or a buffer you own.
  • {:?} works on anything that implements Debug, and nearly every std type does: Vec, HashMap, Option, Result, tuples. Add #[derive(Debug)] to your own types and you're done.
  • eprintln! works like println! but prints to stderr. print! and eprint! don't add a newline.
  • The format string is checked at compile time. A wrong letter, a missing argument or a typo like {nmae} is a compile error. Go builds the mistake and writes it into the output, and C's printf has undefined behaviour; C++20 std::format checks at compile time too.
Gotcha

Printing your own struct with {}

Looks fine: in Go, Kotlin, Python, TypeScript, Java and C# it runs fine; in C it runs but gives a wrong answer. Rust won't compile it.

Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Broken in C. Rust catches it.C++ refuses this too. Same idea, different words.
Rustsrc/main.rs✗ won't compile
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{}", p);
}
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
p := Point{1, 2}
fmt.Println(p)
}
$ go run main.go✓ runs
{1 2}
data class Point(val x: Int, val y: Int)
fun main() {
val p = Point(1, 2)
println(p)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
Point(x=1, y=2)
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p)
$ python3 main.py✓ runs
Point(x=1, y=2)
class Point {
constructor(public x: number, public y: number) {}
}
const p = new Point(1, 2);
console.log(p);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
Point { x: 1, y: 2 }
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
Point p = new Point(1, 2);
System.out.println(p);
}
}
$ javac Main.java && java Main✓ runs
Point[x=1, y=2]
var p = new Point(1, 2);
Console.WriteLine(p);
record Point(int X, int Y);
$ dotnet run✓ runs
Point { X = 1, Y = 2 }
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point p = {1, 2};
printf("%d\n", p);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main! runs · wrong answer
main.c: In function ‘main’:
main.c:10:14: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘struct Point’ [-Wformat=]
   10 |     printf("%d\n", p);
      |             ~^     ~
      |              |     |
      |              int   struct Point
1
C++✗ refuses too

C++ refuses this too: std::format needs a std::formatter for Point, and g++ says it "must be specialized for each type being formatted".

#include <format>
#include <iostream>
struct Point {
int x;
int y;
};
int main() {
Point p{1, 2};
std::cout << std::format("{}\n", p);
}
In file included from main.cpp:1:
/usr/include/c++/14/format: In instantiation of ‘class std::__format::_Checking_scanner<char, Point>’:
/usr/include/c++/14/format:4213:4:   required from ‘consteval std::basic_format_string<_CharT, _Args>::basic_format_string(const _Tp&) [with _Tp = char [4]; _CharT = char; _Args = {Point&}]’
 4213 |           __scanner(_M_str);
      |           ^~~~~~~~~
main.cpp:11:29:   required from here
   11 |     std::cout << std::format("{}\n", p);
      |                  ~~~~~~~~~~~^~~~~~~~~~~
/usr/include/c++/14/format:4068:10: error: static assertion failed: std::formatter must be specialized for each type being formatted
 4068 |         (is_default_constructible_v<formatter<_Args, _CharT>> && ...),
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/format:4068:10: note: ‘std::is_default_constructible_v<std::formatter<Point, char> >’ evaluates to false
/usr/include/c++/14/format: In instantiation of ‘constexpr void std::__format::_Checking_scanner<_CharT, _Args>::_M_parse_format_spec(std::size_t) [with _Tp = Point; _OtherArgs = {}; _CharT = char; _Args = {Point}; std::size_t = long unsigned int]’:
/usr/include/c++/14/format:4085:33:   required from here
 4085 |                 _M_parse_format_spec<_Args...>(__id);
      |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~
main.cpp:11:29:   in ‘constexpr’ expansion of ‘std::basic_format_string<char, Point&>("{}\012")’
/usr/include/c++/14/format:4214:19:   in ‘constexpr’ expansion of ‘__scanner.std::__format::_Checking_scanner<char, Point>::std::__format::_Scanner<char>.std::__format::_Scanner<char>::_M_scan()’
/usr/include/c++/14/format:3955:30:   in ‘constexpr’ expansion of ‘((std::__format::_Scanner<char>*)this)->std::__format::_Scanner<char>::_M_on_replacement_field()’
/usr/include/c++/14/format:4007:15:   in ‘constexpr’ expansion of ‘((std::__format::_Scanner<char>*)this)->std::__format::_Scanner<char>::_M_format_arg(__id)’
/usr/include/c++/14/format:4098:38: error: use of deleted function ‘std::formatter<_Tp, _CharT>::formatter() [with _Tp = Point; _CharT = char]’
 4098 |               formatter<_Tp, _CharT> __f;
      |                                      ^~~
/usr/include/c++/14/format:178:7: note: declared here
  178 |       formatter() = delete; // No std::formatter specialization for this type.
      |       ^~~~~~~~~
/usr/include/c++/14/format:4098:38: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
 4098 |               formatter<_Tp, _CharT> __f;
      |                                      ^~~
/usr/include/c++/14/format:4099:42: error: ‘struct std::formatter<Point, char>’ has no member named ‘parse’
 4099 |               this->_M_pc.advance_to(__f.parse(this->_M_pc));
      |                                      ~~~~^~~~~
main.cpp: In function ‘int main()’:
main.cpp:11:29: error: call to consteval function ‘std::basic_format_string<char, Point&>("{}\012")’ is not a constant expression
   11 |     std::cout << std::format("{}\n", p);
      |                  ~~~~~~~~~~~^~~~~~~~~~~
/usr/include/c++/14/format: In instantiation of ‘static std::__format::_Arg_store<_Context, _Args>::_Element_t std::__format::_Arg_store<_Context, _Args>::_S_make_elt(_Tp&) [with _Tp = Point; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}; _Element_t = std::__format::_Arg_store<std::basic_format_context<std::__format::_Sink_iter<char>, char>, std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle>::_Element_t]’:
/usr/include/c++/14/format:3755:23:   required from ‘std::__format::_Arg_store<_Context, _Args>::_Arg_store(_Tp& ...) [with _Tp = {Point}; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}]’
 3755 |         : _M_args{_S_make_elt(__a)...}
      |                   ~~~~~~~~~~~^~~~~
/usr/include/c++/14/format:3805:14:   required from ‘auto std::make_format_args(_Args& ...) [with _Context = basic_format_context<__format::_Sink_iter<char>, char>; _Args = {Point}]’
 3805 |       return _Store(__fmt_args...);
      |              ^~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/format:4297:61:   required from ‘std::string std::format(format_string<_Args ...>, _Args&& ...) [with _Args = {Point&}; string = __cxx11::basic_string<char>; format_string<_Args ...> = basic_format_string<char, Point&>]’
 4297 |     { return std::vformat(__fmt.get(), std::make_format_args(__args...)); }
      |                                        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~
main.cpp:11:29:   required from here
   11 |     std::cout << std::format("{}\n", p);
      |                  ~~~~~~~~~~~^~~~~~~~~~~
/usr/include/c++/14/format:3733:25: error: static assertion failed: std::formatter must be specialized for the type of each format arg
 3733 |           static_assert(is_default_constructible_v<formatter<_Tq, _CharT>>,
      |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/format:3733:25: note: ‘std::is_default_constructible_v<std::formatter<Point, char> >’ evaluates to false
/usr/include/c++/14/format:3744:38: error: no matching function for call to ‘std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(Point&)’
 3744 |           basic_format_arg<_Context> __arg(__v);
      |                                      ^~~~~
/usr/include/c++/14/format:3480:9: note: candidate: ‘template<class _Tp>  requires  __formattable_with<_Tp, _Context, typename _Context::formatter_type<typename std::remove_const<_Tp>::type>, std::basic_format_parse_context<typename _Context::char_type> > std::basic_format_arg<_Context>::basic_format_arg(_Tp&) [with _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>]’
 3480 |         basic_format_arg(_Tp& __v) noexcept
      |         ^~~~~~~~~~~~~~~~
/usr/include/c++/14/format:3480:9: note:   template argument deduction/substitution failed:
/usr/include/c++/14/format:3480:9: note: constraints not satisfied
In file included from /usr/include/c++/14/compare:40,
                 from /usr/include/c++/14/array:38,
                 from /usr/include/c++/14/format:43:
/usr/include/c++/14/concepts: In substitution of ‘template<class _Tp>  requires  __formattable_with<_Tp, _Context, typename _Context::formatter_type<typename std::remove_const<_Tp>::type>, std::basic_format_parse_context<typename _Context::char_type> > std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(_Tp&) [with _Tp = std::basic_format_context<std::__format::_Sink_iter<char>, char>]’:
/usr/include/c++/14/format:3744:31:   required from ‘static std::__format::_Arg_store<_Context, _Args>::_Element_t std::__format::_Arg_store<_Context, _Args>::_S_make_elt(_Tp&) [with _Tp = Point; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}; _Element_t = std::__format::_Arg_store<std::basic_format_context<std::__format::_Sink_iter<char>, char>, std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle>::_Element_t]’
 3744 |           basic_format_arg<_Context> __arg(__v);
      |                                      ^~~~~
/usr/include/c++/14/format:3755:23:   required from ‘std::__format::_Arg_store<_Context, _Args>::_Arg_store(_Tp& ...) [with _Tp = {Point}; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}]’
 3755 |         : _M_args{_S_make_elt(__a)...}
      |                   ~~~~~~~~~~~^~~~~
/usr/include/c++/14/format:3805:14:   required from ‘auto std::make_format_args(_Args& ...) [with _Context = basic_format_context<__format::_Sink_iter<char>, char>; _Args = {Point}]’
 3805 |       return _Store(__fmt_args...);
      |              ^~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/format:4297:61:   required from ‘std::string std::format(format_string<_Args ...>, _Args&& ...) [with _Args = {Point&}; string = __cxx11::basic_string<char>; format_string<_Args ...> = basic_format_string<char, Point&>]’
 4297 |     { return std::vformat(__fmt.get(), std::make_format_args(__args...)); }
      |                                        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~
main.cpp:11:29:   required from here
   11 |     std::cout << std::format("{}\n", p);
      |                  ~~~~~~~~~~~^~~~~~~~~~~
/usr/include/c++/14/concepts:159:13:   required for the satisfaction of ‘constructible_from<_Tp, _Tp>’ [with _Tp = std::formatter<Point, char>]
/usr/include/c++/14/concepts:173:13:   required for the satisfaction of ‘move_constructible<_Tp>’ [with _Tp = std::formatter<Point, char>]
/usr/include/c++/14/concepts:178:13:   required for the satisfaction of ‘copy_constructible<_Tp>’ [with _Tp = std::formatter<Point, char>]
/usr/include/c++/14/concepts:275:13:   required for the satisfaction of ‘copyable<_Tp>’ [with _Tp = std::formatter<Point, char>]
/usr/include/c++/14/concepts:280:13:   required for the satisfaction of ‘semiregular<_Formatter>’ [with _Formatter = std::formatter<Point, char>]
/usr/include/c++/14/format:2526:13:   required for the satisfaction of ‘__formattable_with<_Tp, _Context, typename _Context::formatter_type<typename std::remove_const<_Tp>::type>, std::basic_format_parse_context<typename _Context::char_type> >’ [with _Tp = Point; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>]
/usr/include/c++/14/concepts:160:30: note: the expression ‘is_constructible_v<_Tp, _Args ...> [with _Tp = std::formatter<Point, char>; _Args = {std::formatter<Point, char>}]’ evaluated to ‘false’
  160 |       = destructible<_Tp> && is_constructible_v<_Tp, _Args...>;
      |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/format: In instantiation of ‘static std::__format::_Arg_store<_Context, _Args>::_Element_t std::__format::_Arg_store<_Context, _Args>::_S_make_elt(_Tp&) [with _Tp = Point; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}; _Element_t = std::__format::_Arg_store<std::basic_format_context<std::__format::_Sink_iter<char>, char>, std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle>::_Element_t]’:
/usr/include/c++/14/format:3755:23:   required from ‘std::__format::_Arg_store<_Context, _Args>::_Arg_store(_Tp& ...) [with _Tp = {Point}; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}]’
 3755 |         : _M_args{_S_make_elt(__a)...}
      |                   ~~~~~~~~~~~^~~~~
/usr/include/c++/14/format:3805:14:   required from ‘auto std::make_format_args(_Args& ...) [with _Context = basic_format_context<__format::_Sink_iter<char>, char>; _Args = {Point}]’
 3805 |       return _Store(__fmt_args...);
      |              ^~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/format:4297:61:   required from ‘std::string std::format(format_string<_Args ...>, _Args&& ...) [with _Args = {Point&}; string = __cxx11::basic_string<char>; format_string<_Args ...> = basic_format_string<char, Point&>]’
 4297 |     { return std::vformat(__fmt.get(), std::make_format_args(__args...)); }
      |                                        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~
main.cpp:11:29:   required from here
   11 |     std::cout << std::format("{}\n", p);
      |                  ~~~~~~~~~~~^~~~~~~~~~~
/usr/include/c++/14/format:3280:7: note: candidate: ‘std::basic_format_arg<_Context>::basic_format_arg() [with _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>]’
 3280 |       basic_format_arg() noexcept : _M_type(__format::_Arg_none) { }
      |       ^~~~~~~~~~~~~~~~
/usr/include/c++/14/format:3280:7: note:   candidate expects 0 arguments, 1 provided
/usr/include/c++/14/format:3223:11: note: candidate: ‘constexpr std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(const std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >&)’
 3223 |     class basic_format_arg
      |           ^~~~~~~~~~~~~~~~
/usr/include/c++/14/format:3223:11: note:   no known conversion for argument 1 from ‘Point’ to ‘const std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >&’
/usr/include/c++/14/format:3223:11: note: candidate: ‘constexpr std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >&&)’
/usr/include/c++/14/format:3223:11: note:   no known conversion for argument 1 from ‘Point’ to ‘std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >&&’
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0277?
error[E0277]: `Point` doesn't implement `std::fmt::Display`
 --> src/main.rs:8:20
  |
8 |     println!("{}", p);
  |               --   ^ `Point` cannot be formatted with the default formatter
  |               |
  |               required by this formatting parameter
  |
help: the trait `std::fmt::Display` is not implemented for `Point`
 --> src/main.rs:1:1
  |
1 | struct Point {
  | ^^^^^^^^^^^^
  = note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
What happens in Go

Go runs this and prints {1 2}. Every struct has a default format, so you never have to write one.

What happens in Kotlin

Kotlin runs this and prints Point(x=1, y=2), because a data class writes toString for you. A plain class would print something like Point@1b6d3586.

What happens in Python

Python prints Point(x=1, y=2), because @dataclass writes a __repr__ for you. A plain class without one prints something like <__main__.Point object at 0x7f...>.

What happens in TypeScript

Prints Point { x: 1, y: 2 }. console.log shows any object's fields. Putting it in a template literal instead, ${p}, would print [object Object].

What happens in Java

A record writes toString for you, so this prints Point[x=1, y=2]. A plain class would print something like Point@1b6d3586 unless you override toString.

What happens in C#

A record writes ToString for you, so this prints Point { X = 1, Y = 2 }. A plain class would only print its type name, Point, unless you override ToString.

What happens in C

gcc with -Wall (the runner's flag) warns that %d wants an int, not a struct Point, but builds it anyway; with no flags it says nothing. It printed 1 here, just the bits of x; passing the wrong type to printf is undefined behaviour.

What happens in C++

C++ refuses this too: std::format needs a std::formatter for Point, and g++ says it "must be specialized for each type being formatted".

What Rust sees

{} uses the Display trait, and Rust never writes Display for you. It can't guess whether a point should look like (1, 2), 1,2 or x=1 y=2.

Why Rust says no

Display is the version your users see, so Rust makes you write it. For a quick look while debugging, #[derive(Debug)] plus {:?} gives you Point { x: 1, y: 2 } with one line of code. For real output, implement Display, as in the fix.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
use std::fmt;
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{}", p);
}
$ cargo run
(1, 2)

The toolkit

Macros, attributes and naming owns the format-string syntax: {}, {:?}, {0}, {name}, width, precision and {{. This page covers what is behind it: which trait each letter calls, how to write those traits for your own types, and where the text goes. Outputs come from Linux on 64-bit ARM with Rust 1.99.1, a debug build.

One trait for each letter

Every {...} calls a method of a trait on the value, and the letter after the colon picks the trait. A type prints that way only if it implements the trait.

You write Trait Meant for
{} Display people
{:?} {:#?} Debug programmers; # spreads it over lines
{:x} {:X} LowerHex UpperHex hexadecimal
{:o} {:b} Octal Binary base 8, base 2
{:e} {:E} LowerExp UpperExp scientific notation
{:p} Pointer an address
Rustsrc/main.rsRun ↗
fn main() {
let n = 255;
println!("Display {{}}: {}", n);
println!("Debug {{:?}}: {:?}", "hi");
println!("LowerHex {{:x}}: {:x}", n);
println!("UpperHex {{:X}}: {:X}", n);
println!("Octal {{:o}}: {:o}", n);
println!("Binary {{:b}}: {:b}", n);
println!("LowerExp {{:e}}: {:e}", 1234.5);
println!("UpperExp {{:E}}: {:E}", 1234.5);
println!("Debug in hex {{:x?}}: {:x?}", vec![255, 16]);
let address = format!("{:p}", &n);
println!(
"Pointer {{:p}}: starts with 0x? {}",
address.starts_with("0x")
);
}
$ cargo run
Display  {}:   255
Debug    {:?}: "hi"
LowerHex {:x}:  ff
UpperHex {:X}:  FF
Octal    {:o}:  377
Binary   {:b}:  11111111
LowerExp {:e}:  1.2345e3
UpperExp {:E}:  1.2345E3
Debug in hex {:x?}: [ff, 10]
Pointer  {:p}:  starts with 0x? true

The program prints only whether the {:p} text starts with 0x, because the address is different on every run. {:x?} is Debug with integers in hex. An integer has every trait in the table except Pointer, which is for references and pointers. The letters are a closed set: {:K} is an unknown format trait, and {:p} on an integer is E0277:

Rustsrc/main.rs✗ won't compile
fn main() {
println!("{:K}", 1.5); // no such trait
println!("{:p}", 255); // an integer is not a pointer
}
$ cargo build
error: unknown format trait `K`
 --> src/main.rs:2:17
  |
2 |     println!("{:K}", 1.5); // no such trait
  |                 ^
  |
  = note: the only appropriate formatting traits are:
          - ``, which uses the `Display` trait
          - `?`, which uses the `Debug` trait
          - `e`, which uses the `LowerExp` trait
          - `E`, which uses the `UpperExp` trait
          - `o`, which uses the `Octal` trait
          - `p`, which uses the `Pointer` trait
          - `b`, which uses the `Binary` trait
          - `x`, which uses the `LowerHex` trait
          - `X`, which uses the `UpperHex` trait

error[E0277]: the trait bound `{integer}: std::fmt::Pointer` is not satisfied
 --> src/main.rs:3:22
  |
3 |     println!("{:p}", 255); // an integer is not a pointer
  |               ----   ^^^ the trait `std::fmt::Pointer` is not implemented for `{integer}`
  |               |
  |               required by this formatting parameter

Writing Display

impl Display is one method, fmt, which gets a &mut Formatter (named f): the destination for the text, plus the spec, everything after the colon (>8, .2, #x). It also gives you to_string(), through a blanket impl (Common traits); the ToString docs say to implement Display, not ToString. What you can do with the Formatter:

Call What it does
f.write_str("text") writes the text exactly as given
write!(f, "{}-{}", a, b) formats, then writes; returns fmt::Result, so end with it or add ?
f.pad("text") writes text after applying width, fill, alignment and precision
Display::fmt(&self.0, f) lets another value print itself, with the caller’s spec
f.width() f.precision() f.fill() f.align() f.alternate() f.sign_plus() f.sign_aware_zero_pad() read the spec
f.debug_struct("Name"), debug_tuple, debug_list, debug_map, debug_set builders for Debug

Each trait in the first table has its own fmt. This LowerHex reads the # flag:

Rustsrc/main.rsRun ↗
use std::fmt;
struct Rgb(u8, u8, u8);
impl fmt::LowerHex for Rgb {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if f.alternate() {
f.write_str("#")?;
}
write!(f, "{:02x}{:02x}{:02x}", self.0, self.1, self.2)
}
}
fn main() {
let orange = Rgb(255, 128, 0);
println!("{:x}", orange);
println!("{:#x}", orange);
// Rgb has no Display, no Binary and no UpperHex, so {} or {:X} would not compile.
println!("length of {{:x}}: {}", format!("{:x}", orange).len());
}
$ cargo run
ff8000
#ff8000
length of {:x}: 6

To see the spec your code receives, print it:

Rustsrc/main.rsRun ↗
use std::fmt::{self, Alignment, Display, Formatter};
struct Spec;
impl Display for Spec {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let align = match f.align() {
Some(Alignment::Left) => "left",
Some(Alignment::Right) => "right",
Some(Alignment::Center) => "center",
None => "none",
};
write!(
f,
"width={:?} fill={:?} align={} precision={:?} plus={} alt={} zero={}",
f.width(),
f.fill(),
align,
f.precision(),
f.sign_plus(),
f.alternate(),
f.sign_aware_zero_pad()
)
}
}
fn main() {
println!("{}", Spec);
println!("{:<5}", Spec);
println!("{:*^+#012.3}", Spec);
}
$ cargo run
width=None fill=' ' align=none precision=None plus=false alt=false zero=false
width=Some(5) fill=' ' align=left precision=None plus=false alt=false zero=false
width=Some(12) fill='*' align=center precision=Some(3) plus=true alt=true zero=true

{:*^+#012.3} is fill *, centered, a sign, #, a 0 flag, width 12, precision 3. Nothing applies any of it: it is data, and each fmt decides what it means.

Width is your job

Common traits showed the surprise; this is the cause. Padding is not applied to the text your fmt writes. write!(f, "#{}", self.0) starts a new format with an empty spec, so the caller’s {:>8} is in f and nothing reads it. Three types, three behaviours:

Rustsrc/main.rsRun ↗
use std::fmt::{self, Display, Formatter};
struct Plain(u32);
struct Padded(u32);
struct Forward(f64);
impl Display for Plain {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "#{}", self.0) // a new, empty format spec: the caller's is dropped
}
}
impl Display for Padded {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(&format!("#{}", self.0)) // applies width, fill, alignment and precision
}
}
impl Display for Forward {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(&self.0, f) // hands the caller's spec to the f64
}
}
fn main() {
println!("write!: [{:>8}] [{:<8}]", Plain(42), Plain(42));
println!(
"pad: [{:>8}] [{:<8}] [{:*^8}]",
Padded(42),
Padded(42),
Padded(42)
);
println!("pad, .2: [{:.2}]", Padded(12345));
println!(
"forward: [{:8.2}] [{:+.1}]",
Forward(3.14159),
Forward(3.14159)
);
println!("to_string: [{}]", Padded(42).to_string());
}
$ cargo run
write!:   [#42] [#42]
pad:      [     #42] [#42     ] [**#42***]
pad, .2:  [#1]
forward:  [    3.14] [+3.1]
to_string: [#42]

Plain ignores the width. Padded uses f.pad, which applies width, fill and alignment, and also precision: {:.2} cuts the text to two characters, which is why #12345 became #1. Forward hands the spec to its f64, so 8.2 and +.1 work for free. Use pad for text-like output and forwarding for a wrapper around a number. To pad several pieces as a whole, build the String first and then call pad.

Debug has the same rule. The std::fmt docs say “alignment might not be implemented by some types. In particular, it is not generally implemented for the Debug trait”, and suggest formatting first, then padding the result:

Rustsrc/main.rsRun ↗
fn main() {
println!("[{:>8}]", "ab");
println!("[{:>8?}]", "ab");
println!("[{:5?}]", Some(1));
println!("[{:>10}]", format!("{:?}", Some(1)));
}
$ cargo run
[      ab]
["ab"]
[Some(    1)]
[   Some(1)]

The derived Debug for Some(1) passes the spec to the 1 inside, so {:5?} pads the number and not the whole value.

A list: pass the spec on

A Vec has no Display, because there is no single obvious way to print a list:

Rustsrc/main.rs✗ won't compile
fn main() {
let scores = vec![90, 85];
println!("scores: {}", scores);
}
$ cargo buildWhat is E0277?
error[E0277]: `Vec<{integer}>` doesn't implement `std::fmt::Display`
 --> src/main.rs:3:28
  |
3 |     println!("scores: {}", scores);
  |                       --   ^^^^^^ `Vec<{integer}>` cannot be formatted with the default formatter
  |                       |
  |                       required by this formatting parameter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Vec<{integer}>`
  = note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead

For a quick look, {:?} works, and Debug on a slice already passes the spec to every item. For text in your own shape, join strings:

Rustsrc/main.rsRun ↗
fn main() {
let scores = [1.0, 2.26, 3.0];
println!("{:?}", scores);
println!("{:.1?}", scores); // Debug on a slice passes the spec to each item
let words: Vec<String> = scores.iter().map(|s| format!("{s:.1}")).collect();
println!("{}", words.join(", "));
}
$ cargo run
[1.0, 2.26, 3.0]
[1.0, 2.3, 3.0]
1.0, 2.3, 3.0

Those are the everyday answers. A Display wrapper is for a list inside a larger format! that takes the caller’s spec. You cannot impl it for Vec (neither Display nor Vec is yours: the orphan rule), so wrap a slice. The loop forwards the spec to every item with item.fmt(f), and ? hands a failed write back:

Rustsrc/main.rsRun ↗
use std::fmt::{self, Display, Formatter};
struct List<'a, T>(&'a [T]);
impl<T: Display> Display for List<'_, T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
for (i, item) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
item.fmt(f)?; // every item gets the caller's spec
}
Ok(())
}
}
fn main() {
println!("[{}]", List(&[1.5, 2.25, 3.0]));
println!("[{:.1}]", List(&[1.5, 2.26, 3.0]));
println!("[{:>4}]", List(&[1, 22, 333]));
println!("[{}]", List::<i32>(&[]));
}
$ cargo run
[1.5, 2.25, 3]
[1.5, 2.3, 3.0]
[   1,   22,  333]
[]

{:.1} and {:>4} reached every item, because each f64 or i32 received the spec. write!(f, "{}", item) would have lost both.

Debug: derive it, or build it

#[derive(Debug)] is the default choice, and the std docs say “generally speaking, you should just derive a Debug implementation”. Write it by hand when a field should not appear. The builders print the right shape and make {:#?} work for free:

Rustsrc/main.rsRun ↗
use std::fmt;
struct Account {
id: u32,
owner: String,
password: String,
}
impl fmt::Debug for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Account")
.field("id", &self.id)
.field("owner", &self.owner)
.finish_non_exhaustive()
}
}
fn main() {
let a = Account {
id: 7,
owner: "ada".to_string(),
password: "hunter2".to_string(),
};
println!("{:?}", a);
println!("{:#?}", a);
println!(
"password printed: {}",
format!("{a:?}").contains(&a.password)
);
}
$ cargo run
Account { id: 7, owner: "ada", .. }
Account {
    id: 7,
    owner: "ada",
    ..
}
password printed: false

finish_non_exhaustive() ends with .. so a reader knows a field was left out. {:?} on text shows its escapes, which makes invisible differences visible:

Rustsrc/main.rsRun ↗
fn main() {
println!("{}", "tab:\t quote:\" newline:\n end");
println!("{:?}", "tab:\t quote:\" newline:\n end");
println!("{:?} {:?} {:?}", 'a', '\'', '\u{e9}');
println!("{:?}", "it's");
println!("{:?}", "e\u{301} \u{200b} \u{7f}");
println!("{:?} {:?}", "日本", String::from("smile \u{1F600}"));
println!("{} {:?}", 1.0, 1.0);
println!("{} {:?}", 1e21, 1e21);
}
$ cargo run
tab:	 quote:" newline:
 end
"tab:\t quote:\" newline:\n end"
'a' '\'' 'é'
"it's"
"e\u{301} \u{200b} \u{7f}"
"日本" "smile 😀"
1 1.0
1000000000000000000000 1e21

Tab, newline, control characters, the zero-width space U+200B and the combining accent after the e are escaped, and so is " in a string (a ' only in a char), while é, 日本 and the emoji print as themselves. A float with no fraction prints as 1.0 in Debug and 1 in Display; 1e21 is 1e21 in Debug and all its digits in Display. A later section says why not to depend on the exact form.

Numbers

The spec letters ({:08.3}, {:+}, {:#x}) are in the earlier lesson. A precision cuts text and an integer ignores it ({:.3} of "Hello" is Hel, of 7 it is 7). Zeros go after the sign ({:08.3} of -3.14159 is -003.142), and a bool pads like text. {:x} of a negative integer shows the bits of its type (-1i8 is ff, -255i32 is ffffff01), and {:e} of 1234.5 is 1.2345e3. check-table.rs asserts each of these. Floats have the odd cases:

Rustsrc/main.rsRun ↗
fn main() {
let sum = 0.1 + 0.2;
println!("{{}}: {}", sum);
println!("{{:.1}}: {:.1}", sum);
println!(
"round trip: {}",
sum.to_string().parse::<f64>().unwrap() == sum
);
println!("{} {} {}", f64::NAN, f64::INFINITY, -f64::INFINITY);
println!("{} {:?}", -0.0, -0.0);
println!("{{:e}}: {:e} {:e}", 1234.5, 0.00012);
for x in [0.5, 1.5, 2.5, 3.5, -0.5] {
print!("{:.0} ", x);
}
println!("<- {{:.0}}");
for x in [0.125, 0.375, 2.675, 1.005] {
print!("{:.2} ", x);
}
println!("<- {{:.2}}");
println!("2.675 is really {:.20}", 2.675);
println!("{}", 1234567.5);
}
$ cargo run
{}:   0.30000000000000004
{:.1}: 0.3
round trip: true
NaN inf -inf
-0 -0.0
{:e}: 1.2345e3 1.2e-4
0 2 2 4 -0 <- {:.0}
0.12 0.38 2.67 1.00 <- {:.2}
2.675 is really 2.67499999999999982236
1234567.5

0.30000000000000004 is what 0.1 + 0.2 produces in f64, not a formatting fault: {} prints enough digits to parse back to the same value (the third line checks it). NaN and the infinities print as words, and -0.0 and {:.0} of -0.5 keep their sign. For rounding, the std docs say “Rust uses round half-to-even”: 0.5 and 2.5 give 0 and 2, 1.5 and 3.5 give 2 and 4, and the exact ties 0.125 and 0.375 give 0.12 and 0.38. 2.675 gives 2.67 because it is not a tie: the nearest f64 is a little below it (the last line prints 20 digits). Languages differ here: Java, Kotlin and JavaScript’s toFixed round 0.5, 1.5, 2.5 to 1 2 3, while Go, Python, C, C++ and C# print 0 2 2 like Rust (each language’s tab below has its run).

Width and precision from variables

The earlier lesson showed {:>w$} and {:.p$}. There are three more forms: 1$ takes the width from argument 1, .* takes the precision from the next argument before the value, and a named argument (width = 4) works too. The common use is a column as wide as its widest entry:

Rustsrc/main.rsRun ↗
fn main() {
let names = ["ada", "grace", "alan"];
let width = names.iter().map(|n| n.len()).max().unwrap(); // the widest name
for (i, name) in names.iter().enumerate() {
let score = 2.0 * i as f64 + 0.3;
println!("{i:>2} | {name:<width$} | {score:>6.1}");
}
let (w, p, x) = (9, 2, 3.14159);
println!("[{x:>w$.p$}]"); // width and precision from variables
println!("[{:>1$}]", 42, 6); // width is argument 1
println!("[{:.*}]", 3, 2.71828); // .* takes the precision first, then the value
println!("[{:>width$}]", 9, width = 4); // a named argument
}
$ cargo run
 0 | ada   |    0.3
 1 | grace |    2.3
 2 | alan  |    4.3
[     3.14]
[    42]
[2.718]
[   9]

Only a plain name can be captured, not {a + b} or {p.x}; compute it first. A .* uses two arguments, which shifts the count for the {} after it.

Where the text goes

Macro Destination
print! println! standard output. Panics if the write fails (the docs)
eprint! eprintln! standard error
format! a new String
write! writeln! anything with a write_fmt method: a String, a Vec<u8>, a file, a locked stdout. Returns a Result
dbg! standard error, with file, line and expression; returns the value
panic! assert! todo! the message is a format string too
Rustsrc/main.rsRun ↗
use std::fmt::Write as _; // for write! into a String
use std::io::Write as _; // for write! into bytes, files, stdout
fn main() {
let mut text = String::new();
write!(text, "{}-{}", 1, 2).unwrap();
writeln!(text, "!").unwrap();
println!("String: {:?}", text);
let mut bytes: Vec<u8> = Vec::new();
write!(bytes, "{}-{}", 1, 2).unwrap();
println!("Vec<u8>: {:?}", bytes);
let mut out = std::io::stdout().lock();
writeln!(out, "stdout: through write!").unwrap();
writeln!(out, "also stdout").unwrap();
drop(out); // release the lock so that other threads can print
eprintln!("stderr: eprintln!");
println!("done");
}
$ cargo run
String: "1-2!\n"
Vec<u8>: [49, 45, 50]
stdout: through write!
also stdout
stderr: eprintln!
done

A String and a byte destination need different traits, and both are called Write: std::fmt::Write writes text, std::io::Write writes bytes. Import the one you need, or both with as _, which brings in the methods without taking the name. Forget it and you get this:

Rustsrc/main.rs✗ won't compile
fn main() {
let mut s = String::new();
write!(s, "n = {}", 42).unwrap();
println!("{s}");
}
$ cargo buildWhat is E0599?
error[E0599]: cannot write into `String`
 --> src/main.rs:3:12
  |
3 |     write!(s, "n = {}", 42).unwrap();
  |            ^
  |
note: must implement `io::Write`, `fmt::Write`, or have a `write_fmt` method
 --> src/main.rs:3:12
  |
3 |     write!(s, "n = {}", 42).unwrap();
  |            ^
  = help: items from traits can only be used if the trait is in scope
help: trait `Write` which provides `write_fmt` is implemented but not in scope; perhaps you want to import it
  |
1 + use std::fmt::Write;
  ... (notes pointing into the standard library trimmed)

The runner shows both streams together. Buffering and flush are in Files and I/O. dbg! takes ownership of its argument, hence dbg!(&name). The std docs say it “works exactly the same in release builds” and that its exact output “should not be relied upon”: a quick look, not logging.

Rustsrc/main.rsRun ↗
fn main() {
let n = dbg!(2 + 3) * 10; // prints, and hands the value back
println!("n = {n}");
let name = String::from("ada");
dbg!(&name); // dbg!(name) would move it
dbg!(name.len(), &name);
let v = vec![1, 2];
dbg!(&v);
}
$ cargo run
[src/main.rs:2:13] 2 + 3 = 5
n = 50
[src/main.rs:6:5] &name = "ada"
[src/main.rs:7:5] name.len() = 3
[src/main.rs:7:5] &name = "ada"
[src/main.rs:10:5] &v = [
    1,
    2,
]

format_args!, and what format! costs

format! builds a String, which keeps its text on the heap, so a non-empty format! allocates. write! into a buffer with room does not, and format_args! builds only a description of the text (a fmt::Arguments), which println! and write! are made from. This counts calls to the allocator, which are not time:

Rustsrc/main.rsRun ↗
use std::alloc::{GlobalAlloc, Layout, System};
use std::fmt::{Arguments, Write as _};
use std::io::Write as _;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
// Counts calls to the system allocator. (`unsafe impl` is boilerplate here.)
struct Counting;
static CALLS: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
CALLS.fetch_add(1, Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static COUNTING: Counting = Counting;
fn count(work: impl FnOnce()) -> usize {
let before = CALLS.load(Relaxed);
work();
CALLS.load(Relaxed) - before
}
// Writes the description out; no String is ever built.
fn pass_on(args: Arguments) {
std::io::sink().write_fmt(args).unwrap();
}
fn main() {
let n = 42;
let mut empty = String::new();
let mut room = String::with_capacity(64);
println!(
"format!(\"n = {{n}}\"): {}",
count(|| drop(format!("n = {n}")))
);
println!(
"format!(\"{{n:>200}}\"): {}",
count(|| drop(format!("{n:>200}")))
);
println!(
"write! into String::new(): {}",
count(|| write!(empty, "n = {n}").unwrap())
);
println!(
"write! with_capacity(64): {}",
count(|| write!(room, "n = {n}").unwrap())
);
println!(
"pass format_args! on: {}",
count(|| pass_on(format_args!("n = {n}")))
);
}
$ cargo run
format!("n = {n}"):        1
format!("{n:>200}"):       6
write! into String::new(): 1
write! with_capacity(64):  0
pass format_args! on:      0

format! allocated once for a short text and six times for {n:>200}, where the padding made the String grow again (this run’s count, not a promise). write! allocated once into String::new() and never into a String with room. The last line passes a format_args! to a function that writes it to io::sink(): 0, so a function can take fmt::Arguments and pass it on with no String built. (Creating one alone could not fail the count, so the program measures the passing.) A logging wrapper forwards its arguments the same way (the earlier lesson left that for here):

Rustsrc/main.rsRun ↗
use std::fmt;
// Takes the format string and its arguments, not a finished String.
fn log(level: &str, args: fmt::Arguments) {
println!("[{level}] {args}");
}
macro_rules! info {
($($arg:tt)*) => { log("info", format_args!($($arg)*)) };
}
fn main() {
log("warn", format_args!("disk at {}%", 91));
info!("user {} logged in from {}", "ada", "10.0.0.7");
info!("{:>6.2}|{:<6}|", 3.14159, "ab");
}
$ cargo run
[warn] disk at 91%
[info] user ada logged in from 10.0.0.7
[info]   3.14|ab    |

Translating from what you know

Going deeper

What the macro hands to your code

The format string is checked at compile time, but the text is made at run time. The macro builds a fmt::Arguments, which the docs call a “precompiled version of a format string and its arguments”, and at run time each value’s trait method is called with its spec in a Formatter. So a trait that a type lacks is a compile error, and a spec that a trait ignores is not.

For a one-off view of a value you do not need a new type: fmt::from_fn (stable since Rust 1.93) turns a closure into a value that implements Display and Debug:

Rustsrc/main.rsRun ↗
use std::fmt;
struct Matrix([[i32; 2]; 2]);
impl Matrix {
// A value that prints a row, without a new type and impl for it.
fn row(&self, i: usize) -> impl fmt::Display + '_ {
fmt::from_fn(move |f| write!(f, "{} {}", self.0[i][0], self.0[i][1]))
}
}
fn main() {
let m = Matrix([[1, 2], [3, 4]]);
println!("row 0: {}", m.row(0));
println!("row 1: {}", m.row(1));
println!("as string: {:?}", m.row(1).to_string());
}
$ cargo run
row 0: 1 2
row 1: 3 4
as string: "3 4"

When fmt returns an error

fmt::Result lets a destination whose write fails cancel the formatting. The fmt::Error docs say an impl “should not return this Error unless they received it from their Formatter”. Return one anyway and write! into a String hands it back, while to_string() panics, because for a String the only possible failure is a bug in your Display:

Rustsrc/main.rs! panics at runtimeRun ↗
use std::fmt::{self, Write};
struct Broken;
impl fmt::Display for Broken {
fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error) // nothing went wrong with the destination: this is a bug
}
}
fn main() {
let mut text = String::new();
println!("write! returned: {:?}", write!(text, "{}", Broken));
println!("{}", Broken.to_string());
}
$ cargo run
write! returned: Err(Error)

thread 'main' (1) panicked at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/alloc/src/string.rs:2953:14:
a Display implementation returned an error unexpectedly: Error
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Debug is not a file format

The std docs for Debug say: “Derived Debug formats are not stable, and so may change with future Rust versions. Additionally, Debug implementations of types provided by the standard library (std, core, alloc, etc.) are not stable”. So parsing {:?} output, or saving it for comparison, can break on a compiler upgrade. When the text is a format that another program reads, use a library such as serde (Serde), and use Display for text that you promise to keep.

No locale

Some languages change formatting by the machine’s language settings: C’s printf follows setlocale (man page), C# follows CultureInfo.CurrentCulture, and Java’s String.format uses a Locale. The C and C# behaviour is from their docs, not run (this image has no German locale or culture data), so there a 1.5 may print as 1,5. Rust’s formatting does not. The docs say the format functions “do not have any concept of locale and will produce the same results on all systems regardless of user configuration”. There is no thousands separator and no way to choose a decimal comma. People use crates for that, num-format for grouping and ICU4X for full locale data; this page has not run either.

Practice

Try it

Each exercise has checks in main: replace the todo!() and run until it prints all checks passed. The starters compile and fail when run.

A tag that pads

Tag(42) must print as #42, and {:>8}, {:<6} and {:*^7} must work on it. Text longer than the width is not cut. Writing it with write! compiles and fails the checks. (f.pad also cuts to a precision, and building a String per print allocates; both are fine here.)

Starterexercises/tag.rs
use std::fmt;
/// Prints as `#<number>`, for example `#42`.
struct Tag(u32);
impl fmt::Display for Tag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
todo!("print #<number>, but obey the width, fill and alignment the caller asked for")
}
}
fn main() {
assert_eq!(Tag(42).to_string(), "#42");
assert_eq!(format!("[{:>8}]", Tag(42)), "[ #42]");
assert_eq!(format!("[{:<6}]", Tag(7)), "[#7 ]");
assert_eq!(format!("[{:*^7}]", Tag(7)), "[**#7***]");
assert_eq!(format!("[{:3}]", Tag(12345)), "[#12345]"); // wider than the width: not cut
assert_eq!(format!("{:<6}|{:>6}|", Tag(1), Tag(22)), "#1 | #22|");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/tag.solution.rsRun ↗
use std::fmt;
/// Prints as `#<number>`, for example `#42`.
struct Tag(u32);
impl fmt::Display for Tag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(&format!("#{}", self.0))
}
}
fn main() {
assert_eq!(Tag(42).to_string(), "#42");
assert_eq!(format!("[{:>8}]", Tag(42)), "[ #42]");
assert_eq!(format!("[{:<6}]", Tag(7)), "[#7 ]");
assert_eq!(format!("[{:*^7}]", Tag(7)), "[**#7***]");
assert_eq!(format!("[{:3}]", Tag(12345)), "[#12345]"); // wider than the width: not cut
assert_eq!(format!("{:<6}|{:>6}|", Tag(1), Tag(22)), "#1 | #22|");
println!("all checks passed");
}
$ cargo run
all checks passed

Join with your separator, keep the spec

{:?} and .join(", ") are the everyday answers. Joined is for a list inside a larger format!: Display items (no quotes), a separator you choose, and a width or precision after the colon that reaches every item ({:.1} of [1.0, 2.26] is 1.0, 2.3). Empty and one-item slices must work, and so must items that pad themselves. Joining the items’ to_string() results compiles and fails.

Starterexercises/joined.rs
use std::fmt::{self, Display};
/// Prints the items with `Display`, separated by the second field. Whatever the caller
/// put after the colon (`{:.1}`, `{:>4}`) applies to every item.
struct Joined<'a, T>(&'a [T], &'a str);
impl<T: Display> Display for Joined<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
todo!(
"write each item, with the separator between them, so that the caller's spec reaches the items"
)
}
}
/// An id that pads itself (it uses `f.pad`, so a width and a fill reach it).
struct Id(u32);
impl Display for Id {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(&format!("#{}", self.0))
}
}
fn main() {
assert_eq!(Joined(&[1, 2, 3], ", ").to_string(), "1, 2, 3");
assert_eq!(Joined(&["a", "b"], " | ").to_string(), "a | b"); // Display: no quotes
assert_eq!(Joined(&[7], ", ").to_string(), "7");
assert_eq!(Joined::<i32>(&[], ", ").to_string(), "");
assert_eq!(format!("{:.1}", Joined(&[1.0, 2.26], ", ")), "1.0, 2.3");
assert_eq!(
format!("{:>4}", Joined(&[1, 22, 333], "|")),
" 1| 22| 333"
);
assert_eq!(format!("{:>3}", Joined(&["a", "b"], ", ")), " a, b");
assert_eq!(
format!("{:>5}", Joined(&[Id(1), Id(22)], ";")),
" #1; #22"
);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/joined.solution.rsRun ↗
use std::fmt::{self, Display};
/// Prints the items with `Display`, separated by the second field. Whatever the caller
/// put after the colon (`{:.1}`, `{:>4}`) applies to every item.
struct Joined<'a, T>(&'a [T], &'a str);
impl<T: Display> Display for Joined<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (i, item) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(self.1)?;
}
item.fmt(f)?;
}
Ok(())
}
}
/// An id that pads itself (it uses `f.pad`, so a width and a fill reach it).
struct Id(u32);
impl Display for Id {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(&format!("#{}", self.0))
}
}
fn main() {
assert_eq!(Joined(&[1, 2, 3], ", ").to_string(), "1, 2, 3");
assert_eq!(Joined(&["a", "b"], " | ").to_string(), "a | b"); // Display: no quotes
assert_eq!(Joined(&[7], ", ").to_string(), "7");
assert_eq!(Joined::<i32>(&[], ", ").to_string(), "");
assert_eq!(format!("{:.1}", Joined(&[1.0, 2.26], ", ")), "1.0, 2.3");
assert_eq!(
format!("{:>4}", Joined(&[1, 22, 333], "|")),
" 1| 22| 333"
);
assert_eq!(format!("{:>3}", Joined(&["a", "b"], ", ")), " a, b");
assert_eq!(
format!("{:>5}", Joined(&[Id(1), Id(22)], ";")),
" #1; #22"
);
println!("all checks passed");
}
$ cargo run
all checks passed

A Debug that hides a secret

Session derives Debug, and its token is a Secret. Write Debug for Secret so it prints <redacted> wherever it appears ({:?}, {:#?}, inside a Vec or an Option), without revealing even the length. A new field of that type is covered by its type; finish_non_exhaustive has to be remembered per struct.

Starterexercises/session.rs
use std::fmt;
/// A value that must never be printed.
struct Secret(String);
#[derive(Debug)]
struct Session {
user: String,
token: Secret,
hits: u32,
}
// Debug must not show what is inside, not even its length.
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
todo!("write <redacted>")
}
}
fn main() {
let s = Session {
user: "ada".to_string(),
token: Secret("s3cr3t".to_string()),
hits: 3,
};
assert_eq!(
(s.user.as_str(), s.token.0.as_str(), s.hits),
("ada", "s3cr3t", 3)
);
assert_eq!(
format!("{:?}", s),
r#"Session { user: "ada", token: <redacted>, hits: 3 }"#
);
assert_eq!(
format!("{:#?}", s),
"Session {\n user: \"ada\",\n token: <redacted>,\n hits: 3,\n}"
);
assert!(!format!("{:?} {:#?}", s, s).contains(&s.token.0));
// The text must not depend on the secret, even in other values.
assert_eq!(format!("{:?}", Secret(String::new())), "<redacted>");
let two = vec![Secret("a".to_string()), Secret("bb".to_string())];
assert_eq!(format!("{:?}", two), "[<redacted>, <redacted>]");
assert_eq!(format!("{:?}", Some(&s.token)), "Some(<redacted>)");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/session.solution.rsRun ↗
use std::fmt;
/// A value that must never be printed.
struct Secret(String);
#[derive(Debug)]
struct Session {
user: String,
token: Secret,
hits: u32,
}
// Debug must not show what is inside, not even its length.
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("<redacted>")
}
}
fn main() {
let s = Session {
user: "ada".to_string(),
token: Secret("s3cr3t".to_string()),
hits: 3,
};
assert_eq!(
(s.user.as_str(), s.token.0.as_str(), s.hits),
("ada", "s3cr3t", 3)
);
assert_eq!(
format!("{:?}", s),
r#"Session { user: "ada", token: <redacted>, hits: 3 }"#
);
assert_eq!(
format!("{:#?}", s),
"Session {\n user: \"ada\",\n token: <redacted>,\n hits: 3,\n}"
);
assert!(!format!("{:?} {:#?}", s, s).contains(&s.token.0));
// The text must not depend on the secret, even in other values.
assert_eq!(format!("{:?}", Secret(String::new())), "<redacted>");
let two = vec![Secret("a".to_string()), Secret("bb".to_string())];
assert_eq!(format!("{:?}", two), "[<redacted>, <redacted>]");
assert_eq!(format!("{:?}", Some(&s.token)), "Some(<redacted>)");
println!("all checks passed");
}
$ cargo run
all checks passed

Read more