---
title: "5.3 Introduction to String"
description: "Rust's owned, growable text type: creating Strings, growing them with push_str, and what len() really counts."
url: "https://learnrust.net/chapter-5/introduction-to-string/"
last_updated: "2026-06-12"
---

# 5.3 Introduction to String

Lesson [1.6](https://learnrust.net/chapter-1/reading-input/) made you a promise: "why text comes in two flavors (this one, and the string literals you've been printing) is a chapter 5 story." This lesson and the next *are* that story. And it starts with an introduction to someone you already know: every time you've read input, you've written `String::new()`. You've been manufacturing Strings for four chapters. Today you learn what you've been making.

## An owned, growable text type

A **String** is Rust's owned, growable text type. Both adjectives are load-bearing. *Growable*: a `String`'s contents can change length at runtime, which is why `read_line` can pour an arbitrarily long line into one, and why `String` can't be a fixed-size fundamental type like the chapter 4 cast (lesson [4.1](https://learnrust.net/chapter-4/introduction-to-fundamental-data-types/) noted text is built *out of* fundamental pieces; a `String` manages a resizable collection of bytes for you). *Owned*: the `String` is responsible for the text it holds; the text lives as long as the `String` does and goes away when it goes away. The full weight of "owned" is chapter 8's headline act; for now, read it as "this one's *mine*," in contrast with something arriving next lesson.

There are three common ways to make one:

```rust
fn main() {
    let empty = String::new();
    let from_fn = String::from("carrot");
    let converted = "potato".to_string();

    println!("[{empty}]");
    println!("{from_fn} and {converted}");
}
```

```
[]
carrot and potato
```

`String::new()` makes an empty one, ready to receive input or grow. `String::from("...")` and `"...".to_string()` both make a `String` from literal text; they do the same job, and which one a programmer writes is mostly habit. (Printing an empty `String` prints nothing, hence the brackets to make the nothing visible.)

## Growing a String

Mutation needs `mut`, same as every other type. The growing tools are `push_str`, which appends text, and `push`, which appends one character:

```rust
fn main() {
    let mut order = String::from("salad");
    order.push_str(", soup");
    order.push('!');
    println!("{order}");
}
```

```
salad, soup!
```

Mind the quote marks on `push('!')`: single quotes, because `push` takes a `char`, lesson [4.8](https://learnrust.net/chapter-4/char-and-unicode/)'s one-character type. Write `push("!")` and you'll get the mismatched-types error you'd predict, with the same charming quote-mark correction 4.9 showed.

Rust also lets you concatenate with `+`, and you'll see it in other people's code, but it comes with a surprise: `+` *consumes* the `String` on its left side, which stops compiling in ways we can't explain until chapter 8 puts ownership on the table. This course owes you that explanation and will pay it there; until then, `push_str` and `format!` (lesson 5.5, two lessons away) cover every concatenation you need, with no surprises.

## What len() counts

Strings know their length, but the unit deserves a warning label:

```rust
fn main() {
    let plain = String::from("cafe");
    let fancy = String::from("café");
    println!("{}", plain.len());
    println!("{}", fancy.len());
}
```

```
4
5
```

`len()` counts **bytes**, not characters. Lesson [4.8](https://learnrust.net/chapter-4/char-and-unicode/) explained that Unicode gives every character a catalog number and that the popular ones fit small; in a `String`, text is stored in the UTF-8 encoding, where plain ASCII characters take one byte each and rarer characters take more. `é` takes two, so `"café"` is four characters in five bytes. For the ASCII-only text of most examples, bytes and characters agree, which is exactly what makes this bug a delayed-action one: code that "worked for months" meets its first accented name. Counting characters properly is a job for iterators (chapter 19); until then, treat `len()` as a size in memory, not a letter count. Its honest sibling `is_empty()` asks the question you usually meant anyway.

(`len()` returns a `usize`, of course: it's a memory measurement, and lesson [4.3](https://learnrust.net/chapter-4/isize-usize-and-integer-literals/) told you they'd keep arriving with collections of things. `String` is the first collection this course has admitted to.)

## Text is not numbers

One more boundary, cheap to state now that chapter 4 trained the instinct: `"45"` is text that depicts a number, not a number. It won't add, compare against `45`, or convert implicitly; lesson [4.10](https://learnrust.net/chapter-4/numeric-conversions/)'s no-implicit-conversions policy applies with full force at this border too. Crossing it deliberately is `parse`, whose full teardown is lesson 5.6.

## Quiz time

**Question #1**

What does this print?

```rust
fn main() {
    let mut s = String::new();
    s.push_str("naïve");
    println!("{} {}", s.len(), s.is_empty());
}
```

<details class="solution">
<summary>Show solution</summary>

```
6 false
```

Five characters, but `ï` costs two bytes in UTF-8, so `len()` reports 6. `is_empty()` is `false`: the string has contents, however you count them.

</details>

**Question #2**

Predict the compiler's reaction:

```rust
fn main() {
    let mut greeting = String::from("hello");
    greeting.push(", world");
    println!("{greeting}");
}
```

<details class="solution">
<summary>Show solution</summary>

`error[E0308]: mismatched types`: `push` takes a single `char`, and `", world"` is a string literal (expected `char`, found `&str`). The fix is either `push_str(", world")` or, if only one character was wanted, single quotes. The double-quote/single-quote type boundary from lesson [4.8](https://learnrust.net/chapter-4/char-and-unicode/) cuts both ways.

</details>

**Question #3**

Write a program that asks for the user's first name, then their last name, and builds (not just prints) a single `String` containing `Firstname Lastname` before printing it on one line, like `Ada Lovelace`. Use `push_str` and `push`; keep the input recipe and `trim` from lessons 1.6 and 5.2.

<details class="solution">
<summary>Show solution</summary>

```rust
use std::io;

fn main() {
    println!("First name?");
    let mut first = String::new();
    io::stdin()
        .read_line(&mut first)
        .expect("failed to read input");

    println!("Last name?");
    let mut last = String::new();
    io::stdin()
        .read_line(&mut last)
        .expect("failed to read input");

    let mut full = String::from(first.trim());
    full.push(' ');
    full.push_str(last.trim());

    println!("{full}");
}
```

`String::from(first.trim())` starts the new `String` from the cleaned first name, `push` adds the single space (a `char`, single quotes), `push_str` appends the cleaned last name. Building the value in a variable, rather than printing pieces, is the new skill: `full` can now be handed to a function, measured, or grown further.

</details>

The promised flavor count was two, and this was only the first. The second flavor is hiding in plain sight, in the quotes of every literal you've written since your first program, and inside `trim`'s mystery return type. Next lesson it takes off the mask.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
