Skip to content
All insights

Academy

Custom I/O: Teaching Rust to Read and Write Your Own Types

Master Rust custom I/O! Learn how to implement Display and FromStr traits so your custom structs work seamlessly with println!, format!, and string parsing.

6 min read

Custom I/O: Teaching Rust to Read and Write Your Own Types
On this page

Here's a frustration that hits every developer about three days into learning Rust. You create a beautiful struct—let's say a PhoneEntry with a name and a number—and you try to print it with println!("{}", entry). The compiler responds with a message that essentially says: "I have no idea how to display this type. You haven't told me how."

And you think: "But... it's a struct. It has fields. Just print the fields!" And the compiler, with infinite patience and zero compromise, repeats: "You haven't told me how."

This feels annoying at first. In Python, every object has a default __repr__. In Java, every object inherits toString() from Object. Why can't Rust just figure it out?

The answer reveals something profound about Rust's design philosophy: there's no single "right" way to display a type. A PhoneEntry might be displayed as "Alice: 555-1234" in a user interface, "PhoneEntry{name: Alice, number: 555-1234}" in a debug log, or "Alice,555-1234" in a CSV export. Rather than picking one default that's wrong half the time, Rust asks you to be explicit. And once you implement the Display trait, your type works seamlessly with println!, format!, write!, and every other formatting macro in the language.

At RantAI, where our AI systems produce structured output that needs to be displayed in logs, dashboards, APIs, and user interfaces, having explicit control over how each type renders itself isn't just nice—it's essential. This article, drawn from Chapter 4, Section 4.3.3 of our guide "The Rust Programming Language," shows you how to implement Display and FromStr to give your types full I/O citizenship in the Rust ecosystem.

Display: Making Your Types Printable#

The Display trait is what println!("{}", x) uses. Implement it, and your type works everywhere formatting is needed:

Rust
use std::fmt;

struct PhoneEntry {
    name: String,
    number: String,
}

impl fmt::Display for PhoneEntry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.name, self.number)
    }
}

fn main() {
    let entry = PhoneEntry {
        name: "Alice".to_string(),
        number: "555-1234".to_string(),
    };

    println!("{}", entry);           // "Alice: 555-1234"
    let s = format!("Contact: {}", entry);  // Works with format! too
    println!("{}", s);               // "Contact: Alice: 555-1234"
}

The fmt method receives a Formatter and writes to it using the write! macro. This is the same mechanism that String, i32, and every other displayable type uses—your custom type is a first-class citizen, indistinguishable from built-in types in how it participates in the formatting system.

Debug vs. Display: Two Audiences#

Rust
#[derive(Debug)]  // Auto-generated debug output
struct PhoneEntry {
    name: String,
    number: String,
}

// Display: for end users
impl fmt::Display for PhoneEntry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.name, self.number)
    }
}

fn main() {
    let entry = PhoneEntry {
        name: "Alice".to_string(),
        number: "555-1234".to_string(),
    };

    println!("Display: {}", entry);    // "Alice: 555-1234" — for humans
    println!("Debug: {:?}", entry);     // PhoneEntry { name: "Alice", number: "555-1234" } — for devs
}

Display (triggered by {}) is the human-readable format—what end users see. Debug (triggered by {:?}) is the developer-readable format—what you see in logs and error messages. Most types derive Debug automatically and implement Display manually. This separation of concerns means your user-facing output can be clean and polished while your debug output includes every internal detail.

FromStr: Teaching Your Types to Parse Themselves#

If Display handles output, FromStr handles input—converting a string into your type:

Rust
use std::str::FromStr;
use std::num::ParseIntError;

#[derive(Debug)]
struct PhoneEntry {
    name: String,
    number: String,
}

impl FromStr for PhoneEntry {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<&str> = s.splitn(2, ':').collect();
        if parts.len() != 2 {
            return Err(format!("Expected 'name:number', got '{}'", s));
        }
        Ok(PhoneEntry {
            name: parts[0].trim().to_string(),
            number: parts[1].trim().to_string(),
        })
    }
}

fn main() {
    let entry: PhoneEntry = "Alice: 555-1234".parse().unwrap();
    println!("{:?}", entry);  // PhoneEntry { name: "Alice", number: "555-1234" }

    let result: Result<PhoneEntry, _> = "invalid input".parse();
    println!("{:?}", result);  // Err("Expected 'name:number', got 'invalid input'")
}

Once you implement FromStr, your type works with the .parse() method on strings—the same method used for parsing i32, f64, bool, and every other parseable type. Your custom type integrates seamlessly into Rust's parsing ecosystem, including user input reading, configuration file parsing, and command-line argument handling.

The Symmetry: Display + FromStr = Round-Trip#

The idiomatic pattern is to make Display and FromStr inverses—if you can display a value as a string and parse that string back, you get round-trip serialization for free:

Rust
fn main() {
    let original = PhoneEntry {
        name: "Bob".to_string(),
        number: "555-5678".to_string(),
    };

    let serialized = format!("{}", original);   // "Bob: 555-5678"
    let restored: PhoneEntry = serialized.parse().unwrap();
    println!("{:?}", restored);  // PhoneEntry { name: "Bob", number: "555-5678" }
}

This pattern is everywhere in Rust: IP addresses, socket addresses, colors, dates. If a type implements both Display and FromStr, you can convert it to and from strings reliably. It's simple, composable, and requires no serialization framework for basic use cases.

Broader Implications: Types as Full I/O Citizens#

When your types implement Display and FromStr, they participate fully in Rust's I/O ecosystem. They can be printed, formatted, logged, parsed from user input, read from configuration files, and serialized to text—all using the same standard mechanisms that built-in types use. There's no special casing, no "oh, that only works for primitive types" limitations.

At RantAI, every domain type in our system—agent configurations, simulation parameters, processing results—implements Display for human-readable output and Debug for developer diagnostics. Many also implement FromStr for configuration parsing. The result is a codebase where every type is a first-class citizen that works naturally with println!, format!, error messages, and logging frameworks.

Practical Applications & Strategic Takeaways#

For newcomers: Derive Debug on everything (#[derive(Debug)]). Implement Display when you need human-readable output. The two serve different audiences and should look different.

For library authors: If your type can be meaningfully represented as a string, implement both Display and FromStr with round-trip compatibility. Users will thank you when they can serialize your types to config files and parse them back.

For teams: Establish a convention for Display formatting early. Should PhoneEntry display as "Alice: 555-1234" or "Alice (555-1234)"? Decide once, implement once, and every use site is consistent.

Our Commitment to Open Knowledge#

RantAI is committed to open education. Custom I/O with Display and FromStr is covered in Chapter 4, Section 4.3.3 of our guide, "The Rust Programming Language," freely available online.

Explore these concepts further: https://trpl.rantai.dev

Support Our Mission & Get Your Handbook#

What's the most useful Display implementation you've written? Or the most creative FromStr parser? Share your custom I/O patterns below!

#RustLang #Display #FromStr #Traits #CustomTypes #RantAI #LearnRust #IOPatterns #SystemsProgramming #CleanCode

Next article

Vec Mastery: The Dynamic Array That Powers 90% of Rust Programs