Formatting: Display and Debug
Every println!, format!, and write! runs on the same machinery: the std::fmt traits. Derive Macros and Common Traits noted that #[derive(Debug)] gives you {:?}, but stopped there. This tutorial goes into the two formatting traits you actually implement, Debug (for developers) and Display (for users), the format-spec mini-language ({:>8.2}, {:#?}, {value:width$}), and how implementing Display unlocks .to_string() for free. Getting formatting right is the difference between readable logs, clean error messages, and polished CLI output versus a wall of default-derived noise.
Debug vs Display: Two Audiences
The two core traits exist for two different readers, and conflating them is the usual mistake:
Debug({:?}) is for programmers: diagnostic output, logs,dbg!, test failures. It should show structure, and you almost always#[derive(Debug)]it.Display({}) is for end users: the human-facing representation. It is never derived, you implement it by hand, precisely because "how to present this to a user" is a judgment call the compiler can't make.
use std::fmt;
#[derive(Debug)] // {:?} — structural, for developers
struct Temperature { celsius: f64 }
impl fmt::Display for Temperature { // {} — human-facing, hand-written
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
let t = Temperature { celsius: 21.567 };
println!("{t:?}"); // Temperature { celsius: 21.567 } — Debug
println!("{t}"); // 21.6°C — Display
The rule: derive Debug on essentially everything (it costs nothing and helps every log and test), and implement Display only for types with a meaningful human representation. There is intentionally no #[derive(Display)], that's a signal to think about the user-facing form rather than dumping fields.
Implementing Display: Write Into the Formatter
Display::fmt receives a &mut Formatter and returns fmt::Result. You don't build a String, you write! directly into the formatter (which may be a terminal, a file, or a buffer), the same write! from The Read and Write Traits, avoiding an intermediate allocation:
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) // writes into f, returns its Result
}
}
Gotcha: implement
Displayby writing into theFormatter, never by building aStringand returning it, the signature won't even let you (it returnsfmt::Result, notString). Usewrite!(f, ...)and propagate its result with?if you have multiple writes. A common mistake is callingself.to_string()inside your ownDisplayimpl, which recurses infinitely (to_stringcallsDisplay). Write the fields directly instead.
The payoff is automatic: implementing Display gives you ToString (and thus .to_string()) for free, via a blanket impl (Generics and Trait Bounds). So point.to_string() and format!("{point}") both work the moment Display exists, no separate to_string method needed. This is also why Display is the right trait for a user-facing error's message (Error Libraries generates it from #[error("...")]).
The Format Spec Mini-Language
The text inside {} after a colon is a compact spec controlling width, alignment, precision, and more. It works in every formatting macro, and learning it replaces a lot of manual string padding:
println!("{:>8}", "hi"); // " hi" — right-align, width 8
println!("{:<8}|", "hi"); // "hi |" — left-align
println!("{:^8}", "hi"); // " hi " — center
println!("{:08.2}", 3.14159); // "00003.14" — zero-pad, width 8, 2 decimals
println!("{:+}", 42); // "+42" — always show sign
println!("{:#x}", 255); // "0xff" — alternate form (hex with prefix)
println!("{:b}", 5); // "101" — binary
The grammar is {:[fill][align][sign][#][width][.precision][type]}. The most-used pieces: </>/^ for alignment, a number for minimum width, .N for precision (decimals on floats, max length on strings), and type letters (x/X hex, b binary, o octal, e scientific). Width and precision can also come from arguments: {:width$} or {:.prec$} pulls the value from a named/positional argument, so println!("{:width$}", x, width = 10) sets width dynamically.
Pretty Debug and Named Arguments
Two everyday conveniences round out the picture. The # flag on Debug ({:#?}) switches to pretty-printed, multi-line output, invaluable for inspecting nested structures:
#[derive(Debug)]
struct Config { name: String, retries: u32, tags: Vec<String> }
let c = Config { name: "svc".into(), retries: 3, tags: vec!["a".into(), "b".into()] };
println!("{c:?}"); // Config { name: "svc", retries: 3, tags: ["a", "b"] } — one line
println!("{c:#?}"); // multi-line, indented — each field on its own line
And since Rust 2021, you can capture variables directly by name in the braces ({c}, {c:#?}) instead of listing them as trailing arguments, cleaner for simple cases:
let name = "Ada";
let score = 95;
println!("{name} scored {score}"); // captures name, score from scope
println!("{score:>5}"); // capture + format spec together
// trailing-argument form still needed for expressions: println!("{}", a + b);
Inline capture works only for bare variable names, not expressions or field accesses ({self.x} doesn't work, {} with a trailing arg does). Use inline capture for plain locals and the trailing form for anything computed.
Formatting Reference
| You want | Use |
|---|---|
| Developer/diagnostic output | #[derive(Debug)] + {:?} |
| Pretty multi-line debug | {:#?} |
| User-facing representation | hand-implement Display + {} |
.to_string() on your type | implement Display (gives ToString free) |
| Align / pad to a width | {:>8} / {:<8} / {:^8} |
| Fixed decimals | {:.2} |
| Hex / binary / octal | {:x} / {:b} / {:o} (add # for prefix) |
| Width/precision from a variable | {:width$} / {:.prec$} |
| Interpolate a local variable | {name} (inline capture) |
The throughline: Debug is derived and for developers; Display is hand-written and for users, and both flow through the same Formatter you write! into. The format spec is a small language worth memorizing the common pieces of (>, width, .precision, #), because it eliminates manual padding and rounding throughout your output code.
Key Takeaways
Debug({:?}) is for developers and should be derived on nearly everything;Display({}) is for end users and is never derived, you hand-write it because presentation is a judgment call.- Implement
Display::fmtbywrite!-ing into the&mut Formatterand returningfmt::Result; never build and return aString, and never callself.to_string()inside it (infinite recursion). - Implementing
DisplaygivesToString/.to_string()for free via a blanket impl, and is the right trait for user-facing error messages. - The format spec
{:[fill][align][sign][#][width][.precision][type]}controls alignment (</>/^), width, decimals (.N), sign (+), alternate form (#), and radix (x/b/o); width/precision can come from arguments via$. - Use
{:#?}for pretty multi-line debug, and inline capture ({name}) for plain local variables (not expressions or field accesses, which still need the trailing-argument form).