Extension Traits in Rust to Add Methods to Foreign Types
Rust forbids adding inherent methods to foreign types, but a trait you define can. Extension traits are how itertools, futures and tokio add methods to the standard library.

You cannot add a method to str, Vec or Option: they live in another crate, and Rust does not let you reopen a type the way Ruby or C# does. What you can do is define a trait with the method you want and implement it for the foreign type. Import the trait and the method appears as if it had always been there. This is the extension trait pattern, and it is how itertools, futures::StreamExt, tokio::io::AsyncReadExt and anyhow::Context all work. This post builds one, covers the rules that make it legal, the naming and scoping conventions, the ambiguity trap and the pattern's limits. Everything compiled on Rust 1.99, edition 2024.
The shape of an extension trait
The trait is yours, so you can implement it for any type, including one from the standard library. The orphan rule forbids implementing a foreign trait for a foreign type, and this does neither of those things:
trait StrExt {
fn is_blank(&self) -> bool;
fn truncate_chars(&self, max: usize) -> String;
}
impl StrExt for str {
fn is_blank(&self) -> bool {
self.trim().is_empty()
}
fn truncate_chars(&self, max: usize) -> String {
if self.chars().count() <= max {
return self.to_string();
}
let mut out: String = self.chars().take(max.saturating_sub(1)).collect();
out.push('~');
out
}
}
fn main() {
println!("{}", " \t".is_blank());
println!("{}", "hello".is_blank());
let title = String::from("Extension traits in Rust, a long title");
println!("{}", title.truncate_chars(16));
println!("{}", "short".truncate_chars(16));
println!("{}", "naïve café".truncate_chars(6));
}true
false
Extension trait~
short
naïve~The impl is for str, not String or &str, and it still works on all three because of method resolution: auto-deref finds str's methods, including trait methods in scope. Implementing for the unsized type is the general advice, since one impl covers everything that derefs to it. The truncation counts characters, not bytes, and so a string with multi-byte characters is not cut mid-codepoint (cutting at a byte index would panic).
The trait must be in scope
The method exists only where the trait is imported. In the same module that defines it there is nothing to do; elsewhere it needs a use. Forgetting that gives a puzzling error, because the type clearly has the method in the file next door:
mod text {
pub trait StrExt {
fn is_blank(&self) -> bool;
}
impl StrExt for str {
fn is_blank(&self) -> bool {
self.trim().is_empty()
}
}
}
fn main() {
println!("{}", " ".is_blank());
}error[E0599]: no method named `is_blank` found for reference `&'static str` in the current scope
--> src/main.rs:14:24
|
14 | println!("{}", " ".is_blank());
| ^^^^^^^^ method not found in `&'static str`
|
= help: items from traits can only be used if the trait is in scope
help: trait `StrExt` which provides `is_blank` is implemented but not in scope; perhaps you want to import it
|
1 + use crate::text::StrExt;
|The compiler found the trait and told you to import it. Adding use text::StrExt; fixes it. In a library, convention puts extension traits in a module called prelude or ext, so users write one glob import. The error and its cousins are listed in E0599.
A blanket impl: extending every type that has a capability
The more powerful form implements the trait for all types meeting a bound. This is how a method like .pipe(f), .tap(f) or .context("...") is added to everything:
use std::fmt::Display;
trait ResultExt<T, E> {
fn context_msg(self, msg: &str) -> Result<T, String>;
}
impl<T, E: Display> ResultExt<T, E> for Result<T, E> {
fn context_msg(self, msg: &str) -> Result<T, String> {
self.map_err(|e| format!("{msg}: {e}"))
}
}
trait Pipe: Sized {
fn pipe<R>(self, f: impl FnOnce(Self) -> R) -> R {
f(self)
}
}
impl<T> Pipe for T {}
fn parse_port(s: &str) -> Result<u16, String> {
s.parse::<u16>().context_msg("invalid port")
}
fn main() {
println!("{:?}", parse_port("8080"));
println!("{:?}", parse_port("99999"));
let n = " 42 ".trim().pipe(|s| s.parse::<i32>().unwrap_or(0)).pipe(|n| n * 2);
println!("{n}");
}Ok(8080)
Err("invalid port: number too large to fit in target type")
84impl<T> Pipe for T {} implements the trait for literally every sized type, using the default method in the trait body. The trait has no required methods, so the impl block is empty. Blanket impls are powerful and also the usual source of trouble, as the next section shows. The same approach is how anyhow adds .context() to every Result whose error is an std::error::Error.
The ambiguity trap
If two traits in scope provide a method with the same name for the same type, the call is ambiguous and the compiler refuses to pick:
trait Describe {
fn name(&self) -> String;
}
trait Label {
fn name(&self) -> String;
}
struct Item;
impl Describe for Item {
fn name(&self) -> String {
"item (describe)".into()
}
}
impl Label for Item {
fn name(&self) -> String {
"item (label)".into()
}
}
fn main() {
let i = Item;
println!("{}", Describe::name(&i));
println!("{}", <Item as Label>::name(&i));
println!("{}", i.name());
}error[E0034]: multiple applicable items in scope
--> src/main.rs:27:22
|
27 | println!("{}", i.name());
| ^^^^ multiple `name` found
|
note: candidate #1 is defined in an impl of the trait `Describe` for the type `Item`
--> src/main.rs:12:5
|
12 | fn name(&self) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^
note: candidate #2 is defined in an impl of the trait `Label` for the type `Item`
--> src/main.rs:18:5
|
18 | fn name(&self) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^
help: disambiguate the method for candidate #1
|
27 - println!("{}", i.name());
27 + println!("{}", Describe::name(&i));
|
help: disambiguate the method for candidate #2
|
27 - println!("{}", i.name());
27 + println!("{}", Label::name(&i));
|Two fully qualified forms work: Trait::method(&x) and <Type as Trait>::method(&x). The bare i.name() does not. This problem is why extension traits should have distinctive method names, and why library authors add a prefix or suffix when a name is likely to collide: if your extension adds .first() to iterators it will clash the day the standard library or itertools adds its own. Rust has a safeguard for the second case, an inherent method always wins over a trait method, so new standard-library methods can shadow yours silently. Clippy and the compiler lint unstable_name_collisions warn when your trait method name collides with an unstable standard method that may stabilise later.
Extending your own generic type with conditional methods
Extension traits also make a clean way to add methods only for elements that qualify, in the same spirit as the conditional impls in generic structs:
trait SliceStats {
fn mean(&self) -> Option<f64>;
fn median(&self) -> Option<f64>;
}
impl SliceStats for [f64] {
fn mean(&self) -> Option<f64> {
if self.is_empty() {
None
} else {
Some(self.iter().sum::<f64>() / self.len() as f64)
}
}
fn median(&self) -> Option<f64> {
if self.is_empty() {
return None;
}
let mut v = self.to_vec();
v.sort_by(f64::total_cmp);
let mid = v.len() / 2;
Some(if v.len() % 2 == 0 { (v[mid - 1] + v[mid]) / 2.0 } else { v[mid] })
}
}
fn main() {
let data = vec![4.0, 1.0, 9.0, 3.0];
println!("{:?} {:?}", data.mean(), data.median());
println!("{:?}", [7.5].median());
println!("{:?}", Vec::<f64>::new().mean());
}Some(4.25) Some(3.5)
Some(7.5)
NoneThe impl is on [f64] specifically, so the methods exist for vectors and arrays of floats and for nothing else. The median sorts a copy with total_cmp, because sorting floats with partial_cmp is the trap described in sorting with Ord and PartialOrd.
Extending Iterator itself
The most common published extension traits add adapters to every iterator, with a blanket impl over Iterator. The implementation returns a small struct that is itself an iterator, so the extension composes with the rest of the chain:
trait IterExt: Iterator + Sized {
fn every_nth(self, n: usize) -> EveryNth<Self> {
EveryNth { inner: self, n: n.max(1), pos: 0 }
}
}
impl<I: Iterator> IterExt for I {}
struct EveryNth<I> {
inner: I,
n: usize,
pos: usize,
}
impl<I: Iterator> Iterator for EveryNth<I> {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
loop {
let item = self.inner.next()?;
self.pos += 1;
if (self.pos - 1) % self.n == 0 {
return Some(item);
}
}
}
}
fn main() {
let ids: Vec<u32> = (1..=10).every_nth(3).collect();
println!("{ids:?}");
let words = ["a", "b", "c", "d", "e"];
let picked: Vec<_> = words.iter().every_nth(2).map(|w| w.to_uppercase()).collect();
println!("{picked:?}");
}[1, 4, 7, 10]
["A", "C", "E"]The supertrait bound Iterator + Sized gives the default method access to self by value, and the blanket impl means that every iterator in the program, including ones that did not exist when the trait was written, gains every_nth. The struct EveryNth is generic over the inner iterator, so the pipeline is still one concrete type with no allocation. This is exactly the shape of itertools::Itertools, which has about 100 such methods; reading its source is the best tutorial on the pattern. The iterator side of this is covered in implementing Iterator.
Why not a wrapper or a free function?
| Option | Call site | Costs |
|---|---|---|
| Free function | is_blank(s) | Does not chain; no method completion |
| Newtype wrapper | Text(s).is_blank() | Wrap at every call; lose the inner type's methods unless Deref |
| Extension trait | s.is_blank() | Needs use; can collide with future methods |
| Inherent method (your own type) | s.is_blank() | Only for types you own |
The extension trait is the best fit when the new behaviour is a natural method that reads left to right in a chain, and the type is not yours. For behaviour that changes the meaning of a type, a newtype is better, since it carries an invariant, as in the newtype pattern.
Compatibility and when not to do this
Publishing an extension trait on std types makes your crate's behaviour depend on what std adds. A new inherent method with your name silently takes precedence, which changes what your callers run without a compile error. Keep the name specific, mark the trait #[doc(hidden)] only if it is internal, and think of adding a method to a public extension trait as a breaking change for anyone who implements the trait themselves, which is why you may want it sealed (see the sealed trait pattern). If a helper is used in one file, a private function is simpler and carries no future risk. Reach for the pattern when you find yourself writing the same free function in three modules and wishing it were a method.


