Default and Keyword Arguments

Published by Programming Line in Ruby. Last updated .

In Ruby, methods rarely take just a rigid list of required arguments — default arguments let you supply fallback values for parameters the caller skips, and keyword arguments let callers pass values by name instead of position. Together they make method signatures self-documenting and flexible: a caller can override only the options they care about, in any order, without memorizing an exact positional sequence. This lesson covers how both features work under the hood, how they combine with splats and blocks, and the mistakes that trip up even experienced Rubyists.

Overview: How Default and Keyword Arguments Work

Every Ruby method has a parameter list, and each parameter can be required, optional with a default, or keyword-based. When you call a method, Ruby binds the arguments you pass to these parameters according to a well-defined algorithm, not left-to-right guesswork.

A default positional argument looks like def greet(name, greeting = "Hello"). If the caller omits greeting, Ruby evaluates the expression "Hello" at call time and assigns it. This matters: the default expression is not evaluated once when the method is defined — it is re-evaluated fresh every single time the method runs and that particular argument is missing. That means a default can safely call another method or reference an earlier parameter, as in def line(a, b = a).

A keyword argument looks like def build_profile(name:, age: 18). Keyword arguments are matched by name, not position, so a caller can write build_profile(age: 30, name: "Ada") and Ruby figures out which value goes where. A keyword with no default (name:) is required — omitting it raises ArgumentError: missing keyword: :name. A keyword with a default (age: 18) is optional.

Under the hood, when Ruby parses a call, it treats trailing key: value pairs as keyword arguments bound to the method’s keyword parameters — a distinct mechanism from passing a literal Hash, even though the two look similar at a glance. This distinction was tightened in Ruby 3.0: positional hashes and keyword arguments no longer auto-convert into each other, removing a class of ambiguous calls (see Common Mistakes below). You can capture “any extra keywords” with a double splat parameter, **options, which gathers unmatched keyword arguments into an ordinary Hash inside the method body.

Syntax

def method_name(pos1, pos2 = "default", *rest, kw1:, kw2: "default", **kwrest, &block)
  # method body
end
Part Meaning
pos1 Required positional argument — caller must supply it, matched by position.
pos2 = "default" Optional positional argument. Uses "default" if the caller doesn’t supply a value in that position.
*rest Splat — collects any remaining positional arguments into an Array.
kw1: Required keyword argument — caller must pass kw1: value, or Ruby raises ArgumentError.
kw2: "default" Optional keyword argument with a fallback value.
**kwrest Double splat — collects any unmatched keyword arguments into a Hash.
&block Captures an implicit block passed to the method as a Proc object.

Ruby expects this rough ordering: required positionals, then optional positionals, then a splat, then keyword parameters, then a double splat, then the block. You will rarely use every piece in one method, but each composes with the others.

Examples

Example 1: Default Positional Arguments

def greet(name, greeting = "Hello")
  "#{greeting}, #{name}!"
end

puts greet("Ruby")
puts greet("World", "Hi")

Output:

Hello, Ruby!
Hi, World!

The first call omits greeting entirely, so Ruby evaluates the default expression "Hello" and uses it. The second call supplies both arguments positionally, so the default is never touched.

Example 2: Keyword Arguments

def build_profile(name:, age: 18, city: "Unknown")
  "#{name} (#{age}) from #{city}"
end

puts build_profile(name: "Ada")
puts build_profile(name: "Grace", age: 36, city: "New York")

Output:

Ada (18) from Unknown
Grace (36) from New York

name: has no default, so it must be supplied every time. age: and city: fall back to their defaults when omitted. The second call reorders the keywords compared to the parameter list — that’s the whole point of keyword arguments: order stops mattering because every value is labeled.

Example 3: Combining Positional, Keyword, and Double-Splat Arguments

def create_user(username, role: "member", **options)
  details = options.map { |k, v| "#{k}=#{v}" }.join(", ")
  summary = "User #{username} [role: #{role}]"
  details.empty? ? summary : "#{summary} {#{details}}"
end

puts create_user("jdoe")
puts create_user("asmith", role: "admin", verified: true, age: 29)

Output:

User jdoe [role: member]
User asmith [role: admin] {verified=true, age=29}

username is a required positional argument. role: is an optional keyword with a default. Anything else the caller passes as a keyword — verified:, age: — has no matching parameter name, so **options sweeps it into a plain Hash the method can inspect generically. This pattern is common for methods that accept a core set of known options plus arbitrary extra metadata.

Example 4: Keyword Defaults That Reference Other Parameters

def describe_temp(celsius:, fahrenheit: celsius * 9.0 / 5 + 32) = "#{celsius}C is #{fahrenheit}F"

puts describe_temp(celsius: 0)
puts describe_temp(celsius: 100, fahrenheit: 212.0)

Output:

0C is 32.0F
100C is 212.0F

This uses an endless method definition (def name(args) = expression), Ruby’s terse one-line syntax for simple methods. The default for fahrenheit is a full expression that reads the already-bound celsius parameter — since defaults are evaluated at call time, in parameter order, an earlier parameter is always available to a later one’s default expression. The second call overrides fahrenheit explicitly, so the conversion expression never runs.

How It Works Step by Step

When you call a Ruby method, argument binding happens roughly in this order:

  1. Ruby counts the required positional parameters and assigns the first arguments to them, left to right.
  2. Trailing key: value pairs in the call are recognized as keyword arguments — not a positional Hash — if the method defines any keyword parameters, and are set aside from the positional arguments.
  3. Remaining positional arguments fill optional positional parameters left to right; for any optional parameter the caller didn’t supply, Ruby evaluates that parameter’s default expression now, in the method’s own scope, meaning it can see already-bound earlier parameters.
  4. If a splat (*rest) is present, every positional argument not yet consumed is collected into an Array.
  5. Each required keyword parameter is matched against the supplied keyword arguments; if one is missing, Ruby raises ArgumentError: missing keyword: :name immediately.
  6. Each optional keyword parameter is filled from the supplied keywords, or, if absent, its default expression is evaluated in order — exactly like optional positionals.
  7. Any keyword the caller passed that doesn’t match a named parameter is collected by a double splat (**kwrest) if present, or triggers ArgumentError: unknown keyword if not.
  8. Finally, if the call passed a block, it’s bound to the trailing &block parameter, if the method declares one.

This ordering is why a default expression can reference an earlier parameter but never a later one, and why Ruby can tell you exactly which named argument is missing rather than a generic “wrong number of arguments” — keyword arguments carry their names all the way through the call.

Common Mistakes

Mistake 1: Using a Shared, Mutable Object as a Default

A default expression is evaluated fresh on every call where it’s needed, so tags = [] as a default is safe — you get a new array each time. But if the default references an existing object, like a constant, every call that uses the default shares the same object:

DEFAULT_TAGS = ["general"]

def add_tag(tag, tags = DEFAULT_TAGS)
  tags << tag
  tags
end

add_tag("ruby")
puts add_tag("rails").inspect

Output:

["general", "ruby", "rails"]

Both calls default to the very same DEFAULT_TAGS array object, so << mutates it in place every time. The second call didn’t just add "rails" to a fresh list — it saw the leftover "ruby" from the first call, permanently corrupting the shared constant. Fix it by making the default a fresh, independent literal instead of a reference to a shared object:

def add_tag(tag, tags: [])
  tags << tag
  tags
end

puts add_tag("ruby").inspect
puts add_tag("rails").inspect

Output:

["ruby"]
["rails"]

Now every call gets a brand-new array, because [] is a literal expression evaluated at call time, not a reference to one long-lived object.

Mistake 2: Passing a Hash Where Keyword Arguments Are Expected

In Ruby 3.0+, positional hashes and keyword arguments are strictly separate — a method that declares keyword parameters will not automatically treat a trailing Hash argument as keywords:

def build_profile(name:, age: 18)
  "#{name} (#{age})"
end

opts = { name: "Ada", age: 36 }
build_profile(opts)

This raises ArgumentError: wrong number of arguments (given 1, expected 0) — Ruby sees one positional argument (the Hash) being handed to a method that accepts zero positional arguments, because name: and age: are keyword parameters, not positional ones. The fix is to explicitly splat the hash into keyword arguments with **:

def build_profile(name:, age: 18)
  "#{name} (#{age})"
end

opts = { name: "Ada", age: 36 }
puts build_profile(**opts)

Output:

Ada (36)

**opts explicitly unpacks the hash’s key-value pairs as keyword arguments at the call site — unambiguous, and it works whether the method takes named keywords, a **kwrest catch-all, or both.

Best Practices

  • Prefer keyword arguments over positional ones once a method takes more than two or three parameters — they make call sites self-documenting and immune to argument-order mistakes.
  • Use required keyword arguments (name:, no default) for values a caller must always think about; reserve defaults for values with a genuinely sensible fallback.
  • Never default a mutable argument to a shared object — a constant, a class variable, another object’s array. Default to a literal ([], {}) or nil and build the object inside the method body instead.
  • Use **options sparingly, only when a method genuinely needs to accept an open-ended, forwardable set of extra options — an explicit keyword list is easier to read and easier for tools to check.
  • When forwarding a hash of options to another method, splat it explicitly with ** rather than relying on implicit conversion, which Ruby 3+ no longer performs.
  • Keep default expressions simple and side-effect-free; a default that raises, logs, or mutates state is a surprise waiting to happen for the next reader.
  • Reach for an endless method definition (def name(args) = expr) only for genuinely one-line methods — once a default expression or body grows complex, switch back to multi-line def...end for readability.

Practice Exercises

  1. Write a method power(base, exponent = 2) that returns base raised to exponent using **. Call it once with just a base and once with both arguments, and puts both results.
  2. Write a method format_name(first:, last:, middle: nil) that returns "First Middle Last" when a middle name is given, and "First Last" when it’s nil. Hint: Ruby’s truthiness means you can branch directly on whether middle is truthy.
  3. Write a method summarize_order(item, quantity: 1, **extras) that returns a string like "3x Widget", plus — if extras isn’t empty — an appended list of the extra key/value pairs. Call it with and without extra keyword arguments and compare the output.

Summary

  • Default arguments (param = value) supply a fallback that’s evaluated fresh, at call time, only when the caller omits that argument.
  • Keyword arguments (param: / param: value) are matched by name, not position, making call sites self-documenting and order-independent.
  • A keyword with no default is required; omitting it raises ArgumentError: missing keyword.
  • **options collects any unmatched keyword arguments into a Hash; ** at a call site splats a Hash back out into keyword arguments.
  • Since Ruby 3.0, a positional Hash is never silently treated as keyword arguments — splat it explicitly with **.
  • Never default a parameter to a shared mutable object; use a literal or nil so each call gets its own independent value.