Why Rust doesn't have default arguments — and how to work around it
\n\nIf 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."
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.
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\nMethod 1. Using Option<T> — the simplest option
\n\nThe most straightforward way is to accept arguments as Option<T> and substitute default values inside the function.
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\nPros:
\n- \n
- Minimal code \n
- Understandable even for beginners \n
- No additional structures required \n
Cons:
\n- \n
- Each call requires wrapping in
Some(...)orNone\n - If there are several parameters, the code becomes cumbersome \n
This approach works well for functions with 1-2 optional parameters. But if there are more, readability suffers.
\n\nMethod 2. Configuration structure — clean and scalable
\n\nWhen 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.
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\nPros:
\n- \n
- Maximum flexibility \n
- You can add new fields without changing the function signature \n
- The
..Default::default()syntax is very readable \n
Cons:
\n- \n
- Requires more boilerplate code \n
- You need to explicitly implement the
Defaulttrait \n
This is the ideal choice for real projects: libraries, configuration functions, complex APIs.
\n\nMethod 3. Builder Pattern — for those who love chains
\n\nThe 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.
struct Greeter {\n name: String,\n greeting: String,\n excitement_level: u8,\n}\n\nstruct GreeterBuilder {\n nam