Default function arguments in Rust: elegant solutions without overloads

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Rust doesn't have default arguments — and how to work around it

\n\n

If you came to Rust from Python, JavaScript, or C++, you've likely encountered a surprise: Rust has no built-in support for default arguments. You can't simply write fn greet(name = "world") — the compiler will say "no."

\n\n

But don't be disappointed! Rust offers other, more flexible and safe approaches. In this tutorial, we'll cover 3 proven ways to implement default argument behavior. You'll learn how to use Option<T>, configuration structures, and the Builder pattern — and which method to choose for your task.

\n\n

This article is based on a real question from Stack Overflow (over 50,000 views) — we've adapted it for an educational format. Let's go!

\n\n

Method 1. Using Option<T> — the simplest option

\n\n

The most straightforward way is to accept arguments as Option<T> and substitute default values inside the function.

\n\n
fn greet(name: Option<&str>) {\n    let name = name.unwrap_or("world");\n    println!("Hello, {}!", name);\n}\n\nfn main() {\n    greet(Some("Anna")); // Hello, Anna!\n    greet(None);         // Hello, world!\n}\n
\n\n

Pros:

\n
    \n
  • Minimal code
  • \n
  • Understandable even for beginners
  • \n
  • No additional structures required
  • \n
\n\n

Cons:

\n
    \n
  • Each call requires wrapping in Some(...) or None
  • \n
  • If there are several parameters, the code becomes cumbersome
  • \n
\n\n

This approach works well for functions with 1-2 optional parameters. But if there are more, readability suffers.

\n\n

Method 2. Configuration structure — clean and scalable

\n\n

When parameters become 3, 5, or 10, a configuration structure comes to the rescue. You create a separate type that stores all settings and implement the Default trait for it.

\n\n
struct GreetConfig {\n    name: String,\n    greeting: String,\n    excitement_level: u8,\n}\n\nimpl Default for GreetConfig {\n    fn default() -> Self {\n        Self {\n            name: "world".to_string(),\n            greeting: "Hello".to_string(),\n            excitement_level: 1,\n        }\n    }\n}\n\nfn greet(config: GreetConfig) {\n    let excitement = "!".repeat(config.excitement_level as usize);\n    println!("{}, {}{}", config.greeting, config.name, excitement);\n}\n\nfn main() {\n    // Use default values\n    greet(GreetConfig::default());\n    // Output: Hello, world!\n\n    // Change only what's needed\n    greet(GreetConfig {\n        name: "Anna".to_string(),\n        ..Default::default()\n    });\n    // Output: Hello, Anna!\n\n    // Fully custom call\n    greet(GreetConfig {\n        name: "World".to_string(),\n        greeting: "Greetings".to_string(),\n        excitement_level: 3,\n    });\n    // Output: Greetings, World!!!\n}\n
\n\n

Pros:

\n
    \n
  • Maximum flexibility
  • \n
  • You can add new fields without changing the function signature
  • \n
  • The ..Default::default() syntax is very readable
  • \n
\n\n

Cons:

\n
    \n
  • Requires more boilerplate code
  • \n
  • You need to explicitly implement the Default trait
  • \n
\n\n

This is the ideal choice for real projects: libraries, configuration functions, complex APIs.

\n\n

Method 3. Builder Pattern — for those who love chains

\n\n

The Builder pattern is a classic in the Rust ecosystem. You create a builder structure that accumulates parameters and at the end calls the build() method.

\n\n
struct Greeter {\n    name: String,\n    greeting: String,\n    excitement_level: u8,\n}\n\nstruct GreeterBuilder {\n    nam                

Blogs

Book Recommendations