rubyguides

Ruby keyword: def

def name([params]) [body] end

def is the keyword that puts a method into existence. It is reserved by the parser, so you cannot use it as a variable name. Once a method is defined, the keyword expression itself returns a Method object, which is occasionally useful for reflection or for assigning the method to a delegate.

method_object = def square(x)
  x * x
end

square(4)         # => 16
method_object     # => #<Method: Object#square>

That Method return is the reason def is technically an expression, not a statement, even though almost everyone treats it as a declaration. For dynamic method creation at runtime — when the method name comes from a string, a config file, or a database column — Ruby offers define_method and method_missing instead; def itself is a static, parse-time construct that the parser has to see literally.

Syntax

def has five syntactic shapes, and the rule about where each one attaches a method is the part most beginners get wrong.

FormWhat it defines
def name (at top level)An instance method on Object, callable as a free function in the script
def name inside a class bodyAn instance method on that class, inherited by subclasses
def self.name inside a class bodyA class method on that class’s singleton class
def receiver.nameA singleton method on the one receiver object
def +@ / def -@ / def +(other) / def [](...)Operator methods

The shorthand def name = expr form (Ruby 3.0+) is a sixth style worth knowing on its own. Picking the right form up front matters: a misplaced def is one of the most common sources of “why is this method on the wrong class?” bugs.

Shorthand and endless methods

A method body can be a single expression placed after =, with no end:

def square(x) = x * x
def greet(name) = "Hello, #{name}!"

square(5)       # => 25
greet("Ruby")   # => "Hello, Ruby!"

The shorthand pays off for tiny one-liners — accessors, predicates, and conversion routines.

Three rules that bite people:

  • The parameter parens are mandatory. def square x = x * x is a SyntaxError.
  • The body has to be a single expression. You cannot write def f(x) = (a; b) or stack a rescue.
  • The shorthand form is rejected for assignment methods (def foo=(x) = ...) and for singleton methods on arbitrary receivers (def obj.foo = ...). It is allowed for def self.foo = ... because self is resolved at call time.

Method names

A method name begins with a letter or any character with the 8th bit set (so UTF-8 source files let you name methods café or 日本語). After that you can have letters, digits, underscores, and more 8th-bit characters. Trailing !, ?, and = are allowed.

Three naming conventions are community convention, not parser rules:

SuffixConventionExample
!Mutates the receiver in place; the non-bang version returns a new valueString#gsub / String#gsub!
?Returns a boolean or truthy predicateArray#include?
=Assignment method; the only allowed arity is one argumentattr= writers, []=

The ! and ? suffixes are not enforced by the parser — def update! could be a totally pure routine and def valid? could return a User object instead of true or false. The convention is what gives them meaning; the library ecosystem and tools like RuboCop lean on it.

You can also name methods after operators. Each binary operator takes exactly one argument. Unary +, -, !, and ~ use a trailing @ to disambiguate them from their binary siblings:

class Temperature
  def initialize(c) = @c = c
  def -@             = Temperature.new(-@c)
  def +(other)       = Temperature.new(@c + other.@c)
  def to_s           = "#{@c}°C"
end

t = Temperature.new(20)
-t                              # => #<Temperature:0x... @c=-20>
(t + Temperature.new(5)).to_s   # => "25°C"

def [](...) and def []=(...) give you element reference and element assignment, which is how arr[0] and arr[0] = 1 are wired up.

Parameters

A method signature can mix positional, default, splat, keyword, double-splat, and block parameters. The ordering is fixed: required positional, then optional positional with defaults, then *splat, then required keyword, then optional keyword, then **double_splat, then &block. The Ruby parser rejects any other order with a SyntaxError, and the error message is often terse enough that beginners have to count commas to find the offender.

Positional and default values

def add(a, b) = a + b

add(1, 2)        # => 3
add(1, 2, 3)     # raises ArgumentError: wrong number of arguments (given 3, expected 2)

Defaults must be grouped: every parameter with a default has to come after every parameter without one. def fn(a = 1, b, c = 1) is a SyntaxError.

def fn(a, b = 1, c = 2) = [a, b, c]

fn(10)        # => [10, 1, 2]
fn(10, 20)    # => [10, 20, 2]

Defaults are evaluated on every call, and a default can reference parameters to its left but not to its right. The right-reference case fails at call time, not parse time, because the local name simply does not exist yet. This per-call evaluation is the source of one of the gotchas further down — stateful defaults that quietly accumulate values across invocations and surprise the next person who reads the code.

Array decomposition and splat

Wrap a parameter in parens to pull it apart:

def pair((a, b)) = [a, b]

pair([1, 2])      # => [1, 2]
pair([1, 2, 3])   # => [1, 2]

A single * in the signature collects the rest into an array. A bare * with no name accepts anything and throws it away:

def middle(first, *mid, last) = mid
def ignore(*)                 = nil

middle(1, 2, 3, 4)   # => [2, 3]
ignore(1, "x", :a)   # => nil

The bare * is a Ruby 3.0+ feature useful for declaring “I take any arguments but do not need them” — common in overrides where the superclass signature is wider than the local implementation cares about, and in stubs that satisfy an interface without doing any work.

Keyword arguments

A trailing colon on a parameter name marks a required keyword argument — without a default expression, the caller must pass it. Add a default and the keyword becomes optional, falling back to the default value when the caller omits it. The required form is the cleanest way to flag a method as needing a particular named value at the call site, because missing the keyword raises ArgumentError immediately rather than silently defaulting to nil:

def add(first:, second:) = first + second

add(first: 1, second: 2)   # => 3
add                         # raises ArgumentError: missing keywords: :first, :second

A double splat collects the rest into a hash. Without any **, a trailing hash at the call site gets absorbed as a positional Hash argument — that is the legacy behaviour from before keyword arguments were a separate concept, and it catches anyone migrating code from Ruby 2.6 or earlier. The **nil marker is the explicit, modern way to say “I refuse any keyword arguments,” and it is the cleanest fix for the Ruby 2.7 → 3.0 warning storm about implicit hash-to-keyword conversion. Pin the marker on a method when the body never reads keyword arguments and you want the call site to fail loudly if a caller slips one in:

def opts(first: 1, **rest) = [first, rest]
def no_kw(**nil)           = :ok

opts(first: 10, x: 1, y: 2)   # => [10, {x: 1, y: 2}]
no_kw(a: 1)                   # raises ArgumentError: no_kw does not accept keyword arguments

Without **nil, a method that does not declare any keyword parameters and is called with a trailing hash will treat the hash as a positional Hash argument, which is rarely what you want.

Block argument and forwarding

A leading & captures the block as a Proc. A bare & (Ruby 3.1+) does the same thing anonymously and is convenient when you just want to forward the block without naming it:

def each_item(&) = @items.each(&)

For a wrapper method that needs to pass every argument and the block straight through — common in delegation patterns, decorators, and logging shims — use the ... forwarding syntax introduced in Ruby 2.7. The wrapper’s signature becomes def wrapper(...) and the body uses target(...) to forward. Both sides must use the literal three dots; a target declared as target(*args, **kwargs, &block) will not match a wrapper that has ... in its own signature, and the resulting ArgumentError is one of the more confusing error messages a newcomer can hit:

def target(*args, **kwargs, &block) = [args, kwargs, block&.call]
def wrapper(...)                    = target(...)

wrapper(1, b: 2) { 3 }   # => [[1], {b: 2}, 3]

Ruby 3.0 lets you put leading positional arguments before the ... so the wrapper can pluck off a method name or a path and forward the rest:

def call_via(method, path, ...) = send(method, path, ...)

Return value

A method returns the value of the last expression in its body. return expr exits early with expr. Endless methods return the single expression they wrap. The implicit-return style is the Ruby norm; explicit return is usually reserved for early-exit guard clauses.

The assignment-method case is the surprise. For a method named foo=, calling it through assignment syntax returns the assigned value, not whatever the method’s body evaluated to:

class Box
  def label=(v)
    @label = v
    return 99
  end
end

b = Box.new
b.label = "fragile"      # => "fragile"
b.send(:label=, "ok")    # => 99

This is why return-based guard logic in attr= writers is silently ignored on obj.attr = v syntax. Branch on the input instead. The same rule means a tap-style side effect in an assignment method will not surface in the assignment expression’s value, even when the method body clearly evaluated to something else. To grab the real return value when you need it, call the writer through send instead.

Scope

The five forms in the syntax table resolve to different scopes. The important distinction is that def self.foo and def String.foo both attach a method to a class’s singleton class, but only one object gets a method defined with def obj.foo syntax.

class Counter
  def increment; end      # instance method on Counter
  def self.zero; end      # class method on Counter
end

counter = "abc"
def counter.shout         # singleton method on this one String
  upcase + "!"
end

counter.shout    # => "ABC!"
"xyz".shout      # raises NoMethodError

Top-level def lands on Object (technically a private method on the main object, but callable as a free function in the script). The pollution is real, so scripts that use def at the top level are silently adding methods to every object in the process. Wrap the top-level body in a Module or use module_function if you want helpers to stay out of Object.

Gotchas

A handful of behaviours show up often in bug reports. The shortest path to each one is a small reproduction, since the parser or runtime will not warn you at definition time.

The most visible landmine is silent redefinition. Defining a method that already exists just overwrites the previous definition. Monkey-patching relies on this; accidental shadowing gets caught by it. Tools like RuboCop can warn on shadowed Object methods, and a quick String.instance_methods(false).grep(/to_i/) check confirms a redefinition before you ship it.

class String
  def to_i = 42   # silently overrides String#to_i
end

"43".to_i   # => 42

Default values are re-evaluated per call. State set inside a default expression leaks across calls, which is occasionally useful and usually a footgun. The shared accumulator below looks innocent, but each call re-runs the default and the same list survives across invocations, so the second caller sees the first caller’s data:

def append(value, list = ($shared ||= []))
  list << value
end

append(:a)   # => [:a]
append(:b)   # => [:a, :b]

The fix is almost always to construct the container inside the method body rather than in the default, so each call gets a fresh object.

A few more worth pinning in muscle memory:

  • **nil versus no **. Methods with no ** and no keyword params convert a trailing hash into a positional Hash (legacy behaviour). **nil makes the call site explicit about its intent.
  • Assignment-method returns are dropped under obj.x = v syntax. Use send if you actually need the method’s return.
  • Shorthand def name = expr has a single expression and requires parens. It is also rejected for assignment methods and for def obj.foo = ....
  • Top-level def pollutes Object. Wrap script-level logic in a module or a main object if you want to avoid it.
  • Singleton methods are one-shot. They belong to one object only, not the class.

External documentation

  • Methods syntax — the canonical Ruby 3.3 reference for def, parameters, scope, and argument forwarding
  • Keywords index — confirms def is a reserved keyword and links to the methods syntax page

See also

  • method_missing — the fallback hook for calls that have no def to match
  • respond_to? — duck-typing check that pairs with whatever def defined
  • Kernel#method — retrieve the Method object that a def produced