A quick heads-up before diving in: this lesson — and the two directly after it — mark a real step up in difficulty. Generics, traits, and lifetimes are where Rust stops feeling like "a careful, stricter cousin of C" and starts feeling like its own thing entirely — where the type system stops merely catching your mistakes and starts actively helping you design better code. If the next few lessons take a little longer to click than the ones behind you, that isn't a sign you're falling behind. It's just an honest description of this particular stretch of the language, true for absolutely everyone who has ever learned it.
Here's the first thing worth knowing, though, before bracing yourself any further: you have already been using the central idea of today's lesson since lesson 14. You simply didn't have a name for it yet.
A shape you've already met, eight lessons running
Every Option<T> you've matched against. Every Result<T, E> you sent climbing upward with ? last lesson. Every Vec<T> you've ever pushed a value into. That <...> — angle brackets wrapped around a single capital letter — is generics. You've been reading it, writing it, and quietly trusting it ever since the enums lesson. Today, you finally find out exactly what it means. And — this is the genuinely enjoyable part — you start writing your own.
The problem, made itchy on purpose
Picture a function that hunts through a list and hands back the largest value inside it. Now picture needing that exact same logic for a list of whole numbers — and, completely separately, for a list of characters:
Read both of those slowly — properly, side by side. Every single line is identical, character for character, except for two small words: i32 and char. The comparison, the loop, the bookkeeping, the return — all of it copy-pasted, with a find-and-replace pass run over the type names. Now ask the question this example exists to provoke: what happens the day you need this exact same logic for a list of f64s? For a list of your own Point values (you'll build that very type in a few minutes)? Copy, paste, rename, again? That itch — the small, nagging feeling that you're about to type something the compiler should honestly be smart enough to figure out on its own — is precisely the itch generics exist to scratch.
One function. Any type that fits.
Read the signature slowly: fn largest<T: PartialOrd>(list: &[T]) -> &T. That T — short for "type," and you could honestly call it anything; T is just the field's longstanding, friendly convention — means "some type, not yet decided, to be filled in later by whoever actually calls this function." Wherever i32 and char once sat, doing two separate jobs in two separate functions, there now sits a single placeholder, doing that one job for absolutely anything that fits. The two functions from a moment ago haven't merely been merged. They've been replaced by something that covers every type they covered — plus every type neither of them ever could.
The bound — and a word about to matter a great deal
That : PartialOrd riding alongside T is called a trait bound, and it is doing real, load-bearing work — delete it, and the function stops compiling at the very first > inside the loop. Which makes complete sense the moment you say it slowly: T could be anything at all — including plenty of types that have no sensible notion of "greater than" whatsoever. The compiler isn't being difficult here. It's refusing to compile a comparison it cannot actually guarantee will make sense for whatever type eventually shows up.
Here's a sentence worth sitting with for a moment, because it's about to become the entire subject of the very next lesson: a trait bound is really just a precise, honest way of saying "T can be any type in the entire world — so long as it can do this one specific thing." And "a thing a type can do" is exactly what a trait names. You'll meet that idea by name, in full, one lesson from now — and a piece of syntax you just typed without a second thought is about to retroactively make a great deal more sense.
The reveal: you've been writing this since lesson 14
Here is the shape of two old friends, written out plainly — slightly simplified from the standard library's actual definitions, but capturing the idea exactly:
Find the <T>. Find the <T, E>. Option is a generic enum whose Some variant can hold a value of any type whatsoever — a number, a string, your own struct, even another Option. Result is a generic enum over two types at once, one for success and a completely separate one for failure — exactly the shape you've reached for in every fallible function since the Option and Result lesson. You met both of these, by name, all the way back in the enums lesson. You have written Some(value) and Ok(value) more times than you could easily count, and you understood every bit of it, every single time. You were fluent in generics long before today — you'd simply never been formally introduced to the name of the friend you'd already made.
Building your own generic struct
The exact same trick works on types you build yourself, not only ones the standard library hands you ready-made. Picture a small Point type, meant to hold an x and a y coordinate:
Same syntax, same idea, now living on a struct: <T> right after the type's name declares the placeholder, and both fields, x and y, are written in terms of it. Notice precisely what that buys you — and precisely what it costs. integer and float both compile cleanly, because inside each one, the two fields agree on a single, consistent T: i32 for one, f64 for the other. But try to build one whose fields don't agree — a whole number for x, a decimal for y, in the very same value — and the compiler stops you cold with mismatched types. You wrote x: T, y: T, and that is a promise, not a suggestion: whatever T turns out to be, both fields will hold exactly that type, and nothing else.
When one placeholder isn't enough
Sometimes that promise is exactly what you want. And sometimes it's too strict — plenty of perfectly sensible points pair up two completely different types. The fix is just... more letters:
<T, U> declares two independent placeholders, and now x and y are each free to be whatever they like — matching, if you want them to, or not, if you don't. Nothing stops you from reaching for a third letter, or a fourth, the moment a type genuinely needs one. Though here's a small piece of field experience worth tucking away for later: the moment you find yourself reaching for a fourth or fifth placeholder on the very same type, that is usually less "this type needs more flexibility" and more the language gently telling you it's time to split it into smaller, more focused pieces.
Giving a generic type its own methods
A struct without methods is just data sitting still. Here's how impl blocks — the ones you've written without a second thought ever since the method-syntax lesson — handle a type that's generic:
Here's the trickiest piece of syntax in this entire lesson, and it rewards being looked at directly, just once: <T> appears twice in that one line, and the two copies are saying two different things. The first — right after impl — is a declaration: "everything inside this block is about to be written generically, over some type I'm going to call T." The second — inside Point<T> — is a specification: "...and specifically, for the version of Point built around that very same T." Read together, in order, the whole line says one coherent sentence: "For any T at all, here is something every single Point<T> can do — no matter which type it happens to be holding." Once that pairing settles in, impl<T> Point<T> stops looking like punctuation soup and starts reading like a small, complete, perfectly ordinary sentence.
The genuinely surprising part: none of this costs you anything
Here's the headline fact this whole lesson has been quietly building toward — the one that tends to stop people in their tracks the first time they actually hear it stated plainly: generic code runs exactly as fast as if you had hand-written a separate version of it for every single concrete type you ever use it with. Not "almost as fast." Not "fast enough that it stops mattering." Exactly as fast — because that is, quite literally, what the compiler builds for you.
The trick has a real name worth knowing: monomorphization. While compiling your program, Rust looks at every concrete type your generic code actually gets used with — in the example a few sections back, that's i32 and char — and quietly generates a separate, fully concrete copy of largest for each one, exactly as if you'd hand-written largest_i32 and largest_char yourself. Then it throws the generic version away completely. By the time your program actually runs, there is no placeholder T left anywhere — no lookup, no indirection, no extra step of any kind. Just two ordinary, independently-optimized pieces of machine code, indistinguishable from what the most patient, repetitive version of you could ever have produced by hand. This is what people mean when they call something a "zero-cost abstraction": you get to write the clean, general version, and the compiler quietly does the tedious part for you — for free, before your program ever takes its first breath.
Quick exercise
Write smallest<T: PartialOrd>(list: &[T]) -> &T — the mirror image of largest, one comparison flipped. Then design a small Pair<T> struct that holds two values of the same type, plus a method both(&self) -> (&T, &T) that hands back references to each of them at once. If the second one felt like it very nearly wrote itself — that isn't luck, and it isn't you being unusually quick today. That's precisely what "one idea, working absolutely everywhere it's needed" is supposed to feel like, the moment it actually clicks.
You now have a real, working name — and genuine, hands-on comfort — for an idea you'd been quietly trusting since lesson 14: write the shape of something exactly once, and let it work for absolutely anything that genuinely fits. There's just one loose thread this lesson left deliberately untied, and it's been sitting in plain view the entire time, right there inside largest's own signature: that small : PartialOrd.
What precisely is a trait bound — and underneath all the syntax, what actually is a trait? That question isn't a footnote anymore. It's the very next lesson. And it's going to feel like several separate pieces you've been quietly carrying around for a while — PartialOrd from minutes ago, Display and Debug from the derive attribute riding atop nearly every struct and enum you've ever written — finally clicking into one coherent picture, all at once: traits.