2024-10-09
007 - Traits vs Duck Typing and Protocols
Learning Rust as a Pythonista: Traits vs. Duck Typing and Protocols
Traits are one of Rust’s most important abstraction tools. They are the bridge between flexibility and type safety.
Why this matters for Python developers
- Python duck typing is flexible but mostly runtime-checked.
- Python protocols add optional static checking.
- Rust traits make behavior contracts explicit and compile-time enforced.
Learning goals
By the end of this lesson, you should be able to:
- Define and implement a trait.
- Use trait bounds for static dispatch.
- Use trait objects (
dyn Trait) for runtime polymorphism.
Concepts in 5 minutes
traitdefines required behavior.impl Trait for Typeimplements that behavior.- Static dispatch:
fn f<T: Trait>(...)orimpl Trait. - Dynamic dispatch:
&dyn Trait/Box<dyn Trait>.
Python baseline snippets
def notify(service):
print(service.send("Build finished"))
from typing import Protocol
class Notifier(Protocol):
def send(self, msg: str) -> str: ...
Rust equivalent snippets
trait Notifier {
fn send(&self, msg: &str) -> String;
}
fn notify_static<T: Notifier>(service: &T, msg: &str) {
println!("{}", service.send(msg));
}
fn notify_dynamic(service: &dyn Notifier, msg: &str) {
println!("{}", service.send(msg));
}
One runnable end-to-end example
trait Notifier {
fn send(&self, msg: &str) -> String;
}
struct EmailNotifier {
from: String,
}
struct SlackNotifier {
channel: String,
}
impl Notifier for EmailNotifier {
fn send(&self, msg: &str) -> String {
format!("email from {}: {}", self.from, msg)
}
}
impl Notifier for SlackNotifier {
fn send(&self, msg: &str) -> String {
format!("slack #{}: {}", self.channel, msg)
}
}
// Static dispatch (monomorphized at compile time)
fn notify_static<T: Notifier>(service: &T, msg: &str) {
println!("[static] {}", service.send(msg));
}
// Dynamic dispatch (trait object)
fn notify_dynamic(service: &dyn Notifier, msg: &str) {
println!("[dynamic] {}", service.send(msg));
}
fn main() {
let email = EmailNotifier {
from: "noreply@pythontorust.nl".to_string(),
};
let slack = SlackNotifier {
channel: "engineering".to_string(),
};
notify_static(&email, "Build finished");
notify_static(&slack, "Tests passed");
let services: Vec<Box<dyn Notifier>> = vec![
Box::new(EmailNotifier {
from: "alerts@pythontorust.nl".to_string(),
}),
Box::new(SlackNotifier {
channel: "ops".to_string(),
}),
];
for service in services {
notify_dynamic(service.as_ref(), "Deployment started");
}
}
Static vs dynamic dispatch (practical)
- Use static dispatch for performance and simple generic APIs.
- Use dynamic dispatch when you need heterogeneous collections (
Vec<Box<dyn Trait>>) or plugin-like behavior.
Common mistakes
- Returning concrete types where trait-object abstraction is needed.
- Reaching for
dyn Traittoo early when generics are enough. - Forgetting object-safety constraints when designing traits for
dynusage.
Quick practice
- Add
SmsNotifierimplementingNotifier. - Create a function that broadcasts one message to a
Vec<Box<dyn Notifier>>.
Recap
Traits let Rust express polymorphism with explicit contracts. Choose static or dynamic dispatch based on API and performance needs.
Next lesson: concurrency part 1 (threads, channels, shared state).
Join the Journey Ahead!
If you're eager to continue this learning journey and stay updated with the latest insights, consider subscribing. By joining our mailing list, you'll receive notifications about new articles, tips, and resources to help you seamlessly pick up Rust by leveraging your Python skills.
Other articles in the series
- 000 - Learning Rust as a Pythonista: A Suggested Path
- 001 - Your First Rust Program
- 002 - Basic Syntax and Structure
- 003 - Ownership, Borrowing, and Lifetimes
- 004 - Error Handling
- 005 - Structs and Enums
- 006 - Iterators and Closures
- 008 - Concurrency in Rust for Python Developers
- 009 - Async Concurrency with Tokio
- 010 - Pattern Matching in Rust
- 011 - Macros in Rust