Overview
Ruby supports keyword arguments as named parameters passed to methods using the key: value
syntax. This feature provides explicit parameter naming, optional parameters with defaults, and required keyword parameters. Ruby implements keyword arguments through the **
splat operator and special syntax in method signatures.
The keyword argument system handles parameter validation, default value assignment, and argument forwarding. Methods can accept keyword arguments alongside positional parameters, enabling flexible API designs. Ruby treats keyword arguments as a hash-like structure internally while maintaining compile-time parameter validation.
def create_user(name, email:, role: 'user', **options)
puts "Name: #{name}, Email: #{email}, Role: #{role}"
puts "Options: #{options}"
end
create_user('John', email: 'john@example.com', role: 'admin', active: true)
# Name: John, Email: john@example.com, Role: admin
# Options: {:active=>true}
Keyword arguments separate into three categories: optional with defaults, required without defaults, and captured extra arguments. The **
operator captures undefined keyword arguments into a hash, similar to how *
captures positional arguments into an array.
# Required keyword argument
def login(username:, password:)
# Implementation
end
# Optional with default
def search(query:, limit: 10)
# Implementation
end
# Capturing extra arguments
def configure(**settings)
settings.each { |key, value| puts "#{key}: #{value}" }
end
Ruby enforces keyword argument requirements at call time, raising ArgumentError
for missing required parameters. The interpreter validates argument names against the method signature, preventing typos in parameter names.
Basic Usage
Methods define keyword arguments using the colon syntax in parameter lists. Required keyword arguments use name:
without default values, while optional arguments use name: default_value
. The method can mix positional and keyword arguments with specific ordering rules.
class DatabaseConnection
def initialize(host:, port: 5432, username:, password:, ssl: false)
@host = host
@port = port
@username = username
@password = password
@ssl = ssl
end
def connect
puts "Connecting to #{@host}:#{@port} (SSL: #{@ssl})"
end
end
# Usage with mixed required and optional keyword arguments
db = DatabaseConnection.new(
host: 'localhost',
username: 'admin',
password: 'secret',
ssl: true
)
db.connect
# Connecting to localhost:5432 (SSL: true)
The double splat operator **
captures additional keyword arguments into a hash parameter. This enables flexible APIs that accept both defined parameters and arbitrary options.
def send_notification(message:, **delivery_options)
puts "Message: #{message}"
if delivery_options[:email]
puts "Email delivery to: #{delivery_options[:email]}"
end
if delivery_options[:sms]
puts "SMS delivery to: #{delivery_options[:sms]}"
end
puts "Priority: #{delivery_options.fetch(:priority, 'normal')}"
end
send_notification(
message: 'Server maintenance tonight',
email: 'admin@company.com',
priority: 'high'
)
# Message: Server maintenance tonight
# Email delivery to: admin@company.com
# Priority: high
Keyword arguments work with block-accepting methods and can delegate arguments to other methods. The delegation preserves argument names and types while enabling method composition.
def process_data(data, transform: :upcase, **options, &block)
transformed = data.send(transform)
if block_given?
result = block.call(transformed, **options)
else
result = default_processing(transformed, **options)
end
result
end
def default_processing(data, prefix: '', suffix: '')
"#{prefix}#{data}#{suffix}"
end
result = process_data('hello world', transform: :capitalize, prefix: '>> ') do |data, **opts|
"PROCESSED: #{data}"
end
puts result
# PROCESSED: Hello world
Hash arguments convert automatically to keyword arguments when passed to methods expecting keywords. This conversion applies only when the hash appears as the last argument and contains symbol keys.
def create_account(name:, email:, type: 'standard')
puts "Creating #{type} account for #{name} (#{email})"
end
# Direct keyword arguments
create_account(name: 'Alice', email: 'alice@example.com', type: 'premium')
# Hash conversion to keyword arguments
account_data = { name: 'Bob', email: 'bob@example.com' }
create_account(**account_data, type: 'premium')
# Creating premium account for Bob (bob@example.com)
Advanced Usage
Keyword argument delegation enables sophisticated method composition patterns. Methods can forward keyword arguments while intercepting specific parameters or adding additional arguments. This pattern supports decorator and middleware implementations.
module Cacheable
def with_cache(cache_key:, ttl: 3600, **kwargs)
if cached_value = cache_get(cache_key)
return cached_value
end
result = yield(**kwargs)
cache_set(cache_key, result, ttl)
result
end
private
def cache_get(key)
@cache ||= {}
entry = @cache[key]
return nil unless entry
return nil if Time.now > entry[:expires]
entry[:value]
end
def cache_set(key, value, ttl)
@cache ||= {}
@cache[key] = { value: value, expires: Time.now + ttl }
end
end
class ApiClient
include Cacheable
def fetch_user_data(user_id:, include_posts: false, **options)
with_cache(cache_key: "user_#{user_id}", **options) do |**forwarded|
api_request("users/#{user_id}", include_posts: include_posts, **forwarded)
end
end
private
def api_request(endpoint, **params)
# Simulate API call
{ endpoint: endpoint, params: params, timestamp: Time.now }
end
end
client = ApiClient.new
data = client.fetch_user_data(
user_id: 123,
include_posts: true,
ttl: 1800,
format: 'json'
)
Method objects and lambda expressions support keyword arguments with identical semantics to regular methods. This enables functional programming patterns with named parameters and argument validation.
# Lambda with keyword arguments
user_validator = ->(name:, email:, age: nil, **metadata) do
errors = []
errors << 'Name required' if name.nil? || name.empty?
errors << 'Invalid email' unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\z/i)
errors << 'Age must be positive' if age && age <= 0
{
valid: errors.empty?,
errors: errors,
metadata: metadata
}
end
# Method object creation
validate_method = user_validator.method(:call)
# Usage with various argument patterns
result1 = user_validator.call(name: 'John', email: 'john@example.com', source: 'signup')
result2 = validate_method.call(name: '', email: 'invalid', age: -5)
puts "Result 1 valid: #{result1[:valid]}, metadata: #{result1[:metadata]}"
puts "Result 2 errors: #{result2[:errors]}"
Metaprogramming with keyword arguments requires careful handling of method signatures and argument forwarding. Ruby provides introspection methods to examine parameter information and build dynamic method calls.
class DynamicProxy
def initialize(target)
@target = target
end
def method_missing(method_name, *args, **kwargs, &block)
if @target.respond_to?(method_name)
method_info = @target.method(method_name).parameters
# Log method call with parameter details
log_call(method_name, method_info, args, kwargs)
# Forward the call preserving all argument types
@target.send(method_name, *args, **kwargs, &block)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
@target.respond_to?(method_name, include_private) || super
end
private
def log_call(method_name, parameters, args, kwargs)
puts "Calling #{method_name}:"
parameters.each_with_index do |param, index|
type, name = param
case type
when :req
puts " #{name}: #{args[index]} (required positional)"
when :opt
puts " #{name}: #{args[index]} (optional positional)" if args[index]
when :keyreq
puts " #{name}: #{kwargs[name]} (required keyword)"
when :key
puts " #{name}: #{kwargs[name]} (optional keyword)" if kwargs[name]
when :keyrest
puts " **#{name}: #{kwargs.except(*parameters.map(&:last))} (keyword rest)"
end
end
end
end
class Calculator
def compute(base:, multiplier: 1, **options)
result = base * multiplier
result += options[:offset] if options[:offset]
result
end
end
calc = Calculator.new
proxy = DynamicProxy.new(calc)
result = proxy.compute(base: 10, multiplier: 3, offset: 5)
# Calling compute:
# base: 10 (required keyword)
# multiplier: 3 (optional keyword)
# **options: {:offset=>5} (keyword rest)
puts "Result: #{result}" # Result: 35
Common Pitfalls
Keyword argument delegation creates subtle bugs when methods modify the arguments hash or when argument names clash. Ruby passes keyword arguments by reference, so modifications affect the original hash structure.
def process_order(items:, **options)
# WRONG: Modifying options affects caller's hash
options[:processed_at] = Time.now
options[:total] = calculate_total(items)
fulfill_order(items: items, **options)
end
def fulfill_order(items:, total:, processed_at:, **metadata)
puts "Fulfilling order: #{items.count} items, total: $#{total}"
puts "Processed at: #{processed_at}"
puts "Metadata: #{metadata}"
end
# The caller's hash gets modified unexpectedly
order_options = { priority: 'high', shipping: 'express' }
items = ['book', 'pen']
process_order(items: items, **order_options)
puts "Original options modified: #{order_options}"
# Original options modified: {:priority=>"high", :shipping=>"express", :processed_at=>..., :total=>...}
# CORRECT: Create a copy before modification
def process_order_safe(items:, **options)
safe_options = options.dup
safe_options[:processed_at] = Time.now
safe_options[:total] = calculate_total(items)
fulfill_order(items: items, **safe_options)
end
private
def calculate_total(items)
items.count * 10 # Simple calculation
end
Required keyword arguments create confusing error messages when missing, especially in deeply nested method calls. Ruby reports the immediate method where the error occurs, not the original call site.
class ConfigurationBuilder
def build(database:, **options)
validate_database_config(database: database)
create_config(database: database, **options)
end
private
def validate_database_config(database:)
check_connection_params(**database) # Error occurs here
end
def check_connection_params(host:, username:, password:)
# Validation logic
puts "Checking connection to #{host} for #{username}"
end
def create_config(database:, **options)
puts "Creating configuration with database: #{database}"
end
end
builder = ConfigurationBuilder.new
# This will raise ArgumentError for missing password, but error location is confusing
begin
builder.build(
database: { host: 'localhost', username: 'admin' } # missing password
)
rescue ArgumentError => e
puts "Error: #{e.message}"
puts "Error occurred in: #{e.backtrace.first}"
end
# Better error handling with context
class ImprovedConfigurationBuilder
def build(database:, **options)
validate_required_database_fields(database)
validate_database_config(database: database)
create_config(database: database, **options)
end
private
def validate_required_database_fields(database)
required_fields = [:host, :username, :password]
missing_fields = required_fields - database.keys
unless missing_fields.empty?
raise ArgumentError, "Missing required database configuration: #{missing_fields.join(', ')}"
end
end
def validate_database_config(database:)
check_connection_params(**database)
end
def check_connection_params(host:, username:, password:)
puts "Checking connection to #{host} for #{username}"
end
def create_config(database:, **options)
puts "Creating configuration"
end
end
Hash-to-keyword conversion fails silently with string keys, leading to runtime errors when methods expect symbol-keyed keyword arguments. This commonly occurs when processing JSON or form data.
def create_user_account(name:, email:, role: 'user')
puts "Creating account: #{name} (#{email}) as #{role}"
end
# JSON data typically has string keys
json_data = { 'name' => 'Alice', 'email' => 'alice@example.com', 'role' => 'admin' }
# This fails silently - no keyword arguments passed
begin
create_user_account(**json_data)
rescue ArgumentError => e
puts "Error with string keys: #{e.message}"
end
# Convert string keys to symbols for keyword argument compatibility
def symbolize_keys(hash)
hash.transform_keys(&:to_sym)
end
# Correct approach
symbolized_data = symbolize_keys(json_data)
create_user_account(**symbolized_data)
# Alternative: Handle mixed key types gracefully
def flexible_user_creation(params)
# Normalize all keys to symbols
normalized = params.transform_keys(&:to_sym)
create_user_account(**normalized)
end
flexible_user_creation(json_data) # Works with string keys
flexible_user_creation(name: 'Bob', email: 'bob@example.com') # Works with symbol keys
Reference
Method Definition Syntax
Syntax Pattern | Description | Example |
---|---|---|
name: |
Required keyword argument | def method(name:); end |
name: default |
Optional keyword with default | def method(name: 'default'); end |
**rest |
Capture extra keyword arguments | def method(**opts); end |
name:, **rest |
Required plus capture | def method(name:, **opts); end |
Parameter Ordering Rules
Ruby enforces strict parameter ordering in method definitions:
- Required positional parameters (
name
) - Optional positional parameters (
name = default
) - Splat parameter (
*args
) - Required keyword parameters (
name:
) - Optional keyword parameters (
name: default
) - Double splat parameter (
**kwargs
) - Block parameter (
&block
)
def complete_signature(req_pos, opt_pos = 'default', *args, req_kw:, opt_kw: 'default', **kwargs, &block)
# Valid method signature following all ordering rules
end
Argument Passing Patterns
Call Pattern | Description | Requirement |
---|---|---|
method(key: value) |
Direct keyword argument | Method defines key: parameter |
method(**hash) |
Hash expansion to keywords | Hash keys must be symbols |
method(pos, key: value) |
Mixed positional and keyword | Follows parameter order |
method(**h1, **h2) |
Multiple hash expansion | Ruby 3.0+ only |
Error Types and Causes
Error | Cause | Example |
---|---|---|
ArgumentError (missing keyword: name) |
Required keyword not provided | def m(name:); end; m() |
ArgumentError (unknown keyword: invalid) |
Unexpected keyword argument | def m(name:); end; m(name: 'x', invalid: 'y') |
ArgumentError (wrong number of arguments) |
Positional argument mismatch | Mixed with keyword issues |
Introspection Methods
Methods for examining keyword argument information:
def sample_method(req:, opt: 'default', **rest)
# Method body
end
method_obj = method(:sample_method)
# Get parameter information
params = method_obj.parameters
# => [[:keyreq, :req], [:key, :opt], [:keyrest, :rest]]
# Parameter types:
# :keyreq - Required keyword argument
# :key - Optional keyword argument
# :keyrest - Keyword rest parameter (**kwargs)
# Check if method accepts keyword arguments
has_keywords = params.any? { |type, _| [:keyreq, :key, :keyrest].include?(type) }
# Get required keyword parameter names
required_keywords = params.select { |type, _| type == :keyreq }.map(&:last)
# => [:req]
# Get optional keyword parameter names
optional_keywords = params.select { |type, _| type == :key }.map(&:last)
# => [:opt]
Compatibility Notes
Ruby versions handle keyword arguments differently:
- Ruby 2.6 and earlier: Automatic conversion between hash and keyword arguments
- Ruby 2.7: Deprecation warnings for automatic conversion
- Ruby 3.0+: Strict separation between positional and keyword arguments
- Ruby 3.0+: Support for multiple hash expansion (
**h1, **h2
)
For cross-version compatibility, explicitly separate positional and keyword arguments and avoid relying on automatic hash conversion.