16.6Display and ToString

Last updated June 13, 2026

Lesson 10.7 left an IOU: {} (the Display formatter) is for human-facing output, and you'd have to implement it yourself because the compiler can't guess how your type should read. You now know what "implement a trait" means, so it's time to pay that IOU. Display is a trait, you implement it by hand, and doing so is the Rust equivalent of C++'s "overload operator<<," a lesson that gets one page here instead of a chapter.

Why Display can't be derived

Recall the split from lesson 16.5: Debug ({:?}) is derivable because "show the type name and fields" has an obvious form, but Display ({}) is not, because how a type should appear to a human is a design decision only you can make. Should a Point display as (3, 4)? 3, 4? x=3, y=4? There's no right answer the compiler could pick, so it makes you choose by implementing Display yourself.

Implementing Display

Display lives in std::fmt, and implementing it means writing one method, fmt:

use std::fmt;

struct Point {
    x: f64,
    y: f64,
}

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: 3.0, y: 4.0 };
    println!("{}", p);
    let s = p.to_string();
    println!("as string: {s}");
}
(3, 4)
as string: (3, 4)

This is the same shape you glimpsed for error types in lesson 12.6, now fully explained. The fmt method takes &self and a Formatter (f), and uses the write! macro, which works exactly like println! but writes into the formatter instead of to the screen. You choose the format string, here "({}, {})", which is the design decision the compiler couldn't make. The return type fmt::Result reports whether writing succeeded; write! produces it, so ending the method with that write! is all you need.

Once Point implements Display, println!("{}", p) works, and so does format!, print!, and every other place {} appears. You taught the type how to present itself once, and the whole formatting system now knows.

ToString comes free

Notice p.to_string() in the example. That method came from nowhere you wrote, and that's the bonus: any type that implements Display automatically gets to_string() for free, producing a String of its Display output. This is a blanket implementation in the standard library: it implements the ToString trait for every type that implements Display, so you implement one trait and get two capabilities. (Blanket implementations, "implement this trait for all types that implement that one," are a powerful pattern you'll meet again; here, just enjoy that to_string() is automatic.)

Key insight

Display is the one place you spell out how your type looks to a person, and you spell it out once. After that, every formatting context ({}, format!, to_string(), printing in an error message) uses it. Contrast Debug, which the compiler derives for the programmer's view. Most types you build for users to see will implement Display; most types you build for yourself will just derive Debug; many do both, exactly as recommended for error types in lesson 12.6.

The C++ comparison

Worth stating plainly, because it's the chapter's thesis in miniature. In C++, making a type printable with << means overloading operator<<, which sits inside a large chapter on operator overloading with its own rules about friend functions, stream references, and return types. In Rust it's one trait with one method, and it's the same machinery as every other trait you've implemented. The next lesson generalizes the point to all operators: +, *, indexing, each is just a trait, and the lesson that handles them is one page where C++ needs sixteen.

Best practice

Implement Display for types that have a natural human-readable form and will be shown to users (errors, domain values like money or coordinates, anything that ends up in a message). Don't implement it for types that have no obvious single presentation, or that are purely internal, just derive Debug for those. And never put debugging detail in Display or polish in Debug: {} is the clean user-facing form, {:?} is the faithful developer dump, and keeping them in their lanes keeps both useful.

Quiz time

Question #1

Why must you implement Display by hand when Debug can be derived?

Show solution

Debug has an obvious automatic form (show the type name and fields), so the compiler can derive it. Display is the human-facing presentation, and how a type should read to a person is a design decision the compiler can't make (is a point (3, 4) or x=3, y=4?). So you choose the format by implementing Display yourself.

Question #2

Implement Display for a Temperature { celsius: f64 } so it prints like 21.5°C.

Show solution
use std::fmt;

struct Temperature {
    celsius: f64,
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}°C", self.celsius)
    }
}

write!(f, "{}°C", self.celsius) writes the chosen format into the formatter. Now println!("{}", temp) and temp.to_string() both produce 21.5°C.

Question #3

You implemented Display for a type. What other method do you get automatically, and from where?

Show solution

You get to_string(), which returns a String of the Display output. It comes from a blanket implementation in the standard library that implements ToString for every type implementing Display. Implement one trait (Display), get the second (ToString) for free.

Display made {} work by implementing a trait. The next lesson reveals that +, *, [], and the other operators are the same story: each is a trait you can implement for your own type, so a Point + Point becomes meaningful, in one lesson where C++ needs a whole chapter.