Coming to Rust from Python

What You Need to Know

Rust replaces Python's garbage collection with compile-time ownership rules and requires explicit type definitions and error handling.

The friction is the point

You write a Python script to parse a CSV, spin up a quick HTTP server, and deploy it. It runs. You tweak it. It runs again. Then you switch to Rust. You write the exact same logic. The compiler refuses to build it. You stare at a wall of red text about borrowed values and moved ownership. The friction feels artificial at first. It is not. That friction is the compiler doing the debugging work for you before the code ever touches a server.

Python treats memory like a shared office space. You grab a desk, leave a coffee cup, and a cleaning crew (the garbage collector) sweeps up when you walk out. Rust treats memory like a strict lease. Every piece of data has exactly one owner. When the owner leaves the room, the data is destroyed. If you want someone else to use it, you hand them a reference. The compiler tracks every reference at compile time. It guarantees that no two people hold a mutable key to the same door at the same time. You trade the convenience of automatic cleanup for the guarantee that your program will never accidentally read freed memory or cause a data race.

The compiler is not your enemy. It is a strict co-pilot that refuses to fly into a storm.

How the compiler actually checks your code

Rust does not interpret your code line by line. It analyzes the entire program structure before generating machine code. This static analysis catches type mismatches, uninitialized variables, and lifetime violations before the binary ever runs. The compilation step takes longer than Python's instant startup. That delay is the price of static guarantees. You pay it once at build time instead of paying it repeatedly at runtime with segmentation faults and memory leaks.

use std::net::TcpListener;
use std::io::{self, Read};

/// Accepts a single connection and prints the first 1024 bytes received.
fn handle_connection(mut stream: std::net::TcpStream) -> io::Result<()> {
    let mut buffer = [0; 1024];
    // Read blocks until data arrives or the connection closes.
    // The &mut buffer slice satisfies the Read trait requirement.
    stream.read(&mut buffer)?;
    // Convert raw bytes to a UTF-8 string slice for printing.
    println!("Received: {:?}", std::str::from_utf8(&buffer));
    // Return Ok to signal successful completion to the caller.
    Ok(())
}

fn main() {
    // Binding to a port can fail if the port is already in use.
    // expect documents why a panic would occur here.
    let listener = TcpListener::bind("127.0.0.1:7878").expect("Failed to bind port");
    println!("Server listening on 127.0.0.1:7878");

    // Iterate over incoming connections. Each loop iteration gets a new stream.
    for stream in listener.incoming() {
        // The iterator yields Result<TcpStream, io::Error>.
        // expect handles the error case by terminating the process.
        let stream = stream.expect("Connection failed");
        // stream is moved into handle_connection, transferring ownership.
        handle_connection(stream);
    }
}

When you run cargo run, the compiler translates this into machine code. It checks that stream is moved into handle_connection, meaning handle_connection becomes the new owner. It verifies that buffer is mutable because read requires a mutable slice. It confirms that expect will panic if binding fails, which is acceptable for a simple server but not for production. The compiler enforces these rules through a combination of type checking and borrow checking. Type checking ensures you pass the right data shapes to the right functions. Borrow checking ensures you never create dangling pointers or data races.

Pay the compilation tax once. Avoid the runtime crash forever.

Handling errors without exceptions

Python relies on exceptions to handle failures. You wrap risky code in try/except blocks and let errors bubble up. Rust treats errors as values. Functions return Result<T, E> for operations that can fail, and Option<T> for values that might be absent. You must explicitly handle both cases. The ? operator propagates errors up the call stack, but it does not hide them. You cannot accidentally swallow a failure by forgetting a try block.

use std::fs;
use std::io;

/// Represents a parsed configuration with a host and port.
struct Config {
    host: String,
    port: u16,
}

/// Reads a config file and parses it into a Config struct.
fn load_config(path: &str) -> io::Result<Config> {
    // fs::read_to_string returns Result<String, io::Error>.
    // The ? operator propagates errors up to the caller.
    let contents = fs::read_to_string(path)?;

    let mut lines = contents.lines();
    // Extract the first two lines or return an error.
    // ok_or_else converts Option to Result with a custom error.
    let host = lines.next().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing host"))?;
    let port_str = lines.next().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing port"))?;

    // Parse the port string into a u16.
    // map_err transforms the ParseIntError into an io::Error.
    let port: u16 = port_str.parse().map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid port"))?;

    // Construct and return the Config struct.
    Ok(Config {
        host: host.to_string(),
        port,
    })
}

The community convention favors expect("clear reason") over bare unwrap() because it documents the failure mode for future maintainers. A bare unwrap() tells the reader nothing about why the code assumes success. An expect message turns a panic into a diagnostic clue. You also run cargo fmt and cargo clippy before committing. The ecosystem runs on consistent formatting and automated linting. Style debates die here. Logic debates survive.

Handle errors as data. Do not treat them as emergencies.

Where Python habits break Rust rules

Python developers trip over three specific patterns when switching to Rust. First, variables are immutable by default. If you try to reassign a value, the compiler rejects you with E0384 (cannot assign twice to immutable variable). You must add mut to the binding. Second, values are moved by default. If you pass a String to a function, you cannot use it afterward. The compiler stops you with E0382 (use of moved value). You must clone it or pass a reference. Third, you cannot borrow mutably while an immutable borrow exists. If you read from a vector and then try to push to it in the same scope, you get E0502 (cannot borrow as mutable because it is also borrowed as immutable).

These errors feel restrictive until you realize they prevent entire classes of bugs. The compiler is not being difficult. It is enforcing memory safety rules that Python enforces at runtime, if it enforces them at all. When you see a borrow checker error, read the highlighted line. The compiler usually points to the exact variable causing the conflict. You fix the scope, you adjust the lifetime, or you clone the data. The error message is a map, not a wall.

Read the error message. It usually tells you exactly how to fix the code.

Choosing the right tool for the job

Use Python when you are prototyping rapidly and need to iterate on logic without waiting for compilation. Use Python when you rely on a massive ecosystem of data science or machine learning libraries that do not have Rust equivalents. Use Rust when you need predictable memory usage and zero-cost abstractions in long-running services. Use Rust when you are building systems where a single memory safety bug could crash the entire process or expose sensitive data. Reach for unwrap() only in tests or throwaway scripts where a panic is acceptable. Reach for ? and Result handling in production code where graceful degradation matters. Reach for references (&T) when you only need to read data. Reach for Rc<T> or Arc<T> when multiple owners must share a value.

Pick the tool that matches the problem. Do not force Rust where Python shines, and do not hide Python's weaknesses behind Rust's complexity.

Where to go next