Metaprogramming Intro
Reviewed & published by Brayan K
Ruby is a dynamic, beginner-friendly programming language whose open, flexible object model makes metaprogramming — code that writes and inspects code at runtime — both natural and powerful.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll call methods dynamically with send, generate methods with define_method, and intercept unknown calls with method_missing.
What You'll Learn in This Lesson
- Calling methods by name with send
- Introspection: respond_to? and instance_methods
- Generating methods with define_method
- Closing over variables in generated methods
- Intercepting calls with method_missing
- When metaprogramming is (and isn't) a good idea
1️⃣ Dynamic Calls and Introspection
send invokes a method named by a symbol or string, so you can decide which method to call at runtime. Pair it with introspection — respond_to?, instance_methods, ancestors — to ask objects about themselves.
# send calls a method BY NAME (a string or symbol) at runtime.
class Calculator
def add(a, b); a + b; end
def multiply(a, b); a * b; end
end
calc = Calculator.new
puts calc.send(:add, 2, 3) # -> 5 (same as calc.add(2, 3))
puts calc.send("multiply", 4, 5) # -> 20
# Choose the method dynamically from data:
op = "add"
puts calc.send(op, 10, 7) # -> 17
# Introspection: ask an object about itself.
puts calc.respond_to?(:add) # -> true
puts calc.respond_to?(:divide) # -> false
puts Calculator.instance_methods(false).sort.inspect # -> [:add, :multiply] (sorted: the raw order varies by Ruby version)
puts 42.class.ancestors.inspect # -> [Integer, Numeric, Comparable, ...]2️⃣ Generating and Intercepting Methods
define_method creates methods at runtime, so you can generate a family of similar methods from a list — no copy-paste. method_missing catches calls to methods that don't exist, letting an object respond to names it was never explicitly given.
# define_method CREATES methods at runtime — no repetitive code.
class Robot
# Generate three similar methods in a loop:
[:walk, :talk, :dance].each do |action|
define_method(action) do
"Robot is #{action}ing"
end
end
end
r = Robot.new
puts r.walk # -> Robot is walking
puts r.dance # -> Robot is danceing (generated, not hand-written!)
# method_missing intercepts calls to methods that don't exist:
class Ghost
def method_missing(name, *args)
"You called '#{name}' with #{args.inspect}, but I don't have it!"
end
def respond_to_missing?(name, include_private = false)
true
end
end
g = Ghost.new
puts g.boo("spooky") # -> You called 'boo' with ["spooky"], but I don't have it!Your turn. Generate getter methods from the keys of a settings hash with define_method. Confirm the TODO, then run it.
# 🎯 YOUR TURN — replace each TODO, then run with: ruby attrs.rb
# Generate getter methods from a list of attribute names.
class Config
SETTINGS = { host: "localhost", port: 8080, debug: true }
def initialize
@data = SETTINGS.dup
end
# 1) For each key, define a method that returns its value.
SETTINGS.each_key do |key|
define_method(key) do # 👉 TODO: define_method with the key
@data[key]
end
end
end
c = Config.new
puts c.host # -> localhost (method generated from the hash key!)
puts c.port # -> 8080
puts c.debug # -> true
# ✅ Expected output:
# localhost
# 8080
# trueMini-Challenge: A Dynamic Dispatcher
Route a list of command strings to methods using send — the core of how CLIs dispatch input. Run with ruby dispatch.rb.
# 🎯 MINI-CHALLENGE: a dynamic dispatcher.
# Save as dispatch.rb and run with: ruby dispatch.rb
#
# 1. Make a class Greeter with methods: hello, goodbye (each returns a string).
# 2. Read a command name from an array: ["hello", "goodbye", "hello"].
# 3. For each command, use send to call that method and print the result.
# (This is how command-line dispatchers route input to methods.)
#
# ✅ Example output:
# Hello!
# Goodbye!
# Hello!
# your code hereCommon Errors (and the fix)
- send on untrusted input — It can call private methods. Use public_send and validate the name first.
- "undefined method" despite method_missing — Define respond_to_missing? too, so the object reports honestly.
- define_method with def syntax — Pass a block, not def: define_method(:x) { ... }.
- method_missing swallowing typos — A broad method_missing hides mistakes. Be specific or call super for unknown names.
- Overusing metaprogramming — If a plain method works, prefer it. Clever code is harder to debug.
Pro Tips
- 💡 You've already used it: attr_accessor is metaprogramming — it defines methods for you.
- 💡 Prefer public_send: it respects method visibility, unlike send.
- 💡 Inspect at runtime: obj.methods and obj.class.ancestors are great debugging tools.
📋 Quick Reference — Metaprogramming
| Concept | Ruby Syntax |
|---|---|
| Call by name | obj.send(:add, 1, 2) |
| Safe call by name | obj.public_send(:m) |
| Has method? | obj.respond_to?(:m) |
| Generate method | define_method(:m) { } |
| Intercept unknown | def method_missing(n,*a) |
| List methods | Klass.instance_methods |
🎉 Lesson Complete!
- ✅ Metaprogramming is code that writes/inspects code at runtime
- ✅ send calls a method chosen by name (prefer public_send)
- ✅ Introspection asks objects what they can do
- ✅ define_method generates methods from data
- ✅ method_missing intercepts unknown calls — use sparingly
Practice quiz
What does metaprogramming mean?
- writing tests
- compiling code
- writing code that writes or inspects code at runtime
- obfuscating code
Answer: writing code that writes or inspects code at runtime. Metaprogramming generates or examines code while the program runs.
What does calc.send(:add, 2, 3) do?
- calls the add method with 2 and 3
- defines add
- raises an error
- returns the symbol :add
Answer: calls the add method with 2 and 3. send invokes a method named by a symbol or string, the same as calc.add(2, 3).
send can accept the method name as:
- only a symbol
- only a string
- only a method object
- a symbol or a string
Answer: a symbol or a string. send takes the name as either a symbol or a string.
Which is safer because it respects method visibility?
- send
- public_send
- __send__
- call
Answer: public_send. public_send will not invoke private methods, unlike send.
What does respond_to?(:add) return when the method exists?
- true
- nil
- false
- the method object
Answer: true. respond_to? returns true when the object has that method.
What does define_method do?
- deletes a method
- calls a method
- creates a method at runtime
- renames a class
Answer: creates a method at runtime. define_method creates methods programmatically, often in a loop.
An advantage of define_method over def is that its body:
- runs faster always
- can close over local variables in scope
- needs no name
- is always private
Answer: can close over local variables in scope. A define_method block closes over surrounding locals; a plain def cannot.
What does method_missing intercept?
- syntax errors
- all method calls
- private methods only
- calls to methods the object does not have
Answer: calls to methods the object does not have. Ruby calls method_missing when you invoke an undefined method.
When overriding method_missing, you should also define:
- initialize
- respond_to_missing?
- to_s
- method_added
Answer: respond_to_missing?. respond_to_missing? keeps respond_to? honest about dynamic methods.
Which familiar feature is itself an example of metaprogramming?
- puts
- require
- attr_accessor
- if/else
Answer: attr_accessor. attr_accessor defines getter/setter methods for you at runtime.
Continue this course
- Previous: File I/O
- Next: Gems & Bundler — Install libraries with gem and manage dependencies with Bundler
- Quick reference: Ruby cheat sheet
Frequently asked questions
What is metaprogramming, in plain terms?
Metaprogramming is writing code that writes or inspects code while the program runs. Instead of hand-typing every method, you can generate methods from data, call methods chosen at runtime by name, or ask an object what it can do. Ruby is famously friendly to this because classes stay open and methods are just messages you send to objects. The payoff is less repetition and very flexible libraries; the cost is code that can be harder to follow, so it's used deliberately.
What does send do, and is it safe?
send calls a method by its name given as a symbol or string, with any arguments: obj.send(:add, 2, 3) is the same as obj.add(2, 3). It's perfect when the method to call is decided at runtime — for example routing a command string to a handler. The safety caveat: send can call private methods and any method named in a variable, so never pass unsanitised user input straight to send. When you only want public methods, use public_send, which respects visibility.
When would I use define_method instead of def?
Use define_method when you want to generate several similar methods programmatically rather than copy-pasting them. Looping over [:walk, :talk, :dance] and calling define_method for each creates all three with one piece of logic. It also lets the method body close over local variables from the surrounding scope, which a plain def cannot. This is exactly how features like attr_accessor and many DSLs work under the hood — they define methods for you based on the names you provide.
What is method_missing and should I use it?
method_missing is a hook Ruby calls when you invoke a method an object doesn't have. By overriding it, you can respond to a whole family of method names dynamically — for instance a proxy that forwards unknown calls elsewhere, or a builder that turns find_by_name into a query. It's powerful but should be used sparingly: it can mask typos as silent behaviour and slow down method lookup. If you do use it, also define respond_to_missing? so the object honestly reports what it can handle.