How to Convert a String to Uppercase or Lowercase in Rust

Use the `to_uppercase()` and `to_lowercase()` methods on string slices (`&str`) or `String` types to convert text, which return a new `String` containing the transformed characters.

The search filter that breaks

You're building a search feature. A user types "rust" into the input box. Your database stores "RUST". The match fails. You reach for a quick fix to normalize the case. In Python or JavaScript, you call .toUpperCase() and move on. In Rust, you call to_uppercase(), but the compiler forces you to acknowledge a detail those languages hide: the result is a new allocation, and the length might change.

Then you test with "Straße". Uppercasing that doesn't just flip the case. It expands the string to "STRASSE". If your code assumes a one-to-one character mapping, the expansion breaks your buffer logic or your hash calculation. Rust makes you handle these realities explicitly.

Strings don't change; they copy

Rust strings are immutable by default. You cannot paint over a String with a new case. The methods to_uppercase() and to_lowercase() act like a photocopier. You feed in the original, and the machine spits out a fresh String with the transformation applied. The original stays exactly as it was.

This design protects you from aliasing bugs. If three parts of your program hold references to the same text, one of them can't suddenly change the case and invalidate the others. The borrow checker enforces this. When you call to_uppercase(), you get a new owner of the data. The old owner keeps their copy.

fn main() {
    // Create an owned String.
    let original = String::from("Rust Programming");

    // to_uppercase returns a NEW String.
    // It does not modify `original`.
    let upper = original.to_uppercase();

    // to_lowercase also returns a new String.
    let lower = original.to_lowercase();

    // String slices (&str) have the same methods.
    // They return a new String, not a slice.
    let slice = "HELLO WORLD";
    let slice_lower = slice.to_lowercase();

    println!("Original: {}", original);
    println!("Upper: {}", upper);
    println!("Lower: {}", lower);
    println!("Slice Lower: {}", slice_lower);
}

The original string never changes. If you need the variable to hold the transformed value, you must reassign it.

Unicode is not a one-to-one game

The most important detail about case conversion in Rust is that it uses full Unicode mapping. This is correct for international text, but it introduces surprises for developers used to ASCII-only tools.

Some characters map one-to-one. 'a' becomes 'A'. Others expand. The German sharp s 'ß' becomes "SS". The long s 'ſ' becomes "S". The method handles this expansion automatically. It allocates a buffer large enough for the result and writes the mapped characters.

This expansion is why Rust does not provide an in-place Unicode case conversion method. You cannot write "SS" into the space occupied by 'ß'. You would need to shift all following characters or reallocate the buffer. Reallocation is expensive and changes the memory address, which breaks references. Rust forces you to acknowledge the allocation by returning a new String. This makes the cost explicit.

Rust uses the default Unicode case mapping. It does not use locale-aware rules. This means Turkish dotted and dotless 'i' variations follow the Unicode default, not the Turkish locale. If you need locale-specific behavior, you must use a third-party crate. The standard library prioritizes consistency and safety over locale complexity.

Realistic usage and conventions

In production code, you rarely convert case just to print it. You usually normalize input for comparison or storage. The idiomatic pattern chains transformations.

/// Normalize a user input for case-insensitive comparison.
fn normalize(input: &str) -> String {
    // Trim whitespace first, then lowercase.
    // Chaining works because trim returns &str.
    // This avoids an intermediate allocation.
    input.trim().to_lowercase()
}

fn main() {
    let user_input = "  RUST  ";
    let normalized = normalize(user_input);

    // Compare against a stored value.
    if normalized == "rust" {
        println!("Match found.");
    }
}

Chain your transformations. Rust makes the pipeline readable and efficient.

A common convention aside: call to_uppercase() directly on the &str. Beginners often write input.to_string().to_uppercase(). This allocates a String just to convert it again. &str implements to_uppercase() directly, so you skip the first allocation. The compiler allows both, but the direct call is the convention.

Performance and ASCII shortcuts

Full Unicode case mapping requires looking up tables for every character. It is not constant time. It iterates through the string and performs mapping logic. For most application logic, this overhead is negligible. The cost of allocation dominates.

If you are processing high-throughput data and you know the input is strictly ASCII, you can skip the Unicode machinery. The standard library provides ASCII-specific methods.

Use char::to_ascii_uppercase() when you are iterating over individual characters and want to avoid Unicode lookup overhead. This method returns a char. It leaves non-ASCII characters unchanged.

Use String::make_ascii_uppercase_mut() when you need to modify a String in place and are certain the content is ASCII. This method mutates the buffer without reallocating. It is faster than to_uppercase() because it avoids the allocation and the Unicode table lookup.

fn main() {
    let mut data = String::from("hello world");

    // Mutate in place. Only affects ASCII characters.
    // Non-ASCII bytes remain untouched.
    data.make_ascii_uppercase_mut();

    println!("{}", data); // HELLO WORLD
}

In-place mutation requires a mut binding. If you try to call make_ascii_uppercase_mut() on an immutable string, the compiler rejects you with E0596 (cannot borrow as mutable). You must declare the variable as mutable or reassign it.

Pitfalls and compiler errors

The biggest trap is assuming length stays constant. It doesn't. If you allocate a fixed-size buffer based on the input length, you will overflow. Rust prevents the overflow by returning a new String, but your logic might still break if you expect a 1:1 character mapping. Always treat case conversion as a length-changing operation.

Another pitfall involves type mismatches. to_uppercase() returns a String. If you try to store the result in a string slice variable, the compiler rejects you with E0308 (mismatched types).

fn main() {
    // This fails to compile.
    // to_uppercase() returns String, not &str.
    // let result: &str = "hello".to_uppercase();
}

Fix this by assigning to a String or using .as_str() if you need a slice for a specific duration.

fn main() {
    // Correct: assign to String.
    let result = "hello".to_uppercase();

    // Or get a slice if you need &str.
    let slice: &str = result.as_str();
}

Double allocation is a subtle performance pitfall. Calling to_string() followed by to_uppercase() allocates twice. Call to_uppercase() directly on the &str to allocate only once.

Decision matrix

Use to_uppercase() when you need full Unicode correctness. This handles international characters, expansions like 'ß' to 'SS', and locale-independent mapping. Use to_lowercase() for the same reasons when converting to lowercase. Use char::to_ascii_uppercase() when you are processing individual characters and know the input is strictly ASCII. This avoids the overhead of Unicode lookup tables. Use String::make_ascii_uppercase_mut() when you need to modify a String in place and are certain the content is ASCII. This saves an allocation. Reach for to_ascii_uppercase() on a &str when you want a new String but want to skip Unicode complexity for ASCII-only data.

Default to Unicode methods. Optimize to ASCII only when profiling proves the overhead matters.

Where to go next