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 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.
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:
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.
Data types serve several vital roles in Rust programming:
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:
(Click on the image for a larger version).
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:
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.
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.
An integer is a whole number without a fractional component, and in Rust, we have two forms of integers:
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
.
Rust: Statically typed with explicit sizes (e.g., i32
, u64
)
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
JavaScript: Uses Number
type for all numbers, including integers, which can lead to precision issues for large values
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.
Rust: Explicit types for floating-point precision (f32
, f64
)
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
JavaScript: Uses Number
type, which is a double-precision floating point (similar to f64
in Rust), but can lead to precision issues
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
Python: Uses bool
type with True
and False
.
JavaScript: Uses boolean
type with true
and false
.
0
and ""
are false
)Rust: Uses bool
type with true
and false
.
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.
Rust: Uses char
type for Unicode Scalar Values.
Python: Characters are just strings of length 1 (str
type).
JavaScript: Characters are also strings of length 1 (String
type).
So that covers all of the scalar types, now let’s look at each data type in the composite category.
In Rust, composite types allow you to group multiple values in one type.
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'
.
Rust: Fixed size, can hold multiple types (e.g., (i32, f64, char)
).
Python: Similar support with tuple
type, using indexing and destructuring.
JavaScript: No direct equivalent; arrays can be used but are not fixed size or type.
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]
Rust: Uses array
for fixed-size collections and vec
for dynamically-sized collections.
Python: Uses list
type, which is dynamic and can hold multiple types.
JavaScript: Uses Array
type, which is dynamic and can hold multiple types.
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).
Rust: Uses struct
for custom data types with named fields.
Python: Uses classes for custom data types with named fields (class
).
JavaScript: Uses objects for custom data types with named fields (object
, class
).
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
}
}
Rust: Uses enum
for enumerations with strong type safety and pattern matching.
Python: Uses enum
module for defining enumerations (enum.Enum
).
JavaScript: No direct equivalent; often use objects with named properties.
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 stringsThough 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!`
Rust: Uses String
for growable strings and &str
for string slices.
Python: Uses str
type, which is immutable. Also offers bytes
for binary data.
JavaScript: Uses String
type, which is immutable.
Accessing elements within arrays, vectors, and strings is a common operation in rust programming.
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 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).
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.
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.
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!
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 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.
If you've made it this far, you've clearly interested in Rust so definitely check out all of my Rust posts and content: