An Introduction To Rust Data Types (With Code Examples)

Jayson Lennon
Jayson Lennon
hero image

If you’ve been coding with Python or JavaScript for a while, and are now starting to learn Rust, then you’ve no doubt come across Rust data types.

However, you’ve probably also noticed that Rust handles these differently from what you’ve used before - especially when it comes to type checking.

There are a few other differences also though, such as memory safety, performance, concurrency, error handling, and more.

But don’t worry, because I’ve got you covered.

In this guide, I’ll break down all you need to know to get started with Rust data types. Everything from integers and booleans to tuples and arrays, and even strings. Heck, we’ll even cover how to access array and string elements and handle possible errors!

Sidenote: If you struggle to understand some of the concepts in this guide, or simply just want to take a deeper dive into Rust, then check out my complete Rust developer course:

learn rust

Learn how to code and build your own real-world applications using Rust so that you can get hired this year. No previous programming or Rust experience is needed.

With that out of the way, let's get into the guide.

What are data types?

Data types are a fundamental programming concept that determines the kind of data that can be stored and manipulated within a program. If you’ve worked with programming languages before, then you’ve no doubt come across these already.

There are three categories of data types in Rust:

  • Scalar types
  • Composite types, and
  • String types

I’ll cover each of these and the different data types inside each category in just a second. But first, let’s look at why data types are important.

Not only that, but let’s also compare these to what you might have already used in JS and Python, and the pros and cons of each.

Why are data types essential in Rust programming?

Data types serve several vital roles in Rust programming:

  1. Memory Management: Each data type in Rust has a specific memory size associated with it. By specifying the data type, Rust knows how much memory to allocate for each variable, which leads to efficient memory utilization
  2. Type Safety and Compile-Time Checking: Rust is a statically typed language. You have to declare the type of each variable, and once declared, this type cannot change at runtime
  3. Enhanced Readability and Maintainability: When you declare the type of a variable, it becomes easier to understand what values can be assigned to the variable. This enhances the readability of your code and provides benefits such as better development environment integration. In the case of large projects, this is extremely beneficial in maintaining the code
  4. Scalability: Understanding data types and using them correctly is an important part of writing scalable Rust code. Proper use of data types can ensure your Rust programs run efficiently, even when working with large data sets or complex operations

The main differences between Rust data types vs JavaScript or Python

The main differences between Rust's data types and those in dynamically typed languages like JavaScript or Python go way beyond just type-checking.

In fact, here are several key distinctions:

Rust data types vs python + javascript

(Click on the image for a larger version).

A quick breakdown of the different Rust data types

Alright, so let’s get into each data type in detail, and compare them to what you’ve used before.

I mentioned earlier that there are three main categories of Rust Data Type:

  • Scalar Types: These represent a single value that cannot be broken into smaller components. Examples include integers and floating-point numbers, booleans, and the character type
  • Composite Types: These group multiple types into one type. Examples include tuples, vectors and arrays, enumerations, and structures
  • String Types: Although not typically classified as either scalar or composite, strings are fundamental in Rust. Rust provides two main string types: String and &str

In the next sections, I’m going to break down each of these types, as well as compare how you might use them in Python and JS, so you can see the differences and pros and cons of each.

Rust scalar types

In Rust, scalar types represent a single value that cannot be broken into smaller chunks. Think of them as the smallest building blocks that can be used to build something larger.

Integer types

An integer is a whole number without a fractional component, and in Rust, we have two forms of integers:

  • Signed, and
  • Unsigned

Signed integers can store both positive and negative numbers, while unsigned integers can only store positive numbers.

Here are some examples of integer types in Rust:

// signed integers
let a: i32 = 10; // 32-bit signed integer
let b: i64 = -15; // 64-bit signed integer
// unsigned integers
let c: u32 = 20; // 32-bit unsigned integer
let d: u8 = 255; // 8-bit unsigned integer

It's also possible to specify the type with a suffix on the number itself, and we can use underscores to make it easier to read large numbers:

// data type as suffix
let e = 1234i32;  // 32-bit signed integer
let f = 1234_u32; // 32-bit unsigned integer

// underscores on large numbers help readability
let g: i64 = 1_234_567;

The usize type is a special numeric type that is the size of a pointer on the target architecture and is used to index into collections. So if the program is compiled on an x86_64 machine, then a usize will be the same size as u64.

Integer data type comparison between Rust, JavaScript, and Python

Rust: Statically typed with explicit sizes (e.g., i32, u64)

  • Pro: Provides fine-grained control over memory usage and performance
  • Con: Requires careful consideration of the appropriate type size for each use case

Python: Uses a single int type that can grow to accommodate large values. This is more flexible but provides less control over size and performance

  • Pro: No need to worry about integer size limits
  • Con: Less control over memory usage and performance

JavaScript: Uses Number type for all numbers, including integers, which can lead to precision issues for large values

  • Pro: Simple and flexible
  • Con: Potential precision issues with very large integers

Floating-point types

Floating-point numbers are numbers with a decimal point. Rust has two primitive types for floating-point numbers: f32 and f64. (They are 32 and 64 bits in size, respectively).

Here's how you can declare floating-point numbers in Rust:

let e: f32 = 3.14; // 32-bit floating point number
let f: f64 = 2.718; // 64-bit floating point number

When using floating point numbers, keep in mind that they will lose precision when performing mathematical operations. f32 has about 6 decimal digits of precision, and f64 has about 15 decimal digits of precision.

If you require exact decimal numbers in your application, you’ll need to implement a rounding strategy or use a crate such as rust_decimal or bigdecimal.

Floating-point data type comparison between Rust, JavaScript, and Python:

Rust: Explicit types for floating-point precision (f32, f64)

  • Pro: Allows precise control over memory usage and performance
  • Con: Requires choosing the appropriate precision level

Python: Uses a single float type which is a double-precision floating point (similar to f64 in Rust). Also offers the decimal module for more precise decimal arithmetic

  • Pro: Easy to use and sufficient for most applications
  • Con: Precision issues can arise in complex calculations

JavaScript: Uses Number type, which is a double-precision floating point (similar to f64 in Rust), but can lead to precision issues

  • Pro: Simple to use
  • Con: Precision issues and lack of integer-specific type can cause unexpected behavior

The Boolean type

In Rust, we have the bool type, which can be either true or false.

Here’s an example:

let g: bool = true; // boolean true
let h: bool = false; // boolean false

Boolean data type comparison between Rust, JavaScript, and Python:

Python: Uses bool type with True and False.

  • Pro: Straightforward and easy to use
  • Con: No significant drawbacks

JavaScript: Uses boolean type with true and false.

  • Pro: Straightforward and easy to use
  • Con: Can sometimes be confusing with type coercion (e.g., 0 and "" are false)

Rust: Uses bool type with true and false.

  • Pro: Simple and clear, with no type coercion issues
  • Con: No significant drawbacks

The character type

In Rust, the character type is denoted by char.

Unlike some other programming languages, which may use char to represent ASCII characters, Rust's char type represents a Unicode Scalar Value, which can represent a lot more than just ASCII.

For example

let a: char = 'a'; // lowercase letter
let b: char = 'A'; // uppercase letter
let heart_emoji: char = '❤'; // an emoji

char literals are specified with single quotes, as opposed to string literals, which use double quotes.

Character data type comparison between Rust, JavaScript, and Python:

Rust: Uses char type for Unicode Scalar Values.

  • Pro: Explicit and capable of representing a wide range of characters
  • Con: Requires understanding of Unicode Scalar Values

Python: Characters are just strings of length 1 (str type).

  • Pro: Simple and flexible.
  • Con: Less explicit, which can lead to confusion about string versus character operations.

JavaScript: Characters are also strings of length 1 (String type).

  • Pro: Simple and flexible
  • Con: Less explicit, similar to Python

So that covers all of the scalar types, now let’s look at each data type in the composite category.

Rust composite types

In Rust, composite types allow you to group multiple values in one type.

Tuple types

Tuples are an ordered list of elements of potentially different types. They have a fixed length that’s established when they’re declared and cannot be changed afterward.

Here's how you declare a tuple in Rust:

let tup: (i32, f64, char) = (500, 6.4, 'J');

In the tuple tup, we’re grouping together an i32, an f64, and a char.

To access the elements of a tuple, you can use dot notation followed by the index of the value you want to access.

let five_hundred = tup.0; // equals 500
let six_point_four = tup.1; // equals 6.4
let j = tup.2; // equals 'J'

Rust also provides a feature called destructuring, which allows you to break the tuple (or other composite data type) up into its individual pieces and then assign those pieces to separate variables:

For example

let tup: (i32, f64, char) = (500, 6.4, 'J');
let (x, y, z) = tup;

Now x is 500, y is 6.4, and z is 'J'.

Tuple data type comparison between Rust, JavaScript, and Python:

Rust: Fixed size, can hold multiple types (e.g., (i32, f64, char)).

  • Pro: Type safety and clarity
  • Con: Fixed length can be limiting

Python: Similar support with tuple type, using indexing and destructuring.

  • Pro: Easy to use and understand
  • Con: No significant drawbacks

JavaScript: No direct equivalent; arrays can be used but are not fixed size or type.

  • Pro: Flexible and dynamic
  • Con: Less explicit and can lead to errors when types are mixed

Vectors and Arrays

Unlike a tuple, an array is a collection of multiple values of the same type. However, like tuples, arrays in Rust also have a fixed length.

Here's how you declare an array:

let nums: [i32; 5] = [1, 2, 3, 4, 5];

In this array nums, we're storing five i32 values. To access elements in an array, you use indexing, like so:

let first = nums[0]; // equals 1
let second = nums[1]; // equals 2

A vector is similar to an array, except you can dynamically size it at runtime.

let nums = vec![1, 2, 3, 4, 5];

Accessing vector elements is the same as an array. You can use the .push() method to put items onto a vector:

let mut nums = vec![1, 2, 3, 4, 5];
nums.push(6);
// nums is now [1, 2, 3, 4, 5, 6]

Vector and Array data type comparison between Rust, JavaScript, and Python:

Rust: Uses array for fixed-size collections and vec for dynamically-sized collections.

  • Pro: Provides both fixed-size and dynamic options, with strong type safety
  • Con: More effort required to choose the appropriate type for each use case

Python: Uses list type, which is dynamic and can hold multiple types.

  • Pro: Very flexible and easy to use
  • Con: Less control over element types and potential for runtime errors

JavaScript: Uses Array type, which is dynamic and can hold multiple types.

  • Pro: Very flexible and easy to use
  • Con: Potential for runtime errors due to mixed types

Structures

Structures allow you to group multiple different data types into a single composite type:

// declaring a new structure
struct Person {
  name: String,  // a field for a String
  age: u8    	// a field for a number
}
let alice = Person {
  name: "Alice".to_string(),
  age: 23
};

We can then access individual fields using dot notation:

let alice_age = alice.age;

And just like with tuples, structures can also be destructured:

let Person {name, age} = alice;

(Structures are not limited to only containing scalar types. They can include any type such as other structures).

Structures data type comparison between Rust, JavaScript, and Python:

Rust: Uses struct for custom data types with named fields.

  • Pro: Strong type safety and clear structure
  • Con: Requires explicit definition and can be verbose

Python: Uses classes for custom data types with named fields (class).

  • Pro: Highly flexible and supports methods
  • Con: More complex to set up compared to simpler data types

JavaScript: Uses objects for custom data types with named fields (object, class).

  • Pro: Highly flexible and supports methods
  • Con: Less strict type-checking can lead to runtime errors

Enumerations

An enumeration is a data type that allows selecting one of many different data types. Think of it like a switch or dial where only one position can be active at any one point.

// declare an enumeration
enum Choice {
  A,  // each choice is called a "variant"
  B,
  C,
}

// we pick B
let b = Choice::B;

Enumerations can also contain associated data on each variant, which is useful when there is some data requirement with the variant.

For example

If we have a drawing system where lines can be drawn, we could construct an enumeration like this:

enum LineCommand {
  // struct-like data
  Translate { x: i32, y: i32 },

  // tuple-like data
  ChangeColor(u8, u8, u8),

  // no data
  Draw,
}

So now if we receive a LineCommand, the data needed to perform the command is available and ready for use:

let action = LineAction::ChangeColor(255, 0, 0);
match action {
	LineAction::ChangeColor(red, green, blue) => {
    	// set red green and blue values
	}
	LineAction::Translate { x, y } => {
    	// move the line to (x,y)
	}
	LineAction::Draw => {
    	// draw the line
	}
}

Enumerations data type comparison between Rust, JavaScript, and Python:

Rust: Uses enum for enumerations with strong type safety and pattern matching.

  • Pro: Powerful and expressive, with strong type safety
  • Con: Requires understanding of Rust's pattern matching syntax

Python: Uses enum module for defining enumerations (enum.Enum).

  • Pro: Easy to use and understand
  • Con: Less powerful in terms of associated data and pattern matching

JavaScript: No direct equivalent; often use objects with named properties.

  • Pro: Simple and flexible
  • Con: Less structured and can lead to errors if not used carefully

Rust string types

A critical component of many programming tasks, especially those involving text manipulation, is the string. While not a scalar or composite type, strings in Rust have unique characteristics that merit their separate classification.

In the context of Rust's data types, the string holds a special place due to its omnipresence. Whether you're reading file names, receiving input from a user, or processing text data, understanding the string type is indispensable for Rust programming.

Rust primarily has two string types:

  • String - A growable, mutable, owned, UTF-8 encoded string type, making it suitable when you need a modifiable string
  • &str - An immutable, fixed-length string slice that is stored somewhere in memory. This type of string is often used for function arguments or static strings

Though different in some ways, both strings are UTF-8 encoded, which means they can contain any properly encoded Unicode character. This gives Rust strings the ability to handle a wide variety of text data.

There are also other types of strings such as OsStr used when dealing with strings provided by the operating system. (For a deep dive on this topic, check out my post about strings in Rust).

For example:

let mut hello = String::from("Hello, ");
hello.push_str("world!"); // push_str() appends a literal to a String
println!("{}", hello); // This will print `Hello, world!`

On the other hand, &str is an immutable fixed-length string slice that is stored somewhere in memory. This type of string is often used for function arguments or static strings.

Here's how you can use &str:

let world = "world!";
println!("{}", world); // This will print `world!`

String data type comparison between Rust, JavaScript, and Python:

Rust: Uses String for growable strings and &str for string slices.

  • Pro: Provides both mutable and immutable options, with strong type safety
  • Con: More complex due to the need to manage ownership and lifetimes

Python: Uses str type, which is immutable. Also offers bytes for binary data.

  • Pro: Simple and easy to use, with powerful built-in methods
  • Con: Immutability can sometimes require workarounds for string manipulation

JavaScript: Uses String type, which is immutable.

  • Pro: Simple and easy to use, with powerful built-in methods
  • Con: Immutability can sometimes require workarounds for string manipulation

How to access array and string elements in Rust

Accessing elements within arrays, vectors, and strings is a common operation in rust programming.

Accessing array and vector elements

In Rust, you can access a specific element in an array or vector by using indexing. The index starts at 0.

For example:

let arr: [i32; 5] = [1, 2, 3, 4, 5];
let first = arr[0]; // 1
let second = arr[1]; // 2

Accessing string elements

Accessing individual characters in a String in Rust is a bit different due to its encoding. Instead, we access parts of the string via slices, like so:

let hello_world = String::from("Hello, world!");
let hello = &hello_world[0..5]; // "Hello"
let world = &hello_world[7..12]; // "world"

In this example, hello will contain "Hello", and world will contain "world".

Slicing works on the byte-level, and not at the Unicode scalar level. This means that it's possible to accidentally slice one Unicode character into two.

(If you need to access individual characters in a string, my post on the topic explains the process).

How Rust handles invalid array, vector, and string element access

Rust takes safety seriously, and this is evident in how it handles invalid array, vector, and string element access.

How?

Well, if you try to access an index beyond the length of an array, vector, or string, Rust will panic - that is, it will stop execution and throw an error - at runtime.

For example

Here's what happens when you try to access an invalid index:

let nums = vec![1, 2, 3, 4, 5];
let sixth = nums[5]; // This will panic!

In the above code, trying to access the sixth element of a five-element array causes Rust to panic with an 'index out of bounds' error.

So what's the solution?

Well, if you want to access elements without a runtime panic, you can use the .get() method:

let arr: [i32; 5] = [1, 2, 3, 4, 5];
let sixth: Option<&i32> = arr.get(5);
// now check the option

This method returns an Option, which will be None if the index is out of bounds, allowing you to handle the error gracefully.

The same behavior occurs with strings:

let hello_world = String::from("Hello, world!");
let invalid = &hello_world[13..20]; // This will panic!

Attempting to slice beyond the length of "Hello, world!" - which is 13 characters long including the space and the exclamation mark - also causes a panic.

TL;DR

Rust's approach of panicking on out-of-bounds access forces the handling of potential errors, leading to safer code, but it requires additional handling to avoid panics.

Time to apply what you’ve learned

And there you have it - a comprehensive introduction to each of Rust’s data types, including scalar types (integers, floating-point numbers, booleans, and characters), composite types (tuples, arrays, vectors, structures, and enumerations), and string types.

We also looked at how Rust's approach to memory safety, type checking, and error handling compares to Python and JavaScript.

I know that was a lot to cover but it’s worth it.

Understanding these data types is crucial for writing efficient, safe, and scalable Rust code. By leveraging Rust's strong type safety and memory management features, you can reduce runtime errors, improve performance, and write more maintainable code.

But now it's time to apply what you've learned. Experiment with these data types in your own Rust programs. Practice accessing and manipulating elements, handling errors, and exploring more advanced concepts as you grow more comfortable. The more you practice, the more proficient you'll become, and the better you'll understand the unique advantages Rust offers.

Keep exploring, keep coding, and enjoy the journey of mastering Rust. Happy coding!

P.S.

If you struggled to understand some of the concepts in this guide, or simply just want to take a deeper dive into Rust, then check out my complete Rust developer course:

learn rust

Learn how to code and build your own real-world applications using Rust so that you can get hired this year. No previous programming or Rust experience is needed.

You’ll not only get access to step-by-step tutorials and projects, but you can ask questions from me and other instructors, as well as other students inside our private Discord.


Check it out and see if it can help you improve your own Rust skills.

BONUS: More Rust tutorials, guides & resources

If you've made it this far, you've clearly interested in Rust so definitely check out all of my Rust posts and content:

More from Zero To Mastery

Top Cargo Subcommands For Rust Development preview
Top Cargo Subcommands For Rust Development

Looking for ways to manage your Rust projects + dependencies, while also streamlining your workflow? 😎 Then check out these top Cargo subcommands!

How To Use The Typestate Pattern In Rust preview
How To Use The Typestate Pattern In Rust

Having issues with data state changes while coding Rust apps? Don't sweat it. In this Rust tutorial, I walk you through 3 ways to solve this issue with the typestate pattern.

Top 15 Rust Projects To Elevate Your Skills preview
Top 15 Rust Projects To Elevate Your Skills

From beginner to advanced, these are the best Rust projects to push your skills, grow your confidence, and wow potential employers. Check them out now!