18.2Vec: creating and updating

Last updated June 13, 2026

Vec<T> is the collection you'll use most: a growable list of values of one type. This lesson covers making one, adding and removing elements, and reading them, with special attention to the two ways of accessing an element, because they differ in a way that's pure Rust safety philosophy.

Creating a vector

Two common ways to make a Vec. Vec::new() makes an empty one (an associated function, lesson 10.6), and the vec! macro makes one with initial values:

fn main() {
    let mut empty: Vec<i32> = Vec::new();
    let numbers = vec![1, 2, 3];

    println!("{numbers:?}");
    empty.push(10);
    println!("{empty:?}");
}
[1, 2, 3]
[10]

vec![1, 2, 3] is the everyday way to write a vector literal; the ! marks it a macro (like println!). With Vec::new(), the compiler often can't tell what T is until you push something, so you either annotate (Vec<i32>) or let the first push decide. numbers is a Vec<i32> inferred from the literal. A Vec prints with {:?} (Debug, lesson 10.7) as a bracketed list.

Adding and removing

A Vec must be mut to change (lesson 1.4). push adds to the end, pop removes from the end and returns it:

fn main() {
    let mut stack = vec![1, 2, 3];
    stack.push(4);
    println!("{stack:?}");        // [1, 2, 3, 4]

    let last = stack.pop();
    println!("{last:?}");         // Some(4)
    println!("{stack:?}");        // [1, 2, 3]
}
[1, 2, 3, 4]
Some(4)
[1, 2, 3]

pop returns Option<T>, not T, because the vector might be empty, there might be nothing to pop. This is Option (lesson 11.3) doing exactly its job: the type forces you to handle the empty case. That's also why while let Some(x) = stack.pop() (lesson 11.6) drains a vector so cleanly: it loops until pop returns None.

Reading elements: the two ways

Here's the lesson's centerpiece. There are two ways to read element i, and they handle the out-of-bounds case differently, on purpose.

Indexing with [] gives you the element directly, and panics if the index is out of bounds:

fn main() {
    let v = vec![10, 20, 30];
    println!("{}", v[1]);     // 20
    println!("{}", v[10]);    // panics
}
20

thread 'main' panicked at src/main.rs:4:20:
index out of bounds: the len is 3 but the index is 10

get(i) returns an Option<&T>: Some(&element) if the index is valid, None if it's out of bounds, no panic:

fn main() {
    let v = vec![10, 20, 30];
    println!("{:?}", v.get(1));     // Some(20)
    println!("{:?}", v.get(10));    // None
}
Some(20)
None

The difference is a choice you make per access. Use [] when an out-of-bounds index would be a bug, a broken assumption that should crash loudly (lesson 12.5's "panic on contract violations"). Use get when the index might legitimately be out of range, the user supplied it, you're probing, so absence is an expected outcome to handle, not a crash. This is the exact panic-versus-Result judgment from chapter 12, applied to indexing, and it's why reading the docs (lesson 13.8) for a method's return type matters: get returning Option is the signature telling you it handles the missing case.

Key insight

v[i] panics out of bounds; v.get(i) returns Option. Both are bounds-checked, Rust never reads past the end silently (the C and C++ buffer-overflow bug class is gone, lesson 1.7). The choice is only about how an out-of-range access is reported: a panic (it's a bug) or a None (it's expected). Reach for [] when the index is known-good and an error there means broken code; reach for get when out-of-range is a possibility you want to handle gracefully.

Vectors are generic and homogeneous

A Vec<T> holds elements all of the same type T (it's generic, lesson 15.3). You can't mix an i32 and a String in one Vec, just as a struct field has one type. When you genuinely need a sequence of mixed types, the tool is an enum whose variants cover the cases (a Vec<Shape>, chapter 11) or trait objects (a Vec<Box<dyn Trait>>, lesson 16.9). The homogeneity isn't a limitation so much as the thing that makes Vec fast and its element type known.

Quiz time

Question #1

What's the difference between v[i] and v.get(i) when i is out of bounds?

Show solution

v[i] panics ("index out of bounds"). v.get(i) returns None (its return type is Option<&T>), no panic. Use [] when an out-of-range index would be a bug that should crash; use get when out-of-range is a legitimate possibility you want to handle. Both are bounds-checked; Rust never reads past the end silently.

Question #2

Why does vec.pop() return Option<T> rather than T?

Show solution

Because the vector might be empty, in which case there's nothing to pop. Returning Option<T> (Some(value) or None) forces the caller to handle the empty case (lesson 11.3) instead of assuming there's always an element. It's also what makes while let Some(x) = vec.pop() drain a vector cleanly.

Question #3

You're reading an index that a user typed in, which could easily be out of range. Which access method should you use, and why?

Show solution

get, which returns Option. User-supplied indices are untrusted and out-of-range is an expected outcome, not a bug, so you want to handle the None case gracefully (print an error, ask again) rather than panic. This is chapter 12's "validate untrusted input, return a recoverable result" applied to indexing. Reserve [] for indices you know are valid.

You can build and read a vector. The next lesson iterates over one, with for, and meets a borrow-checker rule that catches a classic bug: you can't modify a vector while you're iterating over it.