Defining a Trait

Understand the syntax and purpose of trait definitions in Rust—how to declare shared method signatures that establish a contract for behavior across types.

A trait is the piece of Rust that captures the idea "these types can all do the same thing." It is a collection of method signatures—names, parameter lists, and return types—written without bodies. Any type that claims to satisfy the trait must provide those method bodies, in its own way, later. The definition itself is the blueprint; the concrete types are the buildings.

This separation matters because it lets you write code that works with any type that meets the contract, without knowing which concrete type it will be until compile time. In this section we examine exactly how to write that blueprint.

What a Trait Definition Contains

A trait definition is a block that starts with trait and holds one or more method signatures. There is no implementation here—only the shape of the methods. When you later write impl MyTrait for MyType, you fill in what each method actually does.

Think of it as a job description. The description says "must be able to summarize," but it does not say how to do it. Each applicant—each type—brings its own way. The Rust compiler checks that every type that shows up for the job actually provides the method with the exact signature listed.

The trait Keyword and the Basic Syntax

Here is the minimal trait definition that declares a summarize method.

pub trait Summary {
    fn summarize(&self) -> String;
}

This single block is the complete contract. Let’s unpack it piece by piece:

  • pub makes the trait visible outside the current crate. If you leave it off, the trait is private to the module and cannot be used by downstream crates—including binaries that depend on your library. If you intend the trait as a public API, pub is essential.
  • trait is the keyword that starts a trait definition. The name follows in CamelCase by convention. A name like Summary hints at what capability the trait represents—often a verb or an adjective, like Display, Clone, Read.
  • Inside the curly braces, method signatures are listed. Each one looks exactly like a function declaration but ends with a semicolon instead of a body between curly braces. The semicolon signals "this is required; the implementor will provide the body."
  • fn summarize(&self) -> String; is the signature. It takes a shared reference to self (the concrete type that will implement the trait) and returns a String. The &self parameter is how the method gets access to the data of the concrete instance. You can also use &mut self or self if the method needs to mutate or consume the value.

Compiler Guarantee:

Once you compile a trait definition, any type that implements this trait must provide exactly fn summarize(&self) -> String. The compiler will refuse any implementation that changes the parameter types, the return type, or the method name.

Method Signatures and the Semicolon Rule

The semicolon after a method signature in a trait definition is not a stylistic choice; it carries precise meaning.

  • Signature with a semicolon: the trait declares that implementors must supply the body. There is no fallback.
  • Signature with a body (covered in the "Default Implementations" section): the trait provides a body that an implementor can use as-is or override.

A common mistake is to accidentally write a body when you meant to require implementors to write their own, or to put a semicolon when you wanted a default. The compiler will accept both, but the contract changes. If you write a body, the method becomes optional to implement—which can be fine, but it is easy to forget that now types can rely on the default and behave in a way you did not anticipate.

Here is a trait with two required methods:

pub trait Instrument {
    fn play(&self);
    fn tuning(&self) -> &str;
}

Every type that implements Instrument must provide both play and tuning. There is no default sound of an instrument; each one must decide for itself.

Multiple Methods in a Trait

A trait can list any number of method signatures. Each signature sits on its own line, ends with a semicolon, and represents one obligation. When a type implements the trait, it must supply every required method.

pub trait Measurement {
    fn value(&self) -> f64;
    fn unit(&self) -> &str;
    fn label(&self) -> String;
}

There is no limit on the number of methods. The trait groups together all the capabilities that make sense for a single concept. For example, a Measurement trait might require value, unit, and label because anyone using a measurement will likely need all three.

Traits with many methods:

While you can pack many methods into a trait, a trait with too many obligations can become difficult to implement and test. Rust’s own standard library traits tend to be small and focused: Clone has one method, Iterator has one required method (next) and many provided ones. Small, cohesive traits are easier to reason about and compose.

Traits as Shared Behavior Contracts

The real power of a trait definition shows up when you stop thinking about individual types and start thinking about capabilities. Once you have defined a trait, you can write functions that accept anything that implements it.

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Here, &impl Summary says: "accept a reference to any type that implements Summary." The function does not know whether item is a NewsArticle, a SocialPost, or a type defined in a crate you have never seen. It only knows that item has a .summarize() method that returns a String. The trait definition is the bridge that connects the function to all those possible types.

This is why naming a trait well matters. The name should answer the question "what can something do if it implements this trait?" If you name a trait Summarizable, the contract is clear. If you name it Data, the contract is vague and the trait will be misused.

Trait visibility and external use:

If a trait is defined without the pub keyword, no code outside the current module can see it. Other crates cannot implement it, and functions outside the module cannot use it as a bound. A private trait definition has a very limited scope. For library code that is meant to be shared, always make the trait pub.

Common Mistakes When Defining Traits

Forgetting pub on the trait

A trait that is not public cannot be implemented by downstream crates. If you are writing a library and expect users to implement your trait for their own types, the compiler will stop them with an error about a private trait. The fix is simple—add pub—but the error can be confusing the first time you see it.

Leaving off the semicolon after a required method

If you omit the semicolon and write curly braces with a body, you have inadvertently created a default implementation. The trait no longer forces implementors to think about that method. This might be what you want, but if you later discover that many implementors just inherit the default and produce wrong results, you have created a design problem that is hard to fix without breaking existing code.

Using the wrong self parameter

Trait methods typically take &self because they need read-only access to the instance. If you write fn summarize(self) -> String (without the ampersand), the method will consume the value—the caller loses ownership after the call. This is sometimes correct, but it drastically limits how the trait can be used, because you cannot call a consuming method on a reference. For shared behavior that many callers will use, &self is almost always the right choice.

Expecting the trait itself to hold data

A trait definition contains method signatures, not fields. You cannot write pub trait Summary { content: String; }. Traits describe behavior, not structure. If you need shared data, you put that data in a struct and implement the trait for that struct.

Async trait methods need extra tooling:

Writing async fn summarize(&self) -> String; in a trait definition requires the async_trait macro from the async-trait crate in today’s stable Rust. Native async trait methods are on the roadmap but not yet stabilized at the time of writing. Attempting to write async in a trait definition without that macro will produce a compiler error about opaque types.

Where Trait Definitions Fit in a Codebase

You will typically find trait definitions in library crates, near the top of a module or in a dedicated traits.rs file. They are part of the public API surface. A well-designed crate often exports one or more traits that consumers implement, alongside functions that operate on those traits.

For example, the serde crate defines the Serialize and Deserialize traits. Users never call those trait methods directly in normal usage; they just derive or implement them, and the rest of the crate does the heavy lifting. The trait definition is the entry point to the entire serialization framework.

Summary

A trait definition groups method signatures into a named contract. Writing one is a promise: "Any type that implements this trait will have these methods, with these exact signatures." The compiler enforces that promise, and the rest of the type system—generics, trait bounds, trait objects—builds on it.

You now have the blueprint. The concrete types fulfill that blueprint by implementing the trait.