How to extract options from arguments in Rails

Extracting Options from Arguments in Rails

You can safely extract options hash from the end of an argument list using the extract_options! method in Rails. Not only it simplifies the method definition but it keeps the method's API flexible, allowing you to add new options without breaking existing callers.

5 min read
đź’ˇ
Update: A more modern way to accomplish this is to use keyword arguments like **options. I use it myself and totally forgot about it while writing this post late night 🤦‍♂️ Extracting options is an older pattern before this new Ruby feature was introduced.

While reading the Rails source code, I often come across useful pieces of code that I can use in my own applications.

Recently, I came across one such method, extract_options!. Actually, I had seen it before quite a few times, since Rails uses it extensively. But this time, I decided to read how it was implemented and what it exactly does, and I was surprised by how simple and elegant it is. So I figured I’d write a quick post to share it with you.

It is a subtle but powerful pattern for handling method arguments: extracting options from an array of arguments. This pattern is used throughout the framework to allow flexible APIs without requiring us developers to explicitly pass a hash or named parameters. Which does make sense, since optimizing for programmer happiness is a core principle of Ruby on Rails doctrine.

🎋
Place the happiness of the programmer on a pedestal. Ruby took to not only recognize but accommodate and elevate programmer feelings. Whether they be of inadequacy, whimsy, or joy.

Matz jumped implementational hurdles of astounding complexity to make the machine appear to smile at and flatter its human co-conspirator. Ruby is full of optical illusions where that which seems simple, clear, and beautiful to our mind’s eye actually is an acrobatic mess of wires under the hood.

Optimizing for happiness is perhaps the most formative key to Ruby on Rails. It shall remain such going forward.

- DHH, The Ruby on Rails Doctrine

Let’s explore how extracting options works, why it exists, and how you might use it in your own Rails code.


The Problem: Optional Options

Let's say you’re building a method that accepts multiple positional arguments, but also allows an optional hash of options at the end. Here’s a simple version:

def notify(user, message, options = {})
  # do something
end

notify(user, "Up and running!!", urgent: true)

This works fine when you always pass exactly two arguments plus an optional hash. But what if your method accepts a variable number of arguments and options? For example:

def notify(*args)
  user = args[0]
  message = args[1]
  options = args[2] || {}
end

What if args[2] isn’t a hash? What if someone calls it with more than two arguments? How do we know which one is the options hash?


The Solution: extract_options!

Rails adds a method to Array called extract_options!. It lets you cleanly and consistently pull out an options hash from the end of the argument list, but only if one is provided, and only if it’s safe to extract.

Here's the implementation of this method.

class Array
  def extract_options!
    if last.is_a?(Hash) && last.extractable_options?
      pop
    else
      {}
    end
  end
end

It checks whether the last element is a plain Hash (not a subclass), and if so, removes and returns it. Otherwise, it returns an empty hash.

This lets you write methods like this:

def notify(*args)
  options = args.extract_options!
  user = args[0]
  message = args[1]

  puts "User: #{user}"
  puts "Message: #{message}"
  puts "Urgent: #{options[:urgent]}"
  puts "Via: #{options[:via]}"
end

notify("Akshay", "Up and running!", urgent: true, via: "email")

# Output:

User: Akshay
Message: Up and running!
Urgent: true
Via: email

Inside the notify method, the args array holds the following value:

["Akshay", "Up and running!", {urgent: true, via: "email"}]

If you refer to the implementation of extract_options! above, you can see that we're calling pop on the array, which removes and returns the last element in the array (if a number argument is given to pop, it removes and returns that many elements from the array). The array args is also changed and now only contains the positional arguments minus the options.

args = ["Akshay", "Up and running!", {urgent: true, via: "email"}]
options = args.pop # {urgent: true, via: "email"}
args               # ["Akshay", "Up and running!"]

This is how we extract all the trailing options and store them into options.

Now your method can support flexible arguments without worrying about whether options were passed or not and how many were passed. All of them are accessible within the options hash.

No Breaking Changes

Another major benefit of using extract_options! is that it makes your methods flexible and gives you option to change the implementation in future. Because your method definition doesn't restrict the arguments, you’re free to change the internal code of your method without breaking existing callers.

For example, if your method originally takes two required arguments and one options hash, and you later decide to support an additional positional argument and add new options, you can simply update the method body to look for that argument in the correct position. As long as you’re still calling extract_options!, existing client code that passes the options hash at the end won’t need to change, and the new callers can take advantage of the new options.


A Real Example: Redirection in Rails Router

One place where you can see extract_options! in action is in the Rails router, specifically in the implementation of the redirect helper within the routing DSL.

This method supports multiple calling conventions: you can pass a string path, a block, an object that responds to call, or a hash of options that specifies parts of the request to override (like :subdomain or :path).

get "/stories", to: redirect("/articles")
get "/stories/:name", to: redirect("/articles/%{name}", status: 302)

Here’s a simplified version of the method:

def redirect(*args, &block)
  options = args.extract_options!
  status  = options.delete(:status) || 301
  path    = args.shift

  return OptionRedirect.new(status, options) if options.any?
  return PathRedirect.new(status, path) if String === path

  block = path if path.respond_to?(:call)
  raise ArgumentError, "redirection argument not supported" unless block

  Redirect.new status, block
end

Thanks to extract_options!, this method can gracefully support flexible argument structures without complex conditionals. It pulls out the options hash from the end of the arguments (if it exist), allowing the rest of the method to focus on behavior, not parsing.


Custom Example: Logging with Options

You can use this pattern in your own helpers and libraries. Let’s say you have a method that logs events:

def log_event(*args)
  options = args.extract_options!
  event_name = args.first
  timestamp = options[:at] || Time.now

  puts "[#{timestamp}] Event: #{event_name}"
end

log_event("user_signed_in")
log_event("order_created", at: Time.utc(2024, 12, 25))

In future, you can add more options to the log_event for better customizations, such as custom log levels, tags, or output targets, without breaking the existing calls, which are using the older version of the method.


Notes on Extractable Options

By default, extract_options! only considers instances of the core Hash class extractable:

class Hash
  def extractable_options?
    instance_of?(Hash)
  end
end

This avoids accidentally extracting unexpected objects that might behave like a hash. If you subclass Hash and want your subclass to be treated as extractable, you can override this method.

class MyHash < Hash
  def extractable_options?
    true
  end
end

But generally, it's best to stick to regular hashes.


That's a wrap. I hope you found this article helpful and you learned something new. The extract_options! pattern is one of those small-but-powerful conveniences that makes Rails code feel clean and natural. It allows your method's API to remain flexible while keeping the implementation simple. If you’re writing your own method that takes a variable number of arguments with optional options, consider using this pattern.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.