The traits the compiler calls for you
In III.11 you learned how a trait is a contract: a set of methods a type promises to provide, that you call by name. obj.area() runs because the type implements an Area trait you wrote. This chapter is about a smaller, special set of traits where you never write the call. The compiler writes it for you, hidden behind ordinary syntax.
You have been leaning on these since chapter one without knowing their names. When you indexed a vector with v[0], that was a trait call. When you passed a &String to a function wanting &str and it just worked, that was a trait. When a == b compared two of your structs, when ? turned one error type into another, when {:?} printed a struct in a debug log: every one of those is a standard-library trait the language is wired to reach for.
So this chapter is a tour of the std traits that the compiler treats specially. Each section takes one piece of syntax you already use, names the trait behind it, and shows you how to implement that trait on your own type to earn the same syntax. The exhaustive catalog of every std trait is a Part IV story; here you learn the mechanism, the handful that the language itself depends on.
Most traits are ones you call: you write x.draw() because x implements Draw. The traits in this chapter are ones the compiler calls: you write a[i] or a == b and the compiler rewrites it into a trait method call you never see. Implementing the trait is how you opt your type into that syntax.
Deref: why &String works where &str is wanted
Here is a wrinkle you have probably already hit and shrugged past. You have a String, a function wants a &str, and you pass &my_string. It compiles. But &String and &str are different types, so why does the compiler accept one where it asked for the other? In a stricter language you would have to call a conversion method by hand.
The answer is a trait called Deref, the trait behind the * dereference operator. String implements Deref with its target set to str, which is a promise: if you have a reference to me, you can get a reference to an str instead. When the compiler sees a &String in a spot that needs &str, it quietly inserts that conversion. The rule has a name: deref coercion. Run this and watch three different coercions fire without a single explicit cast:
// A &String works where &str is wanted, and method calls reach through &Vec.
// That reaching-through has a name: deref coercion.
fn greet(name: &str) -> String {
format!("hello, {name}")
}
fn first_byte(bytes: &[u8]) -> Option<u8> {
bytes.first().copied()
}
fn main() {
let owned: String = String::from("ada");
// greet wants &str, you have &String. The compiler inserts a deref for you.
println!("{}", greet(&owned));
let nums: Vec<u8> = vec![3, 1, 4, 1, 5];
// first_byte wants &[u8], you have &Vec<u8>. Same coercion, same silence.
println!("first byte: {:?}", first_byte(&nums));
// The method call .len() is defined on str and [T], not on String or Vec,
// yet both calls work: the dot operator auto-derefs until it finds the method.
println!("name has {} chars", owned.len());
println!("vec has {} items", nums.len());
}Three things happened, all silent. greet(&owned) coerced &String to &str. first_byte(&nums) coerced &Vec<u8> to &[u8], because Vec derefs to a slice. And the method calls are the same trick in a different costume: .len() is not defined on String or Vec at all, it lives on str and on [T]. The dot operator auto-derefs: when a method is not found on the type in hand, the compiler derefs one step and looks again, repeating until it finds the method or runs out of steps.
That auto-deref on the dot is why you almost never write * by hand in Rust. Box<T>, Rc<T>, and the other smart pointers from earlier chapters all implement Deref to their inner T, so calling a method through a Box reaches the value inside with no ceremony. The pointer is transparent to method lookup.
If type A implements Deref<Target = B>, then anywhere a &B is wanted you may pass a &A, and any method that exists on B can be called on an A. That single rule is why &String acts like &str, &Vec<T> acts like &[T], and a Box<T> acts like the T it holds.
Deref on your own type, and its DerefMut twin
Deref coercion is not reserved for the standard library. Implement Derefon a type of your own and it inherits the inner type’s entire method surface for free. The classic use is a newtype: a thin wrapper around an existing type that adds a guarantee, like “this string is never empty” or “this integer is always in range,” while still behaving like the thing it wraps. Run this:
// Implement Deref on your own type and it inherits the inner type's methods.
use std::ops::Deref;
// A wrapper that guarantees its string is never empty.
struct NonEmpty(String);
impl NonEmpty {
fn new(text: &str) -> Option<NonEmpty> {
if text.is_empty() {
None
} else {
Some(NonEmpty(text.to_string()))
}
}
}
// Deref says: "when someone reaches through me, hand them the inner String."
impl Deref for NonEmpty {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
fn main() {
let name = NonEmpty::new("ada").expect("not empty");
// .len() and .to_uppercase() are String methods, not NonEmpty methods,
// yet they work: the dot operator derefs NonEmpty to &String and finds them.
println!("length: {}", name.len());
println!("shout: {}", name.to_uppercase());
// It coerces in argument position too: &NonEmpty becomes &str for a &str param.
fn announce(s: &str) {
println!("announcing {s}");
}
announce(&name);
}NonEmpty wraps a String and enforces, in its new constructor, that the string is not empty. But it does not have to reimplement len, to_uppercase, or any of the dozens of String methods. The single Deref impl, returning &self.0, hands all of them through. The wrapper adds a promise without paying for it in boilerplate.
There is a mutable companion, DerefMut, that does the same for &mut. Where Deref turns a &A into a &B, DerefMut turns a &mut A into a &mut B, so a method that needs &mut self on the inner type can be called through your wrapper too. You implement it only when you want mutation to pass through; a read-only wrapper implements Deref alone.
It is tempting, coming from an object-oriented language, to reach for Deref to fake inheritance: wrap a type, deref to it, and get all its methods plus your own. The community guidance is firm that this is a misuse. Deref is meant for smart pointers and thin newtypes, where the wrapper genuinely is a stand-in for the inner value. Use it to add a guarantee, not to build a class hierarchy; for shared behaviour, use a trait (III.10) instead.
Index: the trait behind the square brackets
You write v[0] on a Vec and map["key"] on a HashMap without thinking. In many languages, indexing is a built-in that only the built-in collections enjoy; your own types are stuck with a .get(i) method. In Rust the brackets are not built in. They are sugar for a trait called Index, and any type can implement it.
The desugaring is exact: a[i] means *a.index(i), a call to Index::index followed by a dereference. The trait has one associated type, Output, naming what an index produces, and one method, index, that returns a reference to it. Here is a seven-day week that lets you write week[0]. Run it:
// The square brackets a[i] are a call to the Index trait. Implement it and
// your own type gets the same syntax the standard collections have.
use std::ops::Index;
// A fixed seven-day week, indexed by day number 0..=6.
struct Week {
days: [&'static str; 7],
}
impl Index<usize> for Week {
type Output = str;
// index returns a reference; a[i] is sugar for *a.index(i).
fn index(&self, day: usize) -> &str {
self.days[day]
}
}
fn main() {
let week = Week {
days: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
};
// week[0] desugars to *week.index(0). The Output type is str, so this is &str.
println!("first day: {}", &week[0]);
println!("midweek: {}", &week[2]);
// Reading past the end still panics, exactly like a slice: index just
// forwards to the inner array, which does the bounds check.
let safe = if 9 < 7 { &week[9] } else { "out of range" };
println!("day 9: {safe}");
}week[0] compiles to *week.index(0). Our index forwards to the inner array, so the bounds check, and the panic on an out-of-range index, come from the array for free; we did not write either. That is the same behaviour the standard collections have, because they implement the very same trait. Note that Index takes the index type as a generic parameter, here usize, which is why a HashMap can be indexed by a key type rather than a number.
For mutation through the brackets, as in v[0] = x, there is a parallel trait, IndexMut, whose method returns a &mut instead of a &. You implement it alongside Index when assignment-through-brackets should work; Index alone gives you read-only indexing.
a[i] is not special syntax tied to a few blessed types. It desugars to *a.index(i) through the Index trait, and a[i] = v desugars through IndexMut. Implement the trait and your type indexes exactly like a Vec; the language plays no favourites.
From and Into: conversion you implement once
Every program is full of small conversions: a u16 that should become a typed Port, a &str that should become an owned String, one error type that should become another. In Go you write a free function, NewPort(n), and every package invents its own naming convention. Rust has one standard pair of traits for value conversion, so the spelling is the same everywhere: From and Into.
From<T>means “a value of this type can be built from a T.” Into<U>means “this value can be turned into a U.” They are two views of the same conversion, and here is the rule worth memorising: you implement From, and the standard library gives you Into for free. There is a blanket impl in std that reads, in effect, “for any T and U where U: From<T>, also T: Into<U>.” Write one, get both. Run this:
// Implement From once and you get Into for free. The standard library writes
// the Into impl for you in terms of your From.
// A newtype for a TCP port, validated on the way in.
#[derive(Debug)]
struct Port(u16);
// "A Port can be built from a u16." This is the only conversion you write.
impl From<u16> for Port {
fn from(raw: u16) -> Port {
Port(raw)
}
}
fn main() {
// Call From directly:
let a = Port::from(8080);
println!("from: {a:?}");
// Or call Into, which you never implemented but got anyway:
let b: Port = 6379.into();
println!("into: {b:?}");
// Into shines in generic position: this accepts anything convertible to Port.
fn open(p: impl Into<Port>) -> Port {
p.into()
}
println!("generic: {:?}", open(443u16));
}The only conversion code in that program is the single impl From<u16> for Port. From it we got three call styles: Port::from(8080) calling From directly, 6379.into() calling the Into we never wrote, and the generic open(p: impl Into<Port>) that accepts anything convertible to a Port. That last shape is the idiomatic one: a function takes impl Into<T> so callers can pass either a T or anything that converts to one, and the function calls .into() at the top.
The guidance is blunt: write the From impl, never the Into one. Because of the blanket impl, an Into comes into existence the moment your From does, and writing Into by hand would collide with it. The only reason you ever name Into is as a bound, as in fn open(p: impl Into<Port>), where it is the more natural direction to read. You consume Into; you produce From.
From is for conversions that cannot fail: every u16 is a valid Port, so the conversion is total. When a conversion can fail, for example turning an i64 into a u16 that might not fit, the standard library uses a sibling trait, TryFrom, whose method returns a Result. Same shape, one returns the value and one returns a Result. Reach for TryFrom the moment the conversion has a way to go wrong.
How ? uses From to convert errors
Back in I.02 you met the ? operator and a single sentence you were told to take on faith: that ? can convert one error type into another on its way out. Now you have the trait that does it. When ? early-returns an error, it does not return the error unchanged; it calls From on it first, converting the error of the failing call into the error type the enclosing function returns.
That is the whole trick. A function that returns Result<_, MyError> can call a helper that fails with ParseIntError, and a bare ? on that helper will work, provided you have taught the compiler how to make a MyError from a ParseIntError by implementing From<ParseIntError> for MyError. Run this and watch a ParseIntError arrive inside a ConfigError:
// The ? operator uses From to convert one error type into another on its way
// out. That single fact is the engine behind real error handling (III.13 next).
use std::num::ParseIntError;
// Our own error type, with a variant for each thing that can go wrong.
#[derive(Debug)]
enum ConfigError {
Empty,
BadNumber(ParseIntError),
}
// Teach ? how to lift a ParseIntError into a ConfigError. With this From impl,
// a bare ? on a Result<_, ParseIntError> can return Result<_, ConfigError>.
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> ConfigError {
ConfigError::BadNumber(e)
}
}
fn parse_port(line: &str) -> Result<u16, ConfigError> {
let trimmed = line.trim();
if trimmed.is_empty() {
return Err(ConfigError::Empty);
}
// parse returns Result<u16, ParseIntError>. The ? converts that error into
// ConfigError by calling ConfigError::from, then early-returns it.
let port: u16 = trimmed.parse()?;
Ok(port)
}
fn main() {
for line in [" 8080 ", "", "https"] {
match parse_port(line) {
Ok(port) => println!("{line:?} -> ok, port {port}"),
Err(ConfigError::Empty) => println!("{line:?} -> empty line"),
// The inner ParseIntError, lifted in by ?, is right here to read.
Err(ConfigError::BadNumber(e)) => println!("{line:?} -> bad: {e}"),
}
}
}Look at the one load-bearing line: let port: u16 = trimmed.parse()?;. The parse call returns Result<u16, ParseIntError>, but the function returns Result<u16, ConfigError>. Those error types differ, and the only thing bridging them is our impl From<ParseIntError> for ConfigError. The ? reads it as: on Err(e), return Err(ConfigError::from(e)). Remove that From impl and the ? stops compiling, because the compiler no longer knows how to turn one error into the other.
This is the mechanism that makes real error handling pleasant. A single function can call five helpers that fail in five different ways, and collect them all into one tidy error enum of its own, with nothing more than a From impl per source error and a ? per call. Building those error enums properly, deriving the Error trait, and the ergonomics of doing this at scale, is the subject of the next chapter. From is the engine; III.13 is where you drive it.
? is not magic that converts anything to anything. It calls From, so the conversion has to be in scope as an impl. If the failing call’s error type already isthe function’s error type, no conversion is needed and ? just propagates it (every type has a reflexive From<T> for T). If the types differ and no From connects them, you get a compile error pointing right at the ?, telling you the conversion is missing.
Equality and ordering: PartialEq, Eq, PartialOrd, Ord
You write a == b and a < b on integers without a thought. On your own structs, those operators do not exist until you opt in, and the way you opt in is a small family of traits the compiler reaches for behind the comparison operators. The good news is that for ordinary data you almost never write them by hand; you derive them. Run this:
// Equality and ordering are traits. Derive them and the compare operators,
// sorting, and min/max all start working on your type.
// PartialOrd/Ord order by field, top to bottom, so version sorts correctly.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
}
fn main() {
let a = Version { major: 1, minor: 9 };
let b = Version { major: 1, minor: 12 };
// == and != come from PartialEq; < <= > >= come from PartialOrd.
println!("a == b: {}", a == b);
println!("a < b: {}", a < b); // 1.9 < 1.12 because minor 9 < 12
let mut all = vec![
Version { major: 2, minor: 0 },
Version { major: 1, minor: 12 },
Version { major: 1, minor: 9 },
];
// sort needs Ord; it is derived, so this just works.
all.sort();
println!("sorted: {all:?}");
// max needs Ord too, and returns the larger by that same field order.
println!("newest: {:?}", all.iter().max().unwrap());
}One derive line did a lot. PartialEq is the trait behind == and !=; deriving it compares two values field by field. PartialOrd is behind < <= > >=; deriving it orders by field, top to bottom, which is exactly why 1.9 < 1.12 here (it compares major first, finds them equal, then compares minor). Ord is what sort and max require, a total ordering where every pair of values has a definite order.
The two pairs split on a subtle point. PartialEq and PartialOrd are the partial versions, allowing for values that are not comparable. The reason they exist is floating point: f64 has NaN, a value that is not equal to itself and not ordered against anything, so f64 can only be PartialOrd, never Ord. Eq and Ord are the total versions, the promise that there are no such holes: every value equals itself and every pair has an order. Integers and strings get the full set; f64 stops at partial.
For ==, derive PartialEq. To use the type as a HashMap key or in a HashSet, add Eq (and Hash). For < and friends, derive PartialOrd. To sort a Vec or call max, you need Ord. For plain data with no floats, deriving all four is the common, boring, correct choice.
“Partial” is not a hedge about how complete the implementation is; it is a precise maths term. A partial order may leave some pairs incomparable, and partial equality may allow a value that is not equal to itself. Ord and Eq are the stronger claims, that the order is total and equality is reflexive, with no gaps. f64 earns only the partial versions precisely because NaN punches a hole in both.
Default and the ..Default::default() spread
Rust has no optional or default function arguments: every parameter is required, and a function called with three arguments always takes exactly three. So how do you build a config struct with fifteen fields when a caller wants to set two and accept sane values for the rest? The answer is a trait, Default, paired with a piece of struct syntax. Run this:
// Default gives a type a sensible zero, and ..Default::default() fills in
// every field you did not set. It is Rust's answer to optional arguments.
#[derive(Debug)]
struct ServerConfig {
host: String,
port: u16,
workers: u32,
tls: bool,
}
// Hand-written Default: the "no options given" configuration.
impl Default for ServerConfig {
fn default() -> ServerConfig {
ServerConfig {
host: String::from("127.0.0.1"),
port: 8080,
workers: 4,
tls: false,
}
}
}
fn main() {
// Set only what differs; ..Default::default() supplies the rest.
let custom = ServerConfig {
port: 443,
tls: true,
..Default::default()
};
println!("{custom:?}");
// The bare default is one call away.
println!("{:?}", ServerConfig::default());
}Default has one method, default(), that returns the “no options given” value of a type. The payoff is the line ..Default::default() inside the struct literal, the struct update syntax: it says “set these fields explicitly, and fill every field I did not mention from Default::default().” The caller writes only what differs from the default, which is as close as Rust comes to named optional arguments, and it is fully type-checked.
For most structs you do not write Default by hand. If every field already has a Default, you can #[derive(Default)] and get the field-wise default for free: 0 for numbers, false for bool, the empty String, the empty Vec. We wrote it by hand here only because our defaults are not the all-zero ones (the host and port have real values), which is the case where a hand-written impl Default earns its keep.
Where another language writes connect(host, port = 8080), Rust writes a struct with a Default and lets the caller spread it: Config { port: 443, ..Default::default() }. Every unmentioned field comes from the default, the set is checked at compile time, and adding a new field later does not break existing callers as long as the default covers it.
Display vs Debug: {} for people, {:?} for you
You have been writing {} and {:?} in println! since chapter one. They are not two formats of one trait; they are two different traits, and the brace style chooses which one the compiler reaches for. {} calls Display, the human-facing form. {:?} calls Debug, the programmer-facing form. Run this and read the two lines of output against the two impls:
// {:?} is Debug, for you the programmer; {} is Display, for the end user.
// You derive Debug; you write Display by hand, once, deciding the words.
use std::fmt;
struct Temperature {
celsius: f64,
}
// Display: how this value reads to a human. You choose every character.
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// write! into the formatter is how you emit the text.
write!(f, "{:.1}C", self.celsius)
}
}
// Debug, derived: a developer-facing dump that shows the struct's shape.
impl fmt::Debug for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Temperature {{ celsius: {} }}", self.celsius)
}
}
fn main() {
let t = Temperature { celsius: 21.456 };
// {} reaches for Display: the polished, human form.
println!("display: {t}");
// {:?} reaches for Debug: the diagnostic form, for logs and dbg!.
println!("debug: {t:?}");
}Display is the polished string a user should see: 21.5C, with the precision and the unit you chose. Debugis the diagnostic dump a developer wants in a log or a test failure: it shows the type name and the fields, so you can see the value’s shape. The division of labour is deliberate. You derive Debug on almost everything, because a mechanical field dump is exactly what you want when debugging. You implement Display by hand, and only when a type genuinely has a human reading, because only you can decide the words.
Both methods take a Formatter and emit text into it with write!, returning fmt::Result to signal whether writing succeeded. You almost never call fmt directly; you write the impl and then use {} or {:?}, and the macro machinery calls it for you. One convenience worth knowing: any type that implements Display gets .to_string() automatically, through a blanket impl, the same one-implementation-two-features pattern you saw with From and Into.
Derive Debug freely; it costs nothing and helps every time something goes wrong. But resist writing Display for a type that has no natural human form. A ServerConfig has no single sentence that is the config, so forcing a Display onto it just invents a format nobody asked for. Displayis a promise that “this value reads cleanly to an end user.” Make that promise only when it is true.
That is the special-traits tour. Each symbol you reached for without thinking, the brackets, the dot, the comparison signs, the braces, the ?, turned out to be a trait call the compiler writes for you, and each is a trait you can implement on your own types to earn the same syntax. The full std trait catalog, every one and its exact contract, is the work of Part IV. And the From you just met as an error-converter is about to take centre stage: Part III, chapter 13 builds real error handling on top of it.
The whole chapter in seven lines
- Some std traits are ones the compiler calls for you, hidden behind ordinary syntax; implementing them opts your type into that syntax.
Derefdrives deref coercion:&Stringworks as&str, and the dot operator auto-derefs to find methods on the inner type.Indexis the trait behinda[i](andIndexMutbehinda[i] = v); the brackets are a method call, not a built-in.- Implement
Fromonce and the std blanket impl gives youIntofor free; consumeIntoas a bound, produceFrom. ?converts errors throughFrom, which is how one function collects many failure types into a single tidy error enum (the heart of III.13).- Derive
PartialEq/Eq/PartialOrd/Ordfor comparison and sorting, andDefaultfor the..Default::default()spread. {}isDisplay(for people, written by hand) and{:?}isDebug(for you, almost always derived).