Pybites Logo Rust Platform

Trait Bounds

Hard +4 pts

🎯 In Python, function signatures don't constrain what types are allowed:

def largest(items):
    return max(items)  # works if items support comparison, crashes if not

largest([3, 1, 4])       # 4
largest(["a", "c", "b"]) # "c"
largest([{}, {}])         # TypeError at runtime

You find out about type mismatches when the code runs. Type hints help, but they're advisory:

from typing import TypeVar
T = TypeVar("T", bound=Comparable)

def largest(items: list[T]) -> T:
    return max(items)

Rust's trait bounds serve the same purpose — but they're enforced by the compiler. If a type doesn't meet the bounds, the code won't compile.

Basic bounds

A trait bound constrains a type parameter to types that implement specific traits:

fn print_it<T: Display>(item: T) {
    println!("{}", item);
}

T: Display means "T must implement Display." You can call print_it(42) and print_it("hello"), but not print_it(vec![1,2]) (Vec doesn't implement Display).

Multiple bounds with +

When a function needs multiple capabilities, combine bounds with +:

fn clone_and_print<T: Clone + Display>(item: &T) -> T {
    println!("{}", item);
    item.clone()
}

T: Clone + Display — T must be both cloneable and displayable. This is like Python's Protocol intersection, but checked at compile time.

The where clause

When bounds get complex, inline syntax becomes hard to read:

// Hard to read:
fn process<T: Display + Clone, U: Debug + Default>(t: T, u: U) -> String { /* ... */ }

// Clearer with where clause:
fn process<T, U>(t: T, u: U) -> String
where
    T: Display + Clone,
    U: Debug + Default,
{
    format!("{} {:?}", t.clone(), u)
}

Both are equivalent. The where clause moves bounds after the return type, which scales better for multiple type parameters. Use whichever is more readable — inline for simple cases, where for complex ones.

impl Trait syntax

For function parameters and return types, impl Trait is a shorter alternative:

// These are equivalent for parameters:
fn print_it<T: Display>(item: T) { /* ... */ }
fn print_it(item: impl Display) { /* ... */ }

// For return types, impl Trait is the only option for some cases:
fn make_greeting() -> impl Display {
    "hello world"
}

impl Trait in return position means "I return some type that implements Display, but I'm not telling you which one." This is useful when the concrete type is complex or private.

Owned vs borrowed returns

A common pattern: returning an owned value from a function that takes a slice. Since slices are borrowed, you need Clone to return an owned copy:

fn smallest<T: Ord + Clone>(items: &[T]) -> Option<T> {
    items.iter().min().cloned()
}

Ord lets you compare elements. Clone lets you return an owned copy (since .min() gives you a reference into the slice, and .cloned() copies it).

Without Clone, you could only return Option<&T> — a reference that borrows from the input slice.

Login to see the full task and start coding.

This is a premium exercise

Log in to unlock the full exercise and start coding.

Login to access this exercise