Degen Code

Degen Code

Rust for Pythonistas

Part III: Error Handling & Type Conversions

Sep 25, 2025
∙ Paid
1
Share

Errors & Exceptions

Python uses exceptions to signal that something has gone wrong. A good Python app will anticipate what exceptions might be raised by various function or method calls and define the behavior when those exceptions occur.

Rust does not have exceptions. Instead it follows the “error as value” approach, where instead of raising an exception and breaking the normal control flow of the program, it will return an error instead of a value. The responsibility for handling both options falls to the calling code. Rust operations that can fail typically return a Result type which can hold one of two things: the value or an error.

Part II included Rust code with expressions ending with .unwrap() but I did not elaborate on why. Let’s get into it!

Result is a type of Rust struct called an enum, which organizes and labels related values. It is defined in std::result:

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Angled brackets make another appearance here, which signifies that Result is generic over types T and E. The enum holds two variants (Ok, and Err) which are named variants that wrap a value of a generic type.

Whenever you call a function that might fail, you should expect to receive a Result. The Result will either be an Ok wrapping the good value, or an Err wrapping the error value.

If you control the function and the input and know it will not fail, you might choose to skip the error checking and just access the value directly. Calling unwrap() on an Ok will extract the value from that wrapper. This is what I was doing in Part II.

Put this into Rust Playground:

pub fn get_69() -> Result<u8, String> {
    Ok(69)
}

fn main() {
    let known_good_value = get_69().unwrap();
    println!("{:?}", known_good_value);
}

And run it to see:

69

Now let’s change the function to give an error instead:

pub fn get_69() -> Result<u8, String> {
    Err(String::from("Can't get 69, not nice"))
}

Running this blows up with a panic message:

thread 'main' panicked at src/main.rs:6:37:
called `Result::unwrap()` on an `Err` value: "Can't get 69, not nice"

The unwrap method quickly peels off the wrapper holding a successful value. However if you unwrap an Err, the runtime will panic, triggering an immediate cleanup and shutdown.

Python is like this, too. If a program raises an exception which is not caught, it will eventually reach the entry point of the program and the interpreter will clean up and shut down.

Matching

Dealing with Rust errors is similar to using Python’s structural pattern matching (see PEP-064), whereby you can match directly on the value inside the wrapping variant:

pub fn get_69() -> Result<u8, String> {
    Ok(69)
    // Err(String::from("Can't get 69, not nice"))
}

fn main() {
    let unknown_value = get_69();
    match unknown_value {
        Ok(good_value) => println!("Got a value: {}", good_value),
        Err(message) => println!("Got an error: {}", message),
    }
}

Running this, we get:

Got a value: 69

And after changing the function to return the Err instead:

Got an error: Can't get 69, not nice

The match keyword allows us to set control flow depending on the wrapper variant, and to assign the wrapped value to a variable that we could use. This allows us to offload the “did the call succeed?” flow to the match block, and concentrate on the “then do this…” logic in each arm that follows the match.

The ? Operator

Sometimes an error is unrecoverable, so there’s no point handling it. For situations where defining a complete match block would be overkill, we can use the ? operator which will extract the wrapped values for either the Ok or the Err variant without using a full match block.

Try it out:

pub fn get_69() -> Result<u8, String> {
    // Ok(69)
    Err(String::from("Can't get 69, not nice"))
}

fn main() {
    let only_good_value = get_69()?;
}

This fails because the ? operator can cause main to return a String instead of nothing, which is the default return type. We can adjust the function signature to specify the two possibilities expressed as a Result:

pub fn get_69() -> Result<u8, String> {
    // Ok(69)
    Err(String::from("Can't get 69, not nice"))
}

fn main() -> Result<(), String> {
    get_69()?;
    Ok(())
}

The Ok variant runs successfully with no output, and the Err variant results in the message:

Error: "Can't get 69, not nice"

Notably, using the ? operator on an Err variant does not trigger a panic like unwrap would — it just returns the wrapped error.

Type Conversions

The built-in Rust types are simple to convert. Like types can be converted in-line using cast in an assignment expression:

fn main() {
    let i: u64 = 420;
    let j: u32 = i as u32;
    
    println!(”{i}”);
    println!(”{j}”);
}

Output:

420
420

However, please be careful! Casting between different bit length types can result in wrapping if the source value exceeds the range of the destination type:

fn main() {
    let i: u64 = 420;
    let j: i8 = i as i8;
    
    println!(”{i}”);
    println!(”{j}”);
}

Here, the i8 (8 bit signed integer) wraps from 127 to -128 when it reaches the upper limit of its range:

420
-92

Complex Conversions

As you move beyond the built-ins, you may want to perform more complex type conversions.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2025 BowTiedDevil
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture