---
title: How to Convert a Number to a String in Rust
summary: Use the `ToString` trait for simple conversions or `format!` for formatted output, as both are idiomatic and efficient in Rust.
lang: en
slug: how-to-convert-a-number-to-a-string-in-rust
published: 2026-04-17T16:39:40+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-convert-a-number-to-a-string-in-rust/
---

# How to Convert a Number to a String in Rust



## When numbers need to speak

You're writing a high-score list. The score is an `i32`. You try to print `"Score: " + score` and the compiler throws a fit. Or you're building a JSON response and need that number as text. Converting numbers to strings is the bridge between computation and communication. In Rust, that bridge has a few toll booths depending on how fast you need to go.

The language gives you simple tools for everyday work and specialized tools for when every nanosecond counts. You'll use the simple tools 99% of the time. The specialized tools exist for the 1% where profiling shows string conversion is eating your CPU.

Pick the right bridge, and your data flows smoothly. Pick the wrong one, and you'll pay in allocations or boilerplate.

## The standard path: to_string and format

Most of the time, you just need a number as text. Rust provides two idiomatic ways to do this. Both are fast, safe, and readable.

Call `.to_string()` on a single value. Use `format!` when you need to combine values or add formatting.

```rust
fn main() {
    // to_string() converts a single value to a String.
    // It works on any type that implements Display, including all standard numbers.
    let score = 42;
    let score_text = score.to_string();
    assert_eq!(score_text, "42");

    // format! constructs a String from multiple parts.
    // It uses the same syntax as println! but returns a String instead of printing.
    let message = format!("Current score: {}", score);
    assert_eq!(message, "Current score: 42");

    // format! also handles formatting directives like padding and precision.
    let padded = format!("{:05}", score);
    assert_eq!(padded, "00042");
}
```

Trust `format!` for composition. It calculates the size once and writes straight through.

## Under the hood: Display and allocation

When you call `.to_string()`, Rust looks for the `ToString` trait. Here's the trick: you rarely implement `ToString` yourself. The standard library provides a blanket implementation: if a type implements `Display`, it automatically gets `ToString`.

So `to_string()` is just a wrapper around `format!("{}", value)`. Under the hood, Rust calculates how many characters the number needs, allocates that much memory on the heap, writes the digits, and returns the `String`.

This allocation is cheap for occasional use. A `String` owns its data. When the `String` goes out of scope, the memory is freed. This ownership model keeps Rust safe. You don't have to worry about the string data vanishing while you're still using it.

The allocation is the price of admission for a `String`. Pay it only when you need ownership.

## Custom types and the Display trait

What if you have a custom struct? You can't call `.to_string()` unless you implement `Display`. This is a design choice. Rust forces you to decide how your type looks as text.

Implement `Display` for the user-facing representation. Leave `Debug` for the developer's eyes.

```rust
use std::fmt;

/// Represents a player in the game.
struct Player {
    name: String,
    score: u32,
}

/// Implement Display to define how Player appears as a string.
/// This automatically gives Player the .to_string() method.
impl fmt::Display for Player {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Write the formatted output to the formatter.
        write!(f, "{}: {}", self.name, self.score)
    }
}

fn main() {
    let player = Player {
        name: "Alice".to_string(),
        score: 150,
    };

    // to_string() now works because Display is implemented.
    let text = player.to_string();
    assert_eq!(text, "Alice: 150");
}
```

If you try to call `.to_string()` on a type that doesn't implement `Display`, the compiler rejects this with E0277 (trait bound not satisfied). The error tells you exactly which trait is missing. Implement `Display` to fix it.

Convention aside: `format!` shares its syntax with `println!`. If you know how to print, you know how to format. `println!` returns `()`, `format!` returns `String`. Use `format!` when you need the result; use `println!` when you just want to show it.

## Formatting control: precision and padding

Numbers often need specific formatting. You might need a fixed number of decimal places, leading zeros, or alignment. `format!` handles all of this with specifiers.

Use format specifiers to control the output. Hard-coding padding is a maintenance trap.

```rust
fn main() {
    let price = 19.99;
    let id = 7;

    // .2 limits floats to two decimal places.
    let formatted_price = format!("${:.2}", price);
    assert_eq!(formatted_price, "$19.99");

    // 05 pads integers with zeros to width 5.
    let formatted_id = format!("ID: {:05}", id);
    assert_eq!(formatted_id, "ID: 00007");

    // Combine multiple values with different formatting.
    let report = format!("Item {:03} costs ${:.2}", id, price);
    assert_eq!(report, "Item 007 costs $19.99");
}
```

Floats have a quirk. `3.14` might print as `3.1400000000000001` without care. Rust's default float formatting uses an algorithm that produces the shortest representation that round-trips back to the same float. This avoids unnecessary noise while preserving precision. Use `.2` or other precision specifiers when you need human-readable output.

## The performance path: itoa and ryu

When you're converting millions of numbers in a tight loop, `to_string()` and `format!` become expensive. They allocate a new `String` every time. The allocator has to find memory, initialize it, and later free it. This adds up.

For high-performance scenarios, use `itoa` for integers and `ryu` for floats. These crates write directly into a pre-allocated buffer. No heap allocation per conversion.

Add `itoa = "1"` and `ryu = "1"` to your `Cargo.toml`.

```rust
use itoa::Buffer;

fn main() {
    // Buffer is a reusable scratchpad.
    // It's cheap to create and can be reused across many conversions.
    let mut buf = Buffer::new();

    // format writes the number into the buffer and returns a &str.
    // No allocation happens here.
    let s = buf.format(12345);
    assert_eq!(s, "12345");

    // Reuse the buffer for the next number.
    let s2 = buf.format(67890);
    assert_eq!(s2, "67890");
}
```

`ryu` works the same way for floats. It provides a `Buffer` type that you reuse. The performance gain is significant in hot loops. Benchmarks show `itoa` can be 5x to 10x faster than `to_string()` when converting integers in bulk.

Treat the buffer as a scratchpad. If you write over it, the old note is gone.

## Pitfalls: buffer reuse and trait bounds

The buffer approach introduces a new class of bugs. `itoa::Buffer::format` returns a `&str` that borrows from the buffer. If you reuse the buffer, the previous `&str` becomes invalid.

This is a classic pitfall. The compiler won't always catch it because `&str` doesn't track the buffer's lifetime in a way that prevents reuse. You have to manage the lifetimes manually.

```rust
use itoa::Buffer;

fn main() {
    let mut buf = Buffer::new();

    // format returns a &str pointing into buf.
    let s1 = buf.format(123);

    // Calling format again overwrites the buffer.
    // s1 is now invalid. It points to stale data.
    let s2 = buf.format(456);

    // s1 points to "456" now, not "123".
    // This assertion passes, proving s1 was invalidated.
    assert_eq!(s1, "456");
}
```

To avoid this, only hold the `&str` as long as you need it. Copy the data into a `String` or `Vec<u8>` if you need to keep it. Or structure your code so you process each number immediately after formatting.

Profile before optimizing. `itoa` adds complexity that rarely pays off outside hot loops.

## Decision matrix

Use `to_string()` when you need a quick conversion of a single value and readability matters. Use `format!` when you're combining multiple values, adding text, or applying formatting like padding and precision. Use `itoa` when profiling shows integer-to-string conversion is a bottleneck in a tight loop and you can manage a reusable buffer. Use `ryu` when you have the same performance pressure with floating-point numbers. Reach for `format!` over manual concatenation; it's faster and safer because it calculates the total size upfront.

Readability wins by default. Performance wins when the profiler points the finger.

## Where to go next

- [How to Trim Whitespace from a String in Rust](/en/how-to-trim-whitespace-from-a-string-in-rust/)
- [What is the Cow str type](/en/what-is-the-cow-str-type/)
- [How to Convert a String to an Integer in Rust](/en/how-to-convert-a-string-to-an-integer-in-rust/)