Type Conversion
Type conversion is the process of turning a value of one class into a value of another class — for example, turning the string "42" typed into a form field into the integer 42 you can actually do math with. Ruby is dynamically typed (types are checked while the program runs, not ahead of time) but it is also strongly typed: it will never silently glue a String and an Integer together the way some languages do. Because of that, Ruby gives you a whole toolbox of conversion methods, and each one has a subtly different personality — some fail loudly, some fail quietly, and some only work if the target object cooperates. Understanding which is which is one of the most practical skills you can pick up early in Ruby.
Overview: How Type Conversion Works in Ruby
Every value in Ruby, without exception, is an object. 1.class is Integer, "hi".class is String, and even nil.class is NilClass. There is no primitive/object split like in some languages, and there is no language-level cast operator like (int) in C. Instead, converting a value from one type to another always means calling a method on it. That single fact explains almost everything about how conversion works in Ruby: if an object doesn’t define the method you want, the conversion simply isn’t possible for that object.
Ruby’s built-in conversion methods fall into two families that behave very differently.
Explicit, forgiving conversions
Methods like to_s, to_i, to_f, to_a, and to_h are defined broadly, almost always succeed, and return a sensible best-effort default rather than raising an error. to_s is defined on Object itself and inherited by everything, which is why you can drop virtually any object into string interpolation without it blowing up. These forgiving methods can quietly lose information: "3.9".to_i returns 3 (it truncates, it does not round), and "banana".to_i returns 0 rather than complaining that "banana" isn’t a number at all. nil plays along too: nil.to_s is "", nil.to_a is [], and nil.to_i is 0 — convenient defaults, but also a common source of silently-wrong data, as you’ll see in Common Mistakes below.
Strict, capitalized Kernel conversions
Ruby also provides Integer(), Float(), String(), Array(), and Hash() — private methods on Kernel, called without a receiver, that look like class names but are actually method calls. Unlike their lowercase cousins, these are strict: Integer("banana") raises ArgumentError instead of guessing. This makes them the right tool whenever you’re converting data you don’t fully trust, such as user input, environment variables, or file contents, because a malformed value surfaces as an immediate, obvious error instead of a wrong number that silently propagates through your program.
The implicit conversion protocol
There is a third, quieter layer: methods named to_str, to_int, to_ary, and to_hash. These are Ruby’s duck-typing signal that says "I’m not officially a String/Integer/Array/Hash, but you can treat me as one." Core methods implemented in C, such as String#+ or File.join, look for these methods when they receive an argument that isn’t literally the expected class, and call them automatically. This is exactly why "count: " + 5 raises a TypeError — Integer defines to_i, but not to_str, so it doesn’t qualify as string-like for implicit concatenation. A custom class can opt in to this behavior by defining to_str itself, as the example below demonstrates.
class Money
def initialize(amount)
@amount = amount
end
def to_str
format("$%.2f", @amount)
end
end
receipt = "Total: " + Money.new(19.5)
puts receipt
Output:
Total: $19.50
String#+ doesn’t know anything about Money, but because Money defines to_str, Ruby implicitly converts it to a string before concatenating. This is the same mechanism Array#+ uses with to_ary and Hash#merge uses with to_hash.
Syntax
The general shape is always value.conversion_method for the explicit family, or ConversionMethod(value) for the strict Kernel family. Here is a reference of the most common conversions.
| Method | Converts to | On invalid input |
|---|---|---|
to_s |
String | Never fails — every object has a default |
to_i |
Integer | Returns 0, truncates decimals |
to_f |
Float | Returns 0.0 |
to_a |
Array | Depends on receiver; nil.to_a is [] |
to_h |
Hash | Array must contain [key, value] pairs or raise |
to_sym |
Symbol | Never fails on a String |
Integer(value) |
Integer | Raises ArgumentError/TypeError |
Float(value) |
Float | Raises ArgumentError |
Array(value) |
Array | Never fails — wraps or unwraps as needed |
Examples
Example 1: Basic conversions between numbers and strings
number = 42
text = "3.14"
number_as_string = number.to_s
string_as_float = text.to_f
string_as_int = text.to_i
puts number_as_string
puts number_as_string.class
puts string_as_float
puts string_as_int
puts number + string_as_int
Output:
42
String
3.14
3
45
number.to_s turns the integer into the string "42". text.to_f parses the whole decimal string into 3.14, while text.to_i only reads the leading digits and stops at the dot, giving 3. Because string_as_int is now a real Integer, adding it to number works normally and prints 45.
Example 2: Strict Integer() versus lenient to_i
def safe_to_integer(value)
Integer(value)
rescue ArgumentError, TypeError
nil
end
puts "42".to_i
puts "42abc".to_i
puts safe_to_integer("42").inspect
puts safe_to_integer("42abc").inspect
puts safe_to_integer(" 17 ").inspect
Output:
42
42
42
nil
17
Both "42".to_i and "42abc".to_i return 42 — to_i just reads leading digits and gives up quietly at the first non-digit character. Integer() behaves very differently: it accepts surrounding whitespace (so " 17 " becomes 17) but raises ArgumentError the moment there’s trailing garbage like "abc", which safe_to_integer catches and turns into nil.
Example 3: Converting between Arrays and Hashes
pairs = [[:name, "Ada"], [:language, "Ruby"]]
config_hash = pairs.to_h
puts config_hash.inspect
puts config_hash[:name]
single_value = "solo"
wrapped = Array(single_value)
nil_wrapped = Array(nil)
already_array = Array([1, 2, 3])
puts wrapped.inspect
puts nil_wrapped.inspect
puts already_array.inspect
Output:
{:name=>"Ada", :language=>"Ruby"}
Ada
["solo"]
[]
[1, 2, 3]
to_h turns an array of two-element [key, value] pairs into a Hash. Array() is the safe, universal way to make sure you have an array to work with: it wraps a lone value in a one-element array, turns nil into an empty array instead of raising, and leaves an already-existing array untouched. This is a common trick when a method might receive either a single item or a list of items.
How It Works Step by Step
Consider what actually happens when Ruby evaluates Integer("42abc") versus "42abc".to_i:
"42abc".to_iis a plain method call on a String instance. Internally it scans the string character by character from the start, collecting digits, and stops as soon as it hits a non-digit character. Whatever digits it collected become the result —42. It never raises.Integer("42abc")calls the privateKernel#Integermethod. It attempts to parse the entire string as a valid integer literal. Because"abc"is left over after the digits, the whole conversion is rejected and Ruby raisesArgumentError: invalid value for Integer(): "42abc".- When that error is raised inside a method with a matching
rescueclause (as in Example 2’ssafe_to_integer), control jumps straight to therescuebody, skipping the rest of the method, and the rescued value becomes the method’s return value.
The same "scan and stop" versus "parse the whole thing or fail" contrast applies to to_f/Float() and to most other lenient/strict method pairs in Ruby.
Common Mistakes
Mistake 1: Using + to build a string with a non-string value
count = 5
puts "You have " + count + " items"
This raises TypeError: no implicit conversion of Integer into String and prints nothing at all, because Integer defines to_i (explicit) but not to_str (implicit), so String#+ refuses it. The fix is to use string interpolation, which calls to_s on whatever you hand it, or to convert explicitly first.
count = 5
puts "You have #{count} items"
Output:
You have 5 items
Mistake 2: Letting nil.to_s hide a missing value
def greeting(name)
"Hello, " + name.to_s + "!"
end
puts greeting(nil)
Output:
Hello, !
Because nil.to_s quietly returns "" instead of raising, the caller never finds out that name was missing — they just get a slightly broken greeting instead of an error pointing at the real bug. When a value being nil is actually invalid for your logic, check for it explicitly instead of letting it convert away silently.
def greeting(name)
raise ArgumentError, "name is required" if name.nil?
"Hello, #{name}!"
end
puts greeting("Ada")
Output:
Hello, Ada!
Mistake 3: Trusting to_i to validate input
user_input = "twenty"
count = user_input.to_i
puts "Ordering #{count} widgets"
Output:
Ordering 0 widgets
Nothing here crashes, which is exactly the problem: "twenty".to_i silently becomes 0 instead of signaling that the input wasn’t a valid number, and the program cheerfully places an order for zero widgets. Whenever you’re parsing input that might be wrong, use Integer() so bad data raises instead of disappearing.
def parse_count(input)
Integer(input)
rescue ArgumentError
raise ArgumentError, "invalid quantity: #{input.inspect}"
end
puts parse_count("42")
Output:
42
Best Practices
- Use
Integer()andFloat()for data you don’t fully control — user input, ENV variables, file or API contents — so garbage raises an error instead of quietly becoming0. - Reach for
to_i/to_f/to_sonly on values you already trust are roughly the right shape, where the forgiving default behavior is actually convenient. - Prefer
"#{value}"interpolation over+concatenation when building strings from mixed types — interpolation callsto_sautomatically and never raisesTypeError. to_itruncates toward zero, it does not round. If you need rounding, convert withto_ffirst and callround.- Remember that
nil.to_sis""andnil.to_iis0; guard against unexpectednilexplicitly withnil?checks rather than letting it silently convert into a misleading value. - When writing your own classes, override
to_sfor readable output, and only addto_str/to_int/to_aryif you genuinely want your object to duck-type as a String/Integer/Array in other library code — it’s a strong commitment to a specific interface.
Practice Exercises
- Write a method
total_from_strings(prices)that takes an array like["19.99", "5", "3.50"]and returns the sum as aFloatusingto_f. What does it return if one element is"free"instead of a number? (Hint:to_fon non-numeric text returns0.0rather than raising.) - Write
parse_age(input)that usesInteger()with arescueto return the integer or raise a friendlierArgumentErrorthat includes the original text. Test it with"30"and with"thirty"and confirm the second one raises. - Starting from the hash
{status: "ok", code: 200}, callto_ato get an array of pairs, then callto_hon that array to turn it back into a hash. Print both and confirm the round trip produces an equal hash.
Summary
- Ruby has no cast operator — every conversion is a method call on an object, and only objects that define the method can be converted.
- Lowercase methods like
to_s,to_i,to_f,to_a, andto_hare forgiving: they almost never raise, but can quietly lose data or return a misleading default like0. - Capitalized Kernel methods like
Integer(),Float(), andArray()are strict (exceptArray(), which never fails); use them when validating untrusted input. - The implicit conversion protocol (
to_str,to_int,to_ary,to_hash) is how core methods duck-type arguments; only define these on your own classes when you truly want that interface. nil.to_sis"",nil.to_ais[], andnil.to_iis0— convenient, but a frequent source of silently wrong behavior if you don’t check fornilexplicitly.to_itruncates rather than rounds; convert withto_fand callroundwhen you need proper rounding.
