Ruby Methods: Defining, Calling, and Customizing Methods in Ruby
what are methods?
Methods are the fundamental unit of reusable code in Ruby. They let you package a sequence of operations under a name, then call that name whenever you need those operations performed. Without methods, you’d copy and paste the same code everywhere; a maintenance nightmare that Ruby spares you from.
Think of a method as a named action your program can perform on demand. “Greet the user,” “calculate a total,” “validate an email address”; each of these is a perfect candidate for a method.
Defining a Method
You define a method with the def keyword, followed by the method name, an optional parameter list, and closed with end:
def greet(name)
puts "Hello, #{name}!"
end
greet("Alice")
# => Hello, Alice!
Method names in Ruby follow a convention: lowercase letters with underscores (snake_case). Ruby does not enforce this, but it is the community standard and reading code that deviates from it will surprise other developers.
You can also define methods that take no parameters. A parameterless method is useful when the action requires no external input, such as returning a constant greeting or a computed default value:
def hello
"Hello, world!"
end
hello
# => "Hello, world!"
Ruby allows method names to end with ? (predicates), ! (bang methods), or = (setter methods). These are syntactic conventions that signal intent to other developers; the language itself treats them as part of the name.
parameters and default values
When you need a method to accept input, you define parameters; variables that receive values when the method is called:
def power(base, exponent)
base ** exponent
end
power(2, 3)
# => 8
Default values make parameters optional. Ruby evaluates default values left-to-right at the point of the method call, which means you can reference earlier parameters in later defaults. This pattern simplifies method signatures for common use cases:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def greet(name = "World")
"Hello, #{name}!"
end
greet
# => "Hello, World!"
greet("Alice")
# => "Hello, Alice!"
Be careful with mutable default values like arrays or hashes. If you write def add_item(item, list = []), all calls that skip the list argument share the same array object. This is a classic Ruby gotcha that can produce confusing bugs:
def append(item, list = [])
list << item
list
end
append("a") << "b" # this mutates the shared default array
append("c") # => ["a", "b", "c"] — surprising!
The safer approach is to use nil as the default and initialize inside the method. This guarantees a fresh array on every call that does not pass a list argument:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def append(item, list = nil)
list ||= []
list << item
list
end
append("a")
# => ["a"]
append("b")
# => ["b"]
variable-length arguments
Ruby lets you collect multiple arguments into a single parameter using the splat operator *. The method receives an array containing all the extra arguments, which you can then iterate, reduce, or pass to another method:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def sum(*numbers)
numbers.inject(0, :+)
end
sum(1, 2, 3)
# => 6
sum(1, 2, 3, 4, 5)
# => 15
sum
# => 0
For keyword arguments, the double splat ** collects them into a hash inside the method body. This lets you accept arbitrary keyword arguments without declaring each one in the parameter list:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
def configure(**options)
options.each { |key, value| puts "#{key}: #{value}" }
end
configure(host: "localhost", port: 3000)
# => host: localhost
# => port: 3000
Ruby 3 introduced a trailing comma feature that lets you add new parameters without touching existing call sites. The trailing comma in the parameter list is optional but makes diffs cleaner when you add or remove parameters:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
def greet(name:, age:,)
"#{name} is #{age}"
end
Returning Values
Every Ruby method returns a value; the result of the last expression evaluated in the method body. You do not need an explicit return statement in most cases:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def add(a, b)
a + b
end
add(2, 3)
# => 5
The explicit return keyword is useful when you want to exit a method early, such as when you find a match and do not need to examine the remaining elements. Without return, the method would continue iterating:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def find_first_even(numbers)
numbers.each do |n|
return n if n.even?
end
nil
end
find_first_even([1, 3, 5, 6, 7])
# => 6
If you use return without a value, the method returns nil.
One thing to watch: setter methods (methods ending in =) always return the argument that was passed, not the assigned value. This is a Ruby design choice that ensures assignment expressions work consistently:
def val=(x)
@x = x
end
result = (self.val = 42)
result
# => 42 — not the value of @x
predicate methods
Methods whose names end with ? are called predicates. They are a naming convention signaling that the method returns a boolean value. The community expects predicates to return only true or false:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def empty?(string)
string.length == 0
end
empty?("")
# => true
empty?("hi")
# => false
Ruby has many built-in predicates:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
[1, 2, 3].empty?
# => false
"hello".include?("lo")
# => true
Hash.new.respond_to?(:keys)
# => true
The ? suffix is a convention only. Ruby does not enforce that predicates return true or false, but idiomatic Ruby predicates always do. If a predicate can return nil or some other truthy/falsy value, callers may get unexpected behavior.
Bang Methods
Methods ending with ! are called bang methods. They signal a “dangerous” or “mutating” variant; typically one that changes the receiver in place rather than returning a new object:
original = "hello"
loud = original.upcase!
original
# => "HELLO" — original was mutated
loud
# => "HELLO" — returns the mutated string
Compare with the non-bang version. The original string stays unchanged, and upcase returns a new string with the transformation applied. This distinction lets you choose between mutating and non-mutating behavior at each call site:
Every Ruby method returns a value, whether you use the return keyword or rely on implicit return of the last expression. The first code block demonstrates one return pattern, and the second shows a variation. Knowing when to use explicit returns for early exits versus relying on implicit returns for clean endings keeps your methods readable and predictable.
original = "hello"
loud = original.upcase
original
# => "hello" — unchanged
loud
# => "HELLO" — new string
The ! is a naming convention only. Ruby does not treat bang methods specially; you can define a bang method that does anything. The convention exists so developers can tell at a glance which version of a method mutates state and which one does not.
If a method has only a bang variant (like exit!), the bang typically indicates it terminates the process without running finalizers, as opposed to exit which does run them.
Do not rely on the bang to communicate danger; name your methods so the intent is clear from the name itself.
Setter Methods
Setter methods allow assignment syntax. You define one with def name=(value):
class Person
def name=(value)
@name = value
end
end
person = Person.new
person.name = "Alice"
Ruby provides attr_writer as a shortcut for single-attribute writers. It generates the same name= method without you writing the body explicitly:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
class Person
attr_writer :name
end
person = Person.new
person.name = "Alice"
The = in a setter method name requires a space between def and the name:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def val=(x) # correct
@x = x
end
def val = x # wrong — Ruby parses this differently
@x = x
end
Method Visibility
Ruby gives you control over where methods can be called from. There are three visibility levels:
Public
Public methods are callable from anywhere. This is the default in Ruby; if you do not specify a visibility modifier, your method is public:
class Calculator
def add(a, b)
a + b
end
end
Calculator.new.add(1, 2)
# => 3
private
Private methods cannot be called with an explicit receiver. They are only callable on self, which in instance method context means the current instance. This keeps internal implementation details hidden from outside callers:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
class Greeter
def greet
"#{format_message}!"
end
private
def format_message
"Hello"
end
end
g = Greeter.new
g.greet
# => "Hello!"
g.format_message
# => NoMethodError (private method called with explicit receiver)
Inside the instance, you call format_message without any receiver; Ruby implicitly calls it on self.
Protected
Protected methods are callable by self and any instance of the same class or its subclasses. This is useful when a method needs to compare two instances without being publicly accessible:
class Employee
attr_reader :employee_id
def initialize(id)
@employee_id = id
end
protected
def same_company?(other)
self.employee_id == other.employee_id
end
end
class Manager < Employee
def compare(e1, e2)
e1.same_company?(e2) # calling protected method on another instance
end
end
m = Manager.new(1)
alice = Employee.new(1)
bob = Employee.new(2)
m.compare(alice, bob)
# => false
changing visibility
Visibility applies to all methods defined after the modifier, until another modifier changes it. Calling private without arguments toggles visibility for all subsequent instance method definitions:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
class Example
def public_method; end
private
def private_method_a; end
def private_method_b; end
public
def another_public_method; end
end
For class methods, use public_class_method and private_class_method.
self, the current object
self refers to the current object; the receiver of the method call currently executing:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
class Dog
attr_accessor :name
def bark
puts "#{name} says woof!"
end
def introduce
puts "This is #{self.name}."
end
end
d = Dog.new
d.name = "Rex"
d.bark
# => Rex says woof!
d.introduce
# => This is Rex.
Inside instance methods, self is the instance. Inside class methods, self is the class itself, which is why you define class methods with def self.method_name:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
class Counter
@@count = 0
def self.increment
@@count += 1
end
def self.current
@@count
end
end
Counter.increment
Counter.current
# => 1
When you call a method without an explicit receiver inside an instance method, Ruby implicitly calls it on self. This is why name and self.name are equivalent inside an instance method when name is an accessor.
Keyword Arguments
Keyword arguments bind by name rather than position, which makes callsites easier to read:
def connect(host:, port: 80, ssl: false)
"Connecting to #{host}:#{port} (ssl=#{ssl})"
end
connect(host: "example.com")
# => "Connecting to example.com:80 (ssl=false)"
connect(host: "example.com", port: 443, ssl: true)
# => "Connecting to example.com:443 (ssl=true)"
Ruby 2.1 introduced required keyword arguments using the trailing colon syntax with no default. The caller must provide a value for name and email, while role defaults to “guest” when omitted:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def create_user(name:, email:, role: "guest")
{ name: name, email: email, role: role }
end
create_user(name: "Alice", email: "alice@example.com")
# => { name: "Alice", email: "alice@example.com", role: "guest" }
Ruby 3 made a breaking change to keyword argument handling. In Ruby 2, a positional hash would implicitly fill keyword parameters. In Ruby 3, positional and keyword arguments are strictly separated, so a hash passed as a positional argument no longer auto-converts to keywords:
def greet(name:, greeting: "Hello")
"#{greeting}, #{name}!"
end
# Ruby 2: greet({ name: "Alice" }) worked
# Ruby 3: this raises ArgumentError
# You must call it with:
greet(name: "Alice")
# or explicitly unpack:
greet(**{ name: "Alice" })
The double splat ** in a method definition collects keyword arguments into a hash. The double splat at a call site unpacks a hash into keyword arguments. These are two sides of the same mechanism.
Summary
Methods are where Ruby programs live. You now know how to define them with def, accept input through parameters and keyword arguments, return values explicitly or implicitly, and control visibility with public, private, and protected. You have seen the conventions around predicate methods (?), bang methods (!), and setter methods (=), and you understand how self refers to the current object.
The conventions Ruby uses for method naming are not enforced by the language; they are a shared vocabulary the community settled on. Following them makes your code immediately familiar to other Ruby developers.
See Also
- Ruby Blocks and Iterators; blocks are the natural companion to methods, letting you pass reusable chunks of code around
- Ruby Classes and Objects; methods live inside classes, and understanding how objects work deepens your appreciation of method dispatch