Rust Generics and Lifetimes Explained: Generic T, Trait Bounds and 'a
November 10, 2026
Welcome to episode 19 of Rust for Beginners, a series for complete beginners (no programming experience needed). In the last video, we learned about traits. Today, we'll learn generics: how to write code once, for many types. And at the end, a gentle first look at lifetimes. Every code block below is the exact code from the video, ready to copy.
- All episodes are in the Rust Tutorial for Beginners playlist.
- Versions used: Rust 1.99.1 on Ubuntu, VS Code with rust-analyzer.
1. New project
Let's create a new project called generics, and open it in VS Code.
cargo new generics
cd generics2. Generic fn
We want one function that finds the largest item in a list of numbers, and also in a list of letters. So instead of a real type, we use a placeholder, T, in angle brackets.
src/main.rs:
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = [34, 50, 25, 100, 65];
let letters = ['y', 'm', 'a', 'q'];
println!("Largest number: {}", largest(&numbers));
println!("Largest letter: {}", largest(&letters));
}Error:
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:4:17
|
4 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T` with trait `PartialOrd`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++Stage A2 โ line 1 becomes fn largest<T: PartialOrd>(list: &[T]) -> &T {
Output: Largest number: 100 / Largest letter: y
3. Error
Let's run it. Rust says the greater-than sign can't be used on T.
cargo run4. Bound
T could be any type, and not every type can be compared. The help tells us the fix: require the PartialOrd trait. That's called a trait bound.
5. Fix
Let's add colon PartialOrd after T. Now T can be any type that can be compared.
6. Run A
Let's run it. One function, two types.
cargo run7. Generic struct
Structs can be generic too. A Point of T has an x and a y of the same type T.
struct Point<T> {
x: T,
y: T,
}
fn main() {
let ints = Point { x: 5, y: 10 };
let floats = Point { x: 1.5, y: 4.0 };
let mixed = Point { x: 5, y: 4.0 };
println!("{} {} {}", ints.x, floats.y, mixed.x);
}error[E0308]: mismatched types
--> src/main.rs:9:34
|
9 | let mixed = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number8. Same T
So what about x five and y four point zero?
cargo run9. Why
x made T an integer, so y must be an integer too. One T means one type.
10. impl<T>
Let's remove the mixed point. Methods on a generic struct use impl of T. Then Points of integers and Points of floats both get the method.
cargo runstruct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let ints = Point { x: 5, y: 10 };
let floats = Point { x: 1.5, y: 4.0 };
println!("ints: {}, {}", ints.x(), ints.y);
println!("floats: {}, {}", floats.x(), floats.y);
}Output: ints: 5, 10 / floats: 1.5, 4
11. Lifetimes
Now, lifetimes. longest returns the longer of two string slices. Let's run it.
cargo runfn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let first = String::from("Rust");
let second = String::from("Ferris");
println!("Longest: {}", longest(&first, &second));
}error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++Stage C2 โ line 1 becomes fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
Output: Longest: Ferris
12. The question
Rust asks: the result is borrowed, but from a or from b? It needs to know how long the result stays valid.
13. 'a
We add a lifetime parameter, tick a. It says: the result lives as long as both inputs. We don't change how long anything lives. We just describe it, so Rust can check.
14. Run C
Let's run it. Ferris is longer.
cargo run15. Check
Now the check in action: second dies at the end of the inner block, but we use result after it.
cargo runfn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let first = String::from("Rust");
let result;
{
let second = String::from("Ferris");
result = longest(&first, &second);
}
println!("Longest: {result}");
}error[E0597]: `second` does not live long enough
--> src/main.rs:10:34
|
9 | let second = String::from("Ferris");
| ------ binding `second` declared here
10 | result = longest(&first, &second);
| ^^^^^^^ borrowed value does not live long enough
11 | }
| - `second` dropped here while still borrowed
12 | println!("Longest: {result}");
| ------ borrow later used here16. Error
second does not live long enough. Rust caught a dangling reference before the program ever ran.
Recap
To recap: generics like T let one function or struct work with many types. Trait bounds say what T must be able to do. And lifetimes like tick a tell Rust how long references stay valid.