In this article, we’ll dive into Traits in Rust and compare them to Python’s Duck Typing and Protocols. While Python relies on dynamic typing and flexibility to work with different object types, Rust uses Traits to enforce type safety at compile time. These two approaches reflect the different philosophies of both languages, with Python emphasizing ease of use and flexibility, and Rust focusing on safety and performance.
Python follows the principle of Duck Typing, which means that as long as an object behaves like a certain type (e.g., has the necessary methods), it can be used in place of that type. Python doesn’t require explicit type declarations to define what an object should look like.
class Duck:
def quack(self):
return "Quack!"
class Dog:
def quack(self):
return "I'm a dog, but I can quack!"
def make_quack(animal):
print(animal.quack())
duck = Duck()
dog = Dog()
make_quack(duck) # Output: Quack!
make_quack(dog) # Output: I'm a dog, but I can quack!
In this example, both the Duck
and Dog
classes have a quack
method. Even though the Dog isn’t a duck, Python allows it to be passed to make_quack
because it has the required method.
In Rust, the concept of Traits is somewhat similar to Python’s duck typing, but with stricter rules. Traits are a way to define shared behavior across types, allowing you to enforce that certain types implement specific methods. Unlike Python, Rust does this at compile time, ensuring type safety and reducing runtime errors.
Here’s an example of a simple trait in Rust and how it can be implemented for different types:
trait Quack {
fn quack(&self) -> String;
}
struct Duck;
struct Dog;
impl Quack for Duck {
fn quack(&self) -> String {
String::from("Quack!")
}
}
impl Quack for Dog {
fn quack(&self) -> String {
String::from("I'm a dog, but I can quack!")
}
}
fn make_quack(animal: &impl Quack) {
println!("{}", animal.quack());
}
fn main() {
let duck = Duck;
let dog = Dog;
make_quack(&duck); // Output: Quack!
make_quack(&dog); // Output: I'm a dog, but I can quack!
}
Rust’s traits are often used in conjunction with Generics, allowing you to write functions that can operate on different types as long as they implement a specific trait. This is Rust’s approach to achieving polymorphism.
fn make_quack<T: Quack>(animal: T) {
println!("{}", animal.quack());
}
fn main() {
let duck = Duck;
let dog = Dog;
make_quack(duck); // Output: Quack!
make_quack(dog); // Output: I'm a dog, but I can quack!
}
This allows the make_quack
function to accept any type that implements the Quack
trait, similar to how Python would accept any object with a quack
method.
In Python 3.8, Protocols were introduced (via PEP 544) to provide a static way to describe structural typing, similar to Rust’s traits. Protocols allow you to define types that match certain method signatures, and with type hinting, you can statically check if objects conform to the protocol.
from typing import Protocol
class Quackable(Protocol):
def quack(self) -> str:
...
class Duck:
def quack(self):
return "Quack!"
class Dog:
def quack(self):
return "I'm a dog, but I can quack!"
def make_quack(animal: Quackable):
print(animal.quack())
duck = Duck()
dog = Dog()
make_quack(duck) # Output: Quack!
make_quack(dog) # Output: I'm a dog, but I can quack!
In this example, Quackable
is a protocol that specifies that any class passed to make_quack
must implement a quack
method. This brings Python closer to Rust’s trait system but still remains more dynamic.
mypy
. In Rust, traits are part of the core language and cannot be skipped.In Python, you can pass almost any object to a function as long as it behaves in a certain way (duck typing). In Rust, you can use Trait Bounds to constrain what types a function can accept, making the requirements for the function explicit and enforceable.
fn make_quack<T: Quack>(animal: T) {
println!("{}", animal.quack());
}
In this example, the function make_quack
can only accept types that implement the Quack
trait. This is similar to Python’s duck typing, but with compile-time checks ensuring that the type adheres to the trait’s requirements.
def make_quack(animal):
print(animal.quack())
In Python, make_quack
can accept any object, and it will only fail at runtime if the object doesn’t have a quack
method. This makes Python more flexible but also more prone to runtime errors.
Rust supports both static dispatch (at compile time) and dynamic dispatch (at runtime) through Trait Objects. If you want to allow for more dynamic behavior, you can use trait objects (&dyn Trait
) to allow for polymorphism at runtime, similar to how Python handles object behavior dynamically.
fn make_quack(animal: &dyn Quack) {
println!("{}", animal.quack());
}
This allows make_quack
to accept any type that implements the Quack
trait, but at runtime, the specific method implementation is determined dynamically.
Rust’s Traits provide a powerful way to define shared behavior between types with compile-time safety, while Python’s Duck Typing allows for more dynamic and flexible code but relies on runtime checks. Python’s Protocols (introduced in Python 3.8) bring some of the advantages of Rust’s trait system to Python by allowing static type checks for behavior, though the enforcement remains optional.
While Python’s duck typing makes the language easy to use and flexible, Rust’s trait system gives you strict guarantees about the behavior of types, leading to safer and more performant code.
In the next article, we’ll explore Concurrency in Rust, comparing it with Python’s asyncio
and concurrent.futures
. Stay tuned!
trait Quack {
fn quack(&self) -> String;
}
struct Duck;
struct Dog;
impl Quack for Duck {
fn quack(&self) -> String {
String::from("Quack!")
}
}
impl Quack for Dog {
fn quack(&self) -> String {
String::from("I'm a dog, but I can quack!")
}
}
// Function that accepts any type implementing the Quack trait using dynamic dispatch
fn make_quack(animal: &dyn Quack) {
println!("{}", animal.quack());
}
fn main() {
let duck = Duck;
let dog = Dog;
make_quack(&duck); // Output: Quack!
make_quack(&dog); // Output: I'm a dog, but I can quack!
}
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.
000 - Learning Rust as a Pythonista: A Suggested Path
001 - Learning Rust as a Pythonista: How to Create and Run a Rust File
002 - Learning Rust as a Pythonista: Basic Syntax and Structure
007 - Concurrency in Rust for Python Developers