Week 12 overview
Session 1: generic types
Understanding the problem: code duplication
Generic functions and structs
Generic enums (Option, Result)
Generic methods and implementations
Introduction to trait bounds
Session 2: traits in more depth
Trait definitions and implementations
Default implementations and trait parameters
Trait bounds and where clauses
Common traits: Debug, Clone, Display, Iterator
Implementing traits for custom types
This week focuses on Rust’s two big abstraction tools: generics and traits. If you’re coming from Python, you’ve likely solved similar problems in very different ways.
Python vs. Rust abstraction philosophy:
In Python, you generally don’t worry about code reuse across types, because duck typing lets you write one function and call it with anything that has the right methods at runtime. Python will simply raise an AttributeError or TypeError if something goes wrong. This is flexible but pushes errors to runtime.
In Rust, the compiler needs to know at compile time what types are used and what operations they support. Generics and traits express “this code works for any type that provides these behaviors.” This catches many type errors before execution, and static dispatch can produce specialized code without a virtual-call cost.
Session 1 covers generics: how to write one function or struct that works across many types. Session 2 goes deeper on traits: how to define and enforce shared behavior contracts.
Keep the comparison scoped: Rust checks declared trait constraints at compile time, while Python normally relies on runtime behavior plus optional static analysis. Neither choice removes the need for tests.
Session 1: generics and traits
Understanding the problem
Without generics, you need duplicate code:
fn largest_i32(list: & [i32 ]) -> & i32 {
let mut largest = & list[0 ];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_f64(list: & [f64 ]) -> & f64 {
let mut largest = & list[0 ];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Try it yourself: Run in Rust Playground
Problem : Same logic, different types → code duplication!
In Python, this problem simply doesn’t exist at the function-writing level:
def largest(lst):
result = lst[0 ]
for item in lst:
if item > result:
result = item
return result
largest([34 , 50 , 25 , 100 ]) # works
largest([3.14 , 2.71 , 1.41 ]) # also works
largest(['y' , 'm' , 'a' ]) # also works!
Python’s duck typing lets you write one function that silently handles any type that supports >. There’s no duplication problem.
But notice: what happens if you pass an object that doesn’t support >? Python raises a TypeError at runtime , so you only find out when that code path is actually executed. In a large application, that might not happen until you’re in production.
Rust forces you to think about this upfront. The two largest_i32/largest_f64 functions show what you’d be stuck with without generics: copy-paste programming that’s painful to maintain. Ask students: what if you needed to fix a bug in this logic, you’d have to fix it in every copy!
This is the motivating problem generics solve.
Introducing generics
Generic functions work with multiple types:
fn largest< T: PartialOrd > (list: & [T]) -> & T {
let mut largest = & list[0 ];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
// Works with any type that can be compared!
let numbers = vec! [34 , 50 , 25 , 100 , 65 ];
let result = largest(& numbers);
let chars = vec! ['y' , 'm' , 'a' , 'q' ];
let result = largest(& chars);
Try it yourself: Run in Rust Playground
Key concept : T: PartialOrd is a trait bound - T must implement PartialOrd.
Rust Book: Generic Data Types
Break down the syntax carefully, because it’s dense the first time you see it:
<T> after the function name declares that T is a generic type parameter. It’s like saying “let T stand for any type.”
T: PartialOrd is the trait bound : it says “T can be any type, but it must implement the PartialOrd trait,” which means it supports <, >, <=, >= comparisons.
list: &[T] is a slice of T values (a reference to a contiguous sequence).
The return type &T is a reference to a T value inside that slice.
Python parallel: The closest Python equivalent is a type annotation with TypeVar:
from typing import TypeVar
T = TypeVar('T' )
def largest(lst: list [T]) -> T:
...
But again, Python doesn’t enforce this at runtime. In Rust, if you try to call largest with a type that doesn’t implement PartialOrd, the compiler rejects it immediately with a helpful error message.
Monomorphization: When Rust compiles largest::<i32> and largest::<char>, it generates two completely separate machine code functions: one for i32, one for char. You write it once, but the compiler produces the type-specific versions. This is exactly as fast as largest_i32 and largest_char written by hand.
Generic structs
Define structs that work with any type:
struct Point< T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point { x: 5 , y: 10 };
let float_point = Point { x: 1.0 , y: 4.0 };
}
Try it yourself: Run in Rust Playground
Multiple type parameters:
struct Point< T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5 , y: 4.0 }; // T=i32, U=f64
}
Try it yourself: Run in Rust Playground
Rust Book: Generic Structs
Python parallel: An unparameterized Python dataclass may accept values dynamically. A generic dataclass can express relationships with TypeVar, but enforcement normally comes from a static type checker rather than the runtime:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
In Python you can put x=1 and y=4.0 without issue (int and float) because Python doesn’t enforce type annotations at runtime. In Rust with Point<T>, if you declare Point { x: 5, y: 4.0 }, Rust will reject it because 5 is an integer and 4.0 is a float, and they would need to be the same type T.
That’s where Point<T, U> comes in: it says x and y can be different types. This is explicit, controlled flexibility, and you are in charge of what is allowed.
Syntax note: The <T> and <T, U> always appear right after the struct name in the definition. When you create an instance, Rust usually infers the type from the values you provide, so you rarely need to write Point::<i32, f64> { x: 5, y: 4.0 } explicitly.
Generic enums
You’ve been using generic enums all along:
// Option<T> from standard library
enum Option < T> {
Some (T),
None ,
}
// Result<T, E> for error handling
enum Result < T, E> {
Ok (T),
Err (E),
}
Try it yourself: Run in Rust Playground
Usage:
fn validate_username(name: & str ) -> Result <& str , String > {
if name. trim(). is_empty() {
Err (String :: from("Username cannot be empty" ))
} else {
Ok (name)
}
}
Try it yourself: Run in Rust Playground
Rust Book: Generic Enums
Students have been writing Option<T> and Result<T, E> for weeks. Use their definitions to show that these are generic standard-library enums rather than special syntax.
Python parallel: Python’s equivalent to Option<T> is Optional[T] (or T | None in modern Python). But in Python, None is a valid value for any variable, so you don’t get a compile-time guarantee that you’ve handled the null case. Rust’s Option<T> forces you to explicitly handle Some(value) and None via pattern matching.
Python commonly reports comparable failures by raising exceptions, which are not normally declared in the function’s type. A Rust function returning Result<T, E> advertises an expected failure channel in its signature; code must inspect or transform the result to access the success value.
Connecting to the validate_username example: Python code might raise an exception or return a sentinel value. The Rust version makes the expected failure part of the function’s public contract. Ask: which approach makes code easier to maintain and reason about?
Generic methods
Methods on generic structs:
struct Point< T> {
x: T,
y: T,
}
impl < T> Point< T> {
fn x(& self ) -> & T {
& self . x
}
}
// Method only for specific type
impl Point< f32 > {
fn distance_from_origin(& self ) -> f32 {
(self . x. powi(2 ) + self . y. powi(2 )). sqrt()
}
}
Try it yourself: Run in Rust Playground
Notice : impl<T> declares the generic, then Point<T> uses it.
Rust Book: Generic Methods
The impl<T> Point<T> syntax is one of the most confusing parts for newcomers. Break it down:
impl<T>: “I’m about to implement something, and T is a generic parameter I’ll use”
Point<T>: “I’m implementing it for the type Point<T> where T matches what I just declared”
You need to declare <T> on the impl so Rust knows that the T in Point<T> is a generic parameter (not some concrete type named T).
Python parallel: This is similar to defining methods inside a Python class:
class Point:
def __init__ (self , x, y):
self .x = x
self .y = y
def x_coord(self ): # works for any x type
return self .x
# Only relevant for numeric types, but Python won't enforce this:
def distance_from_origin(self ):
return (self .x** 2 + self .y** 2 ) ** 0.5
In Python, distance_from_origin will crash at runtime if x and y aren’t numbers. In Rust, impl Point<f32> limits that method to only Point<f32> instances: if you have a Point<i32> and try to call .distance_from_origin(), the compiler rejects it. That’s a meaningful safety guarantee.
This pattern adds methods to one concrete instantiation while keeping the core interface generic.
What are traits?
Traits define shared behavior:
pub trait Summary {
fn summarize(& self ) -> String ;
}
struct NewsArticle {
headline: String ,
content: String ,
}
impl Summary for NewsArticle {
fn summarize(& self ) -> String {
format! ("{}: {}" , self . headline, self . content)
}
}
Try it yourself: Run in Rust Playground
Think of traits as: - Interfaces (Java/C#) - Protocols (Swift) - Type classes (Haskell)
Rust Book: Traits
Python parallel, Abstract Base Classes (ABCs): The closest Python equivalent is abc.ABC:
from abc import ABC, abstractmethod
class Summary(ABC):
@abstractmethod
def summarize(self ) -> str :
pass
class NewsArticle(Summary):
def __init__ (self , headline, content):
self .headline = headline
self .content = content
def summarize(self ) -> str :
return f" { self . headline} : { self . content} "
The concepts map closely: - trait Summary ↔︎ class Summary(ABC) - fn summarize(&self) -> String ↔︎ @abstractmethod def summarize(self) -> str - impl Summary for NewsArticle ↔︎ class NewsArticle(Summary)
A better Python parallel, Protocols (Python 3.8+): Python’s typing.Protocol is actually closer to Rust traits because it uses structural typing (duck typing with type checking), not class inheritance:
from typing import Protocol
class Summary(Protocol):
def summarize(self ) -> str : ...
The key difference: Rust trait implementation is nominal : a type explicitly uses impl Trait for Type. Python protocols are structural : a static checker can accept a class with the required members even when it never names the protocol.
Rust structs do not form class-inheritance hierarchies. Traits express shared behavior, while enums, composition, and generic parameters model other relationships.
Default trait implementations
Traits can provide default behavior:
pub trait Summary {
fn summarize_author(& self ) -> String ;
fn summarize(& self ) -> String {
format! ("(Read more from {}...)" , self . summarize_author())
}
}
struct Tweet {
username: String ,
content: String ,
}
impl Summary for Tweet {
fn summarize_author(& self ) -> String {
format! ("@{}" , self . username)
}
// summarize() uses the default implementation
}
Try it yourself: Run in Rust Playground
Rust Book: Default Implementations
Python parallel: This is exactly like providing a non-abstract method in a Python ABC or base class:
from abc import ABC, abstractmethod
class Summary(ABC):
@abstractmethod
def summarize_author(self ) -> str :
pass
def summarize(self ) -> str : # default implementation
return f"(Read more from { self . summarize_author()} ...)"
class Tweet(Summary):
def __init__ (self , username, content):
self .username = username
self .content = content
def summarize_author(self ) -> str :
return f"@ { self . username} "
# summarize() is inherited from the base class
In both Python and Rust, you can: 1. Define a required method (summarize_author): must be implemented by each type 2. Define a default method (summarize): uses the required method, can be overridden
An important Rust pattern: Default implementations can call other methods in the same trait, even ones that don’t have defaults. This lets you build up complex behavior from a few required “primitive” methods. The Iterator trait is the ultimate example: you implement only next(), and you get dozens of methods like map, filter, zip, and enumerate for free.
Overriding defaults: Just like Python, if a type wants different behavior, it can provide its own summarize() implementation. The custom implementation takes precedence.
The where clause
Make complex trait bounds readable:
// Hard to read:
fn some_function< T: Display + Clone , U: Clone + Debug > (t: & T, u: & U) -> i32 {
// ...
}
// Much clearer with where clause:
fn some_function< T, U> (t: & T, u: & U) -> i32
where
T: Display + Clone ,
U: Clone + Debug ,
{
// ...
}
Use where when: - Multiple trait bounds - Complex generic relationships - Improves readability
Rust Book: where Clauses
There’s no real Python equivalent for where clauses because Python doesn’t have the same richness of compile-time type constraints. Think of it purely as a formatting/readability feature.
The problem it solves: Once you start writing real Rust with multiple type parameters and multiple bounds, function signatures can get very long:
fn some_function< T: Display + Clone + Debug + PartialEq , U: Clone + Iterator + Debug > (t: & T, u: & U) { ... }
That’s nearly unreadable! The where clause lets you separate the generic declarations from the bounds:
fn some_function< T, U> (t: & T, u: & U)
where
T: Display + Clone + Debug + PartialEq ,
U: Clone + Iterator + Debug ,
{ ... }
Rule of thumb: If the bounds fit comfortably on one line with the function signature, use inline syntax. If they don’t, use where.
More advanced uses (preview for curious students): where clauses also support associated type bounds like where T::Item: Display, which you can’t express inline. These become important when working with the Iterator trait.
Emphasize that both styles compile to exactly the same code. This is purely about making the code readable for humans.
Returning trait types
Return types that implement traits:
fn returns_summarizable() -> impl Summary {
Tweet {
username: String :: from("rustacean" ),
content: String :: from("Rust is awesome!" ),
}
}
Limitation : Can only return ONE concrete type:
// ❌ ERROR: Can't return different types
fn returns_summarizable(switch: bool ) -> impl Summary {
if switch {
NewsArticle { /* ... */ }
} else {
Tweet { /* ... */ } // Error!
}
}
Rust Book: Returning Traits
Python parallel: Python return type annotations can express this naturally:
def returns_summarizable() -> Summary:
return Tweet(username= "rustacean" , content= "Rust is awesome!" )
And Python has no problem returning different types from different branches: the duck typing just works. This is an area where Rust is more restrictive.
Why the limitation exists: A return-position impl Summary represents one hidden concrete return type. Returning NewsArticle from one branch and Tweet from another would select two concrete types, so this signature does not accept it.
The fix for the limitation (preview, don’t go deep here): Box<dyn Summary> uses a trait object : a pointer with a vtable for runtime dispatch, like Python’s normal object dispatch. This has a small runtime cost but enables returning different types:
fn returns_summarizable(switch: bool ) -> Box < dyn Summary> {
if switch {
Box :: new(NewsArticle { /* ... */ } )
} else {
Box :: new(Tweet { /* ... */ } ) // Works!
}
}
impl Trait = compile-time dispatch (faster, like a C++ template) dyn Trait = runtime dispatch (more flexible, like Python objects)
Students will encounter dyn Trait naturally as they write more complex Rust. Mention it briefly here but don’t go deep: they’ll see it next week or when they hit the limitation in their projects.
Common standard library traits
Frequently used traits you should know:
Debug : Format with {:?} (derive with #[derive(Debug)])
Clone : Create deep copies with .clone()
Copy : Types that can be copied by just copying bits
Display : Format with {} (implement manually)
PartialEq : Compare with == and !=
Eq : Full equivalence relation
PartialOrd : Compare with <, >, <=, >=
Ord : Total ordering
Iterator : Types that can be iterated over
Rust Standard Library Traits
Help students map these to Python’s “dunder” (double underscore) methods, which serve a similar role: they’re special interfaces the language and standard library know about:
Debug
__repr__
Programmer-facing string representation ({:?})
Display
__str__
User-facing string representation ({})
Clone
copy.deepcopy()
Explicit deep copy
Copy
(automatic for primitives)
Implicit bitwise copy, no move semantics
PartialEq
__eq__
== and != operators
Eq
(full equivalence, no NaN)
Required by HashMap keys, sort stability
PartialOrd
__lt__/__gt__/etc.
<, >, <=, >= operators
Ord
__lt__ + total order
sort(), min(), max()
Iterator
__iter__ + __next__
for loops, iterator adapters
Hash
__hash__
Use as HashMap/HashSet key
Default
__init__ defaults
Type::default() constructor
PartialEq vs Eq: Python doesn’t have this distinction. In Rust, PartialEq allows values that aren’t equal to themselves (like f32::NAN != f32::NAN). Eq means every value is equal to itself: a stronger guarantee. Types need Eq to be used as HashMap keys.
PartialOrd vs Ord: Same pattern. f32 only implements PartialOrd because NaN comparisons are undefined. i32 implements Ord because integers have a total, unambiguous ordering.
This is a great reference slide to return to throughout the semester.
Deriving traits
Automatically implement common traits:
#[ derive( Debug , Clone , PartialEq , Eq )]
struct Point {
x: i32 ,
y: i32 ,
}
fn main() {
let p1 = Point { x: 1 , y: 2 };
let p2 = p1. clone();
println! ("{:?}" , p1); // Debug
assert_eq! (p1, p2); // PartialEq
}
Try it yourself: Run in Rust Playground
Can derive: - Debug, Clone, Copy - PartialEq, Eq, PartialOrd, Ord - Hash, Default
Cannot derive : Display, Iterator (must implement manually)
Rust Book: Derivable Traits
Python parallel: The #[derive(...)] attribute is most similar to Python’s @dataclass decorator, which auto-generates __init__, __repr__, __eq__, and optionally __hash__ and __lt__:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
# Automatically gets __repr__, __eq__, and field-based __init__
@dataclass (order= True )
class Point:
x: int
y: int
# Also gets __lt__, __le__, __gt__, __ge__
The Rust #[derive(Debug, Clone, PartialEq)] is conceptually the same: the compiler auto-generates the trait implementation based on the struct’s fields.
How derive works: The derived implementation is field-recursive . For PartialEq, two Points are equal if and only if both x fields are equal AND both y fields are equal. For Debug, it formats each field. This works as long as all the fields themselves implement the trait.
Why can’t Display and Iterator be derived? Because there’s no obvious “default” for user-facing formatting: should Point display as (1, 2) or Point { x: 1, y: 2 } or 1,2? That’s a design decision only you can make. Similarly, there’s no default for what “the next item” means for a custom type.
Common gotcha: If any field doesn’t implement the trait you’re trying to derive, the derive will fail with a compiler error. For example, you can’t #[derive(Clone)] on a struct that contains a File handle, because File doesn’t implement Clone.
Implementing the Display trait
Custom formatting for user-facing output:
use std:: fmt;
struct Point {
x: i32 ,
y: i32 ,
}
impl fmt:: Display for Point {
fn fmt(& self , f: & mut fmt:: Formatter) -> fmt:: Result {
write! (f, "({}, {})" , self . x, self . y)
}
}
fn main() {
let p = Point { x: 1 , y: 2 };
println! ("Point: {}" , p); // Point: (1, 2)
}
Try it yourself: Run in Rust Playground
Rust Book: Display Trait
Python parallel: Implementing Display is almost identical to Python’s __str__:
class Point:
def __init__ (self , x, y):
self .x = x
self .y = y
def __str__ (self ):
return f"( { self . x} , { self . y} )"
p = Point(1 , 2 )
print (f"Point: { p} " ) # Point: (1, 2)
And Python’s __repr__ maps to Rust’s Debug trait:
def __repr__ (self ):
return f"Point(x= { self . x} , y= { self . y} )"
Rust-specific details in the code:
use std::fmt brings the formatting traits into scope, required because fmt::Display is in a sub-module
impl fmt::Display for Point: you’re implementing the Display trait from the fmt module for your Point type
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result: this is the exact signature the trait requires; you must match it exactly
write!(f, "({}, {})", self.x, self.y): write! is a macro that writes formatted text to the formatter f. It returns fmt::Result, which your function returns directly.
When to implement each: - Debug (often via #[derive(Debug)]): when diagnostic formatting is useful - Display (manual): when you have a user-facing string representation that makes semantic sense for your type
Automatic bonus: Once a type implements Display, it automatically implements the ToString trait, which gives you a .to_string() method for free.
Implementing the Iterator trait
Make your types iterable:
struct Counter {
count: u32 ,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32 ; // Associated type
fn next(& mut self ) -> Option < Self :: Item> {
if self . count < 5 {
self . count += 1 ;
Some (self . count)
} else {
None
}
}
}
Try it yourself: Run in Rust Playground
Rust Book: Iterator Trait
Python parallel: This is exactly how Python iterators work, with __iter__ and __next__:
class Counter:
def __init__ (self ):
self .count = 0
def __iter__ (self ):
return self
def __next__ (self ):
if self .count < 5 :
self .count += 1
return self .count
else :
raise StopIteration
The mapping is very direct:
__iter__(self) returning self
impl Iterator for Counter declaration
__next__(self)
fn next(&mut self)
raise StopIteration
return None
return value
return Some(value)
Associated types: The type Item = u32; line selects the item type for this implementation of Iterator. This design makes one Item type part of each implementation’s trait contract.
Provided adaptors: Once you implement next(), the Iterator trait’s provided methods become available on Counter: - .map(), .filter(), .fold(), .collect() - .zip(), .enumerate(), .take(), .skip() - .sum(), .product(), .count() - And many more!
This resembles Python’s __iter__/__next__: implement the primitive operation and gain provided adaptor methods. Rust’s adaptors are lazy and often inline into efficient loops, but terminal operations such as collect() may allocate and performance still depends on the full pipeline.
Generic stack example
Building a generic data structure:
struct Stack< T> {
items: Vec < T>,
}
impl < T> Stack< T> {
fn new() -> Stack< T> {
todo! ("construct an empty stack" )
}
fn push(& mut self , item: T) {
todo! ("place item on top" )
}
fn pop(& mut self ) -> Option < T> {
todo! ("remove and return the top item" )
}
fn is_empty(& self ) -> bool {
todo! ("report whether the stack has no items" )
}
}
Try it in Playground
Python parallel: Python’s built-in list already acts as a stack. You’d normally just use it directly:
stack = []
stack.append(42 ) # push
stack.pop() # pop (returns and removes last element)
stack[- 1 ] # peek (look at top without removing)
len (stack) == 0 # is_empty
In Rust, this is a teaching example: the purpose is to practice generics and traits, not to build something Python doesn’t have. That said, a Rust Stack<T> has a real advantage: it’s a type-safe contract . A Stack<i32> can only hold i32s. Python lists can hold anything mixed together.
Walk through the signatures:
struct Stack<T>: generic struct that wraps a Vec<T>
impl<T> Stack<T>: implementing methods for all T; remember the impl<T> declares the generic before Stack<T> uses it
fn new() -> Stack<T>: associated function that returns an empty stack
fn push(&mut self, item: T): mutably borrows the stack and takes ownership of item
fn pop(&mut self) -> Option<T>: returns None when the stack is empty
fn is_empty(&self): needs only an immutable borrow
Lab preview: Students will extend this with peek and len, then implement Display for it. The supplied tests cover the required behavior.
Session 2: traits in more depth
Traits: defining shared behavior
What we’ll cover:
Trait definitions and custom traits
Implementing traits for your types
Default implementations for code reuse
Traits as function parameters
Advanced trait bounds with where clauses
Common standard library traits
Goal : Read and write trait implementations, bounds, and generic interfaces
Session 2 is a deeper pass at traits, revisiting the concepts from Session 1 with more detail and more examples. Use this slide to quickly re-orient students after the break.
Key message to set up Session 2: Python commonly uses inheritance, protocols, and duck typing for polymorphism. Rust has no class hierarchy; traits express shared behavior and support both static and dynamic dispatch. Compare the designs in context rather than treating one as universally clearer.
The four concepts to emphasize this session: 1. Writing your own traits alongside implementations of standard traits 2. Default implementations for reuse without inheritance 3. Using traits to write flexible function signatures 4. Common standard-library traits that appear throughout Rust code
By the end of Session 2 students should feel comfortable reading and writing impl Trait for Type blocks and understanding what trait bounds in function signatures are saying.
Lab 12 preview: generic stack
What you’ll build:
Generic Stack<T> data structure
Implement core methods: new, push, pop, peek, is_empty, len
Implement Display trait for custom formatting
Use the supplied test suite without modifying it
Learning goals:
Apply generics to create reusable data structures
Implement standard library traits
Understand trait bounds in practice
Read tests for generic types and use their failures as evidence
Where traits and generics are used
Why these skills matter:
Generics: - Foundation of modern language features (Java, C#, TypeScript, Swift, Go) - Reusable library and application interfaces - Constraints and bounds that document supported operations
Traits/Interfaces: - Interface design skills transfer to all languages - Composition as an alternative to inheritance hierarchies - Extensible interfaces and selected plugin architectures - Explicit reasoning about static and dynamic dispatch
Questions for review: - “Explain the difference between generics and dynamic dispatch” - “Design an interface for [problem] - what methods would it have?” - “How would you make this code reusable across different types?” - “What are the tradeoffs between compile-time and runtime polymorphism?”
Use the comparison to sharpen specific questions: Which operations does the function require? Who checks that contract? At what stage does a mismatch appear? Avoid mapping Rust’s type system to Python’s separate EAFP-versus-LBYL runtime style choice.
For the generics-versus-dynamic-dispatch question: Rust’s statically dispatched generics are typically monomorphized, which can enable optimization but increase code size. Rust trait objects use a vtable for runtime dispatch, trading some optimization opportunities for runtime flexibility. Measure when performance matters and explain when each representation fits the design.
Languages to connect this to: - Java/C#: Similar parameter syntax, but different runtime representations and variance rules - TypeScript: Generic and structural interfaces checked by the TypeScript compiler - Go: Structural interface satisfaction rather than Rust’s explicit trait implementation - Haskell: Type classes as a useful comparison for behavior constraints
Ask students to name one similarity and one difference before transferring the Rust mental model to another language.
Key takeaways
Generics: - Enable code reuse without sacrificing type safety - Static dispatch can avoid a virtual-call cost; measure the complete program when performance matters - Work with structs, enums, functions, and methods - Use trait bounds to constrain type parameters
Traits: - Define shared behavior across types - Enable polymorphism without inheritance - Foundation of Rust’s standard library - Can have default implementations for convenience
Together: - Generics and traits support reusable abstractions with compile-time constraints - Type safety at compile time - Explicit reusable interfaces with static or dynamic dispatch choices - Patterns used throughout Rust’s standard library and ecosystem
Use this slide to close the week’s Python-to-Rust comparison.
The core insight for Python developers:
Python’s dynamic runtime and optional static typing support rapid iteration but move some mismatches to tests or execution. Rust requires more contracts at compile time and can reject many type mismatches before execution. Both still need tests for behavior the type system does not express.
Neither approach is universally better. Python’s expressiveness is genuinely valuable for rapid prototyping, data science, and scripts. Rust’s guarantees are genuinely valuable for systems programming, performance-critical code, and large teams where the compiler catches whole classes of bugs.
Quick mental map to leave students with:
Duck typing: any method that exists works
Trait bounds: required methods declared explicitly
Optional[T] type hint
Option<T> enforced by compiler
Exception-based error handling
Result<T, E> in the type signature
__str__ / __repr__
Display / Debug traits
__eq__ / __lt__
PartialEq / PartialOrd traits
__iter__ / __next__
Iterator trait with next()
ABC abstract methods
Required (non-default) trait methods
ABC concrete methods
Default trait implementations
@dataclass
#[derive(...)]
This table is a useful cheat sheet: consider posting it in the course Teams channel.
Next week: idiomatic Rust
Week 13 topics:
Iterators and closures
Smart pointers: Box, Rc, and RefCell
Idiomatic error handling with Result
A new Lab 13 assignment
Why it matters : These patterns help express reusable behavior while preserving Rust’s compile-time checks.
Focus : Transfer this week’s trait and generic foundations into idiomatic Rust patterns.
Week 13’s topics build naturally on this week. A few connections to foreshadow:
Iterators: Today’s separate Counter example provides the bridge to Week 13’s iterator pipelines and closures.
Error handling with Result: Students have seen Result<T, E> as a generic enum; Week 13 develops that pattern more idiomatically.
Smart pointers: Trait bounds and generic types remain central when students work with Box, Rc, and RefCell.
Lab 13 continues the weekly cadence. Encourage students to finish Lab 12’s Display implementation before moving on.
Questions?
Get help:
Office hours: Schedule on Teams
Course discussion: Microsoft Teams channel
AI tools: browser chat, GitHub Copilot, Copilot CLI, or Antigravity CLI
Rust community: users.rust-lang.org
This week’s materials: