Conversions and Coercion

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them. Ruby never turns text into a number behind your back the way a weakly typed language does, and it never refuses to convert a value the way a strictly typed one does. It has four separate mechanisms instead, and knowing which one is at work explains almost every TypeError and every surprising 0 that a program produces: explicit conversion methods that you call (to_s, to_i), implicit conversion methods that core methods call on their arguments (to_str, to_ary), the strict Kernel functions with capitalised names (Integer(), Array()), and coerce, the protocol that lets 1 + fine work when fine is not a number. This page covers all four, the exact error messages they produce, how to implement them in your own classes, and the traps that come with method_missing.

The literal syntax for numbers, strings and collections is in Literals, and the classes themselves are in Numbers, Strings, Symbols, Arrays and Hashes. The splat and double splat at a call site, which use to_a and to_hash, are in Calling Methods and Argument Forwarding; the & that calls to_proc is in Blocks, Procs and Lambdas; multiple assignment, which uses to_ary, is in Assignment and Multiple Assignment. How ==, eql? and <=> relate to the types of their operands is in Equality, Hashing and Comparison, the exception classes that conversions raise are in Exceptions and Error Handling, and the dynamic dispatch that makes the method_missing trap possible is in Dynamic Dispatch and Method Definition.

The four mechanisms

Mechanism Names Who calls it Strictness

Explicit conversion method

to_s, to_i, to_f, to_a, to_h, to_sym, to_r, to_c

You, in your own code.

Lenient: "12abc".to_i is 12. Defined on many classes that are not the target type, such as nil and Struct.

Implicit conversion method

to_str, to_ary, to_hash, to_int, to_proc, to_io, to_path, to_regexp

The core library, on an argument that must be a String, Array, Hash, Integer, Proc, IO, path or Regexp.

Strict: the object must really stand in for the target type, or the call raises TypeError. Defined by very few classes.

Conversion function

Integer(), Float(), String(), Array(), Hash(), Rational(), Complex()

You, when input must be valid.

Strict for text, forgiving for objects; exception: false returns nil instead of raising (not on String(), Array() and Hash()).

Coercion

coerce

The numeric operators and comparisons, on the right operand.

Strict: the pair returned must be usable with the operator.

The difference between the first two is the idea to keep: to_s says "give me a representation of yourself", and nearly everything has one; to_str says that the object is a string without being an instance of String, and almost nothing does. The core library calls the implicit names on arguments, and the explicit names never, which is why "total: " + 5 is a TypeError while "total: #{5}" works: interpolation asks for a representation (to_s), and String#+ asks for a String (to_str).

The documented rules for the implicit family are the same for all four target classes. An object is convertible to a class when it has the conversion method, the method accepts no arguments, and it returns an instance of that class (kind_of? is true). If the method is missing the call raises TypeError (no implicit conversion of X into Y), if the method takes arguments it raises ArgumentError, and if the method returns the wrong class it raises TypeError with the message can't convert X to Y (X#to_y gives Z).

Explicit conversions

The to_x methods are ordinary methods named after the thing they return. They exist on whichever classes find them useful, they never raise for a value they understand, and most of them are deliberately forgiving about input they do not.

Method Where it is defined Behaviour

to_s

Object, so on everything

A String describing the object. Called by interpolation and puts. See to_s, inspect and interpolation.

to_i, to_f

String, Integer, Float, Rational, Complex, nil, Time

Parse as much leading text as is numeric and stop; 0 when none is. Float#to_i truncates toward zero.

to_r, to_c

the same, with String

Exact Rational (a Float converts with its binary error) and Complex. "abc".to_r is (0/1).

to_a

Array, Hash, Range, Struct, Time, nil, every Enumerable

A new Array of the elements. Hash#to_a gives key-value pairs, Time#to_a gives ten calendar fields.

to_h

Array of pairs, Hash, Struct, Data, nil, Enumerable

A Hash. Array#to_h takes a block that returns each pair and raises TypeError for an element that is not a pair.

to_sym

String, Symbol

The Symbol with that name. Any String is allowed: :"with space".

to_proc

Symbol, Method, Hash, Proc

A Proc, so that &object works. See to_proc.

to_set

Enumerable

A Set. See Sets.

chr, ord

Integer, String

Convert between a code point and a one-character String.

to_s(base), to_i(base)

Integer, String

Convert to and from text in a base from 2 to 36; an invalid base raises ArgumentError.

floor, ceil, round, truncate

Float, Rational

Convert to Integer with a stated rounding rule, which is usually what you meant by to_i.

# frozen_string_literal: true

# Explicit conversions are lenient: they read what they can and stop.
"12abc".to_i # => 12
"abc".to_i # => 0
"  42  ".to_i # => 42
"1_000".to_i # => 1000
"3.7kg".to_f # => 3.7
"0x1A".to_i # => 0
"0x1A".hex # => 26
"ff".to_i(16) # => 255
"9".to_i(2) # => 0
"0.1".to_r # => (1/10)
"abc".to_r # => (0/1)
"1+2i".to_c # => (1+2i)

# nil converts to the "empty" value of each type.
nil.to_s # => ""
nil.to_i # => 0
nil.to_a # => []
nil.to_h # => {}

# Numbers convert by rule, not by parsing.
3.7.to_i # => 3
-3.7.truncate # => -3
3.7.floor # => 3
3.2.ceil # => 4
1.5.to_r # => (3/2)
0.1.to_r # => (3602879701896397/36028797018963968)
0.1.rationalize # => (1/10)
255.to_s(16) # => "ff"
65.chr # => "A"
"A".ord # => 65

# Collections convert in both directions.
{ a: 1 }.to_a # => [[:a, 1]]
[[:a, 1]].to_h # => {a: 1}
[1, 2].to_h { [it, it * 2] } # => {1 => 2, 2 => 4}
[1, 2].to_h # raises TypeError: wrong element type Integer at 0 (expected array)
(1..3).to_a # => [1, 2, 3]
name = "title"
name.to_sym # => :title
phrase = "with space"
phrase.to_sym # => :"with space"

Four rules cover most of what goes wrong with these methods:

  • to_i and to_f never complain. "12abc".to_i is 12 and "abc".to_i is 0, so a typo in user input becomes a valid but wrong number. Use them when a best-effort value is the point (reading a leading number from "3.7kg"), and use Integer() and Float() when the input must be valid.

  • to_i ignores prefixes unless you give the base. "0x1A".to_i is 0 and "0x1A".hex is 26. Integer("0x1A") understands the prefix by itself.

  • A Float converts to a Rational exactly, including its error. 0.1.to_r is (3602879701896397/36028797018963968); 0.1.rationalize finds the simplest nearby Rational, (1/10), and Rational("0.1") is exact because it starts from text.

  • nil has a conversion method for most targets (to_s, to_i, to_f, to_a, to_h, to_r, to_c) that returns the empty or zero value, which is why nil.to_a is [] and why value.to_s is a common way to normalise optional input. nil.to_s returns a frozen empty String, and Ruby 4.0 changed the splat *nil so that it no longer calls nil.to_a (the result, an empty list of arguments, is the same).

Money and measurements are the usual reason to leave Float behind: 0.1 + 0.2 is 0.30000000000000004, while two Rationals add exactly. The bundled gem bigdecimal (a bundled gem since Ruby 3.4, so a Gemfile entry is needed under Bundler) offers decimal arithmetic with BigDecimal("0.1"); its to_f, to_r and to_i follow the same conventions.

to_s, inspect and interpolation

to_s is the conversion for people, and inspect is the conversion for programmers. String interpolation, puts, print and Kernel#String call to_s; p, irb and Array#inspect (which formats each element) call inspect. The two are independent: a class that overrides only to_s still prints the default #<Class:0x...> form from inspect, which lists the instance variables, and a class that overrides only inspect has the default to_s. Override to_s for the text a user would read (Shelf::Isbn returns its digits) and inspect for a debugging view; Data and Struct already generate a good inspect.

The result of to_s must be a String. When it is not, interpolation does not raise: it falls back to the default description, as the last lines of the listing in to_str show.

to_proc

&object in an argument list calls object.to_proc and passes the resulting Proc as the block. Symbol#to_proc (&:upcase), Method#to_proc (&method(:Integer)), Hash#to_proc (the Proc looks the argument up in the Hash) and Proc#to_proc (itself) are built in. Any class may define to_proc, which is how a predicate object or a query object becomes usable wherever a block is. It is covered with the other implicit conversions below, because & requires it in the same way that + requires to_str.

Implicit conversions

A method that needs an Array, a Hash, an Integer or a String is, almost always, written to accept anything that can be one, by calling the matching implicit method on its argument. The implicit methods are the library’s duck-typing hooks: defining one is a promise that the object can be used anywhere the target class can.

Method Target Called by (core examples)

to_str

String

String#+, <<, concat, include?, start_with?, split, File.join, File.open and other file names, Array#join (first choice, before to_s), Kernel#String, String.new, method-name arguments such as send, and, in the other direction, String#==.

to_ary

Array

Multiple assignment (a, b = obj), block and method parameters that destructure, Array#+, concat, zip, flatten, Array.try_convert, Kernel#Array (before to_a), puts (it prints each element), and String#% with an array argument.

to_hash

Hash

The double splat **obj, Hash#merge, update and replace, Hash.try_convert, Kernel#Hash, keyword arguments built from **.

to_int

Integer

Any index, count or size argument: Array#[], first, take, fetch, Array.new, String#*, String#[], Integer.sqrt, Kernel#Integer (first choice), Integer.try_convert. Implemented by Integer, Float (truncating), Rational and Complex with a zero imaginary part.

to_proc

Proc

The block-pass &obj.

to_io

IO

IO.select, IO.try_convert, and any method that takes an open stream.

to_path

path String

File.open, File.read, File.basename, File.directory?, Dir.exist? and every other method that takes a file name; Pathname implements it (and not to_str).

to_regexp

Regexp

Regexp.try_convert and Regexp.union.

Arrays hold one exception to the rule that to_ary means "is an Array": the splat *obj calls to_a, not to_ary, and treats an object without to_a as a one-element list. The two methods look alike and are used in different places, which is the cause of most Array conversion surprises (Pitfalls).

to_str

to_str is the strictest promise. Only String defines it in the core library; define it on your own class only when an instance can be substituted for a String everywhere, as a path, a pathname-like value or a string wrapper can. The next listing makes a Shelf::Slug that behaves that way, next to Shelf::Isbn, which only has to_s.

# frozen_string_literal: true

require "shelf"

module Shelf
  # A URL-friendly name that can stand in for a String.
  class Slug
    def initialize(text) = @text = text.downcase.tr(" ", "-")

    # Implicit: "I am a String", used when a method needs one.
    def to_str = @text

    # Explicit: how to display me.
    def to_s = "Slug(#{@text})"
  end
end

slug = Shelf::Slug.new("Moby Dick")
prefix = "title: "
p prefix + slug # => "title: moby-dick"
p "slug is #{slug}" # => "slug is Slug(moby-dick)"
String(slug) # => "moby-dick"
File.join("books", slug) # => "books/moby-dick"
["books", slug].join("/") # => "books/moby-dick"
"moby-dick-2".start_with?(slug) # => true
"moby-dick".include?(slug) # => true
{ "moby-dick" => 1 }[slug] # => nil
text = "moby-dick"
p text == slug # => false
text.eql?(slug) # => false

# Shelf::Isbn has to_s but deliberately no to_str.
isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
p "ISBN #{isbn}" # => "ISBN 9780306406157"
prefix = "ISBN "
p prefix + isbn # raises TypeError: no implicit conversion of Shelf::Isbn into String
p prefix + :symbol # raises TypeError: no implicit conversion of Symbol into String

# A to_s that returns a non-String is ignored, without an error.
broken = Object.new
def broken.to_s = 42
"value: #{broken}".sub(/0x\h+/, "0x...") # => "value: #<Object:0x...>"

What the listing shows:

  • to_str is used by methods that require a String. Interpolation and Kernel#String prefer the representation, and Array#join and String(slug) choose to_str first, so to_s and to_str can disagree and different methods can pick different answers. Keeping the two equal is the safest design when both exist.

  • A Hash lookup is not a conversion: { "moby-dick" => 1 }[slug] is nil because the lookup compares keys with eql? and hash, which do not convert. Neither does eql?.

  • String#== is the one place where the conversion goes in the other direction: when the argument is not a String but responds to to_str, the result is argument == string. With Slug not defining ==, the answer is false. Define == as well if instances must compare equal to Strings (see Equality, Hashing and Comparison).

  • Shelf::Isbn defines to_s, so "ISBN #{isbn}" works, and deliberately not to_str, so "ISBN " + isbn raises TypeError. That is the right design for a value object: it can be printed but is not a String.

  • A to_s that returns something other than a String is not an error in interpolation; Ruby silently prints #<Object:0x...> instead. This is a strong reason to keep to_s trivial.

to_ary and to_hash

to_ary and to_hash make an object destructure like an Array or splat like a Hash. They are rarely worth defining outside collection wrappers, and the listing shows how much of the language they reach.

# frozen_string_literal: true

module Shelf
  # Behaves like an Array of titles wherever Ruby asks for one.
  class ReadingList
    def initialize(*titles) = @titles = titles

    def to_ary = @titles
  end

  # Behaves like a Hash of search options wherever Ruby asks for one.
  class Filters
    def initialize(**options) = @options = options

    def to_hash = @options
  end
end

list = Shelf::ReadingList.new("Dune", "Emma")
first, second = list
[first, second] # => ["Dune", "Emma"]
p ["Ulysses"] + list # => ["Ulysses", "Dune", "Emma"]
[list, ["Ulysses"]].flatten # => ["Dune", "Emma", "Ulysses"]
[list].map { |a, b| "#{a}/#{b}" } # => ["Dune/Emma"]
%w[a b].zip(list) # => [["a", "Dune"], ["b", "Emma"]]
Array(list) # => ["Dune", "Emma"]
list.to_a # raises NoMethodError: undefined method 'to_a' for an instance of Shelf::ReadingList
[*list].size # => 1

def search(author: nil, available: true) = [author, available]

filters = Shelf::Filters.new(author: "Le Guin")
search(**filters) # => ["Le Guin", true]
{ available: false }.merge(filters) # => {available: false, author: "Le Guin"}
search(filters) # raises ArgumentError: wrong number of arguments (given 1, expected 0)

The points to take from it:

  • Multiple assignment, block parameters and method parameters in parentheses all use to_ary to destructure. The splat *list does not: it needs to_a, so [*list] has one element (the list itself) when only to_ary exists. Define to_a as well, usually as an alias, if the object should splat.

  • Array#+, zip, flatten and Array() convert with to_ary. flatten is the dangerous one: it calls to_ary on every element, so a wrapper that answers to_ary is flattened away (below).

  • The double splat **filters calls to_hash, and so does Hash#merge. Since Ruby 3.0 a positional argument is never turned into keywords automatically, so search(filters) is an ArgumentError: pass **filters. This is the separation described in Methods and Parameters.

  • Struct has to_a and to_h but no to_ary, and Data has to_h only, so neither destructures in an assignment. Pattern matching uses deconstruct and deconstruct_keys for that (see Pattern Matching).

to_int and to_proc

to_int is how the library accepts "something integral". Float implements it by truncation, so [1, 2, 3][1.9] is 2, and a class that wants to be an index, a count or a size does the same. to_int does not make an object a number for arithmetic: 3 + copies is a TypeError, because the + operator uses coerce (below), not to_int.

# frozen_string_literal: true

require "shelf"

module Shelf
  # A number of copies that can be used where Ruby asks for an Integer.
  class Copies
    def initialize(count) = @count = count

    def to_int = @count
  end

  # A reusable predicate: &by_author calls to_proc on it.
  class ByAuthor
    def initialize(name) = @name = name

    def to_proc = ->(book) { book.author.name == @name }
  end
end

copies = Shelf::Copies.new(2)
%w[a b c][copies] # => "c"
Array.new(copies, "Dune") # => ["Dune", "Dune"]
p "ab" * copies # => "abab"
Integer(copies) # => 2
[1, 2, 3].first(copies) # => [1, 2]
p 3 + copies # raises TypeError: Shelf::Copies can't be coerced into Integer

def book(isbn, title, author) = Shelf::Book.new(Shelf::Isbn.new(digits: isbn), title, Shelf::Author.new(author))

books = [
  book("978-0-306-40615-7", "Notes", "Ada Lovelace"),
  book("978-0-13-235088-4", "Engines", "Charles Babbage")
]
books.select(&Shelf::ByAuthor.new("Ada Lovelace")).map(&:title) # => ["Notes"]

# The built-in implementers of to_proc.
%w[a b].map(&:upcase) # => ["A", "B"]
%w[1 2].map(&method(:Integer)) # => [1, 2]
%i[a b].map(&{ a: 1, b: 2 }) # => [1, 2]
:upcase.to_proc.lambda? # => true
[1, 2].each(&5) # raises TypeError: no implicit conversion of Integer into Proc

ByAuthor is the useful shape of to_proc: a small object that carries configuration and is passed with & wherever a block is expected. The Proc it returns is a lambda here, which is the usual choice because it checks its arity. Passing something that cannot be converted, such as &5, raises TypeError with the message no implicit conversion of Integer into Proc.

to_path, to_io and the other implicit names

File.open(obj) and the other file-name methods accept anything with to_path before they try to_str, which is why a Pathname works as a file name although it is not a String. A class that represents a location, such as a store file, should implement to_path and leave to_str alone, so it cannot be added to a String by accident. IO.select and IO.try_convert use to_io the same way, so a wrapper around a socket or a file works where a stream is required once it defines to_io. Regexp.union and Regexp.try_convert use to_regexp.

try_convert: conversion that reports instead of raises

String, Array, Hash, Integer, Regexp and IO have a class method named try_convert that applies the protocol and returns nil when the object does not take part. It is the tool to write a method that accepts several shapes. Integer.try_convert was added in Ruby 3.1; the others are older.

# frozen_string_literal: true

tag = Object.new
def tag.to_str = "fiction"

tag2 = Object.new
def tag2.to_int = 3

String.try_convert(tag) # => "fiction"
String.try_convert(:fiction) # => nil
Array.try_convert([1]) # => [1]
Array.try_convert(1..2) # => nil
Hash.try_convert([[1, 2]]) # => nil
Integer.try_convert(tag2) # => 3
Integer.try_convert(1.9) # => 1
Integer.try_convert("1") # => nil
Regexp.try_convert("a+") # => nil
IO.try_convert($stdin).equal?($stdin) # => true

# The idiom behind Kernel#Array, written by hand: a value or a list of values.
def titles_of(value) = Array.try_convert(value) || [value]

titles_of("Dune") # => ["Dune"]
titles_of(%w[Dune Emma]) # => ["Dune", "Emma"]
titles_of(Struct.new(:x, :y).new(1, 2)).size # => 1
Array(Struct.new(:x, :y).new(1, 2)).size # => 2

# respond_to? is the duck-typing check; it is true for both protocols.
[tag.respond_to?(:to_str), :fiction.respond_to?(:to_str), tag2.respond_to?(:to_int)] # => [true, false, true]

titles_of is what Kernel#Array does minus the second step: it tests with to_ary only, so it treats a Struct as one value, while Array() goes on to to_a and turns a Struct into its members. respond_to?(:to_str) is the usual duck-typing test when a try_convert is not available, but try_convert also checks that the result has the right class and is the version the library itself uses.

When to implement an implicit conversion

  • Implement to_str, to_ary, to_hash or to_int only for classes that are substitutes for the target: wrappers, proxies, lazily-loaded collections, path-like values. A value object, a model or a domain type almost never qualifies.

  • Make the result a real instance of the target class, and never mutate or raise inside the conversion.

  • Prefer an explicit method with a meaningful name (to_h, to_a, to_path, to_proc) over an implicit one when callers can use it. Implicit conversions are invoked by code you did not write, with no visible call site.

  • Do not implement to_ary to make destructuring work by accident. Use deconstruct and deconstruct_keys, which pattern matching and case/in use and which flatten and puts ignore.

  • Return self or a copy deliberately: to_ary and to_hash of the core classes return self, so the callee may mutate the argument.

Conversion functions

The capitalised Kernel methods are the strict, input-validating converters. Their names deliberately match the class they return, which is why they are one of the few places where a method name starts with a capital letter, and they are conventionally written with parentheses: Integer("42"). They take any value, apply a documented conversion order, and raise when the value is not convertible.

Function Order of attempts Raises

Integer(value, base = nil, exception: true)

A String is parsed (prefixes, _, surrounding whitespace, the optional base); a Float is truncated; otherwise to_int, then to_i.

ArgumentError for text that is not an integer; TypeError for nil, a Symbol or an unconvertible object; FloatDomainError for NaN or Infinity.

Float(value, exception: true)

A String is parsed (decimal text, exponents, _, hex floats, surrounding whitespace); a number converts; otherwise to_f.

ArgumentError for bad text; TypeError for nil or an unconvertible object.

String(value)

to_str, then to_s.

TypeError when neither returns a String.

Array(value)

to_ary, then to_a, then a one-element Array. nil becomes [].

Nothing for ordinary values.

Hash(value)

nil and [] become {}; a Hash is returned; otherwise to_hash.

TypeError for everything else.

Rational(x, y = 1, exception: true)

Text such as "1/3" or "0.75", numbers, objects with to_r.

ArgumentError for bad text, TypeError for nil, ZeroDivisionError for a zero denominator.

Complex(real, imaginary = 0, exception: true)

Text such as "1+2i", numbers.

ArgumentError, TypeError.

The keyword exception: false is accepted by Integer(), Float(), Rational() and Complex() and turns the ArgumentError and TypeError failures into nil. It does not exist on String(), Array() or Hash(), which raise ArgumentError for it because they have no text to reject. These functions are also what format("%d", value) and "%d" % value call, so the same messages appear there.

Integer and Float

# frozen_string_literal: true

# Integer(): strict parsing of Strings, truncation of numbers.
Integer("42") # => 42
Integer(" 42\n") # => 42
Integer("12_000") # => 12000
Integer("-0b11") # => -3
Integer("0x1A") # => 26
Integer("0o17") # => 15
Integer("017") # => 15
Integer("ff", 16) # => 255
Integer("0x1A", 16) # => 26
Integer("zz", 36) # => 1295
Integer("08", 10) # => 8
Integer(3.99) # => 3
Integer(-3.99) # => -3
Integer("12abc") # raises ArgumentError: invalid value for Integer(): "12abc"
Integer("08") # raises ArgumentError: invalid value for Integer(): "08"
Integer("1e3") # raises ArgumentError: invalid value for Integer(): "1e3"
Integer("4 2") # raises ArgumentError: invalid value for Integer(): "4 2"
Integer("") # raises ArgumentError: invalid value for Integer(): ""
Integer("12", 1) # raises ArgumentError: invalid radix 1
Integer(nil) # raises TypeError: can't convert nil into Integer
Integer([1]) # raises TypeError: can't convert Array into Integer
Integer(Float::NAN) # raises FloatDomainError: NaN
Integer(Float::INFINITY) # raises FloatDomainError: Infinity

# exception: false turns the failures above into nil.
Integer("abc", exception: false) # => nil
Integer(nil, exception: false) # => nil
Integer("12", exception: false) # => 12

Source: Kernel#Integer

What deserves attention in Integer():

  • The prefix decides the base. 0x is hexadecimal, 0b binary, 0o and a bare leading 0 octal, so Integer("017") is 15 and Integer("08") is an error, because 8 is not an octal digit. Reading a user-supplied number such as a zero-padded day, a postcode or "08" therefore needs the base: Integer("08", 10) is 8.

  • A base given with a prefix must agree. Integer("0x1A", 16) is 26; Integer("0b11", 8) is an error. Giving a base for a non-String (Integer(10, 2)) is an ArgumentError (base specified for non string value). The base must be between 2 and 36 or the result is invalid radix.

  • Underscores between digits are accepted ("12_000"), whitespace around the number is accepted (" 42\n"), and anything else is not: no inner space, no exponent ("1e3"), no decimals ("0.5"), no empty string.

  • Non-String arguments are converted, not parsed. A Float is truncated toward zero (Integer(-3.99) is -3), and NaN and Infinity raise FloatDomainError; this is one of the failures exception: false also turns into nil. nil, true and Symbols raise TypeError.

  • Integer() accepts any object with to_int, then to_i. That makes it the right gate for "an integer or something integral", where Integer.try_convert would only check to_int.

Float() is the same idea for decimal text.

# frozen_string_literal: true

# Float(): the same strictness for decimal text.
Float("1e3") # => 1000.0
Float("1_000.5") # => 1000.5
Float("  2.5  ") # => 2.5
Float(".5") # => 0.5
Float("1.") # => 1.0
Float("0x1p3") # => 8.0
Float(3) # => 3.0
Float("3.7kg") # raises ArgumentError: invalid value for Float(): "3.7kg"
Float("") # raises ArgumentError: invalid value for Float(): ""
Float(nil) # raises TypeError: can't convert nil into Float
Float("abc", exception: false) # => nil

# String(): to_str first, then to_s. It never fails for ordinary objects.
String(nil) # => ""
String(12) # => "12"
String(:title) # => "title"
String([1, 2]) # => "[1, 2]"

# Array(): to_ary first, then to_a, otherwise a one-element Array.
Array(nil) # => []
list = [1, 2]
Array(list).equal?(list) # => true
Array(1) # => [1]
Array(1..3) # => [1, 2, 3]
Array("a\nb") # => ["a\nb"]
Array({ a: 1 }) # => [[:a, 1]]
Array(Struct.new(:x, :y).new(1, 2)) # => [1, 2]

# Hash(): nil and [] become {}, a Hash stays, everything else is an error.
Hash(nil) # => {}
empty = []
Hash(empty) # => {}
Hash({ a: 1 }) # => {a: 1}
Hash([[1, 2]]) # raises TypeError: can't convert Array into Hash
Hash(1) # raises TypeError: can't convert Integer into Hash
Hash([[1, 2]], exception: false) # raises ArgumentError: wrong number of arguments (given 2, expected 1)

Notes on the listing:

  • Float("1.") is 1.0 since Ruby 3.4; earlier versions raised ArgumentError for a decimal point with no digits after it. Float("NaN") and Float("Infinity") are errors: text never produces them, though Float(Float::NAN) returns NaN.

  • Array() is the guard-clause conversion for "one value or a list": Array(nil) is [], Array([1]) is the same Array, and anything else becomes a one-element list, except objects that answer to_a. A Hash becomes its pairs, a Range its members, a Struct its values, and a Time its ten calendar fields, and Array(file) reads every line of an open file. Use Array.try_convert(value) || [value] or [value].flatten(1) when only real Arrays should expand.

  • String() is rarely needed because interpolation does the same, but it never returns nil and never raises for an ordinary object (String(nil) is ""), which makes it the neutral choice for "convert whatever this is for display".

  • Hash() is the narrowest: nil, [], a Hash or an object with to_hash. It does not turn an Array of pairs into a Hash; use to_h for that.

Rational and Complex

# frozen_string_literal: true

# Rational() and Complex() parse text and combine integers.
Rational("1/3") # => (1/3)
Rational("0.75") # => (3/4)
Rational(" 1/3 ") # => (1/3)
Rational(1, 3) # => (1/3)
Rational("abc") # raises ArgumentError: invalid value for convert(): "abc"
Rational(nil) # raises TypeError: can't convert nil into Rational
Rational(1, 0) # raises ZeroDivisionError: divided by 0
Rational("abc", exception: false) # => nil
Rational(nil, exception: false) # => nil
Complex("1+2i") # => (1+2i)
Complex("x", exception: false) # => nil

# A Float carries its binary error into the Rational; text does not.
Rational(0.1) # => (3602879701896397/36028797018963968)
Rational("0.1") # => (1/10)
[0.1 + 0.2, Rational("0.1") + Rational("0.2")] # => [0.30000000000000004, (3/10)]

Rational("0.1") is (1/10) because the text is read as a decimal fraction; Rational(0.1) is the huge fraction that the binary Float really holds. Prefer the text form for money, rates and anything typed by a person, and see Numbers for the full numeric tower, round(half:) and BigDecimal.

Strict or lenient: a decision per input

Pick the conversion by what a bad input should do. The shelf gem reads a loan period from the environment:

# frozen_string_literal: true

# Reading a loan period from the environment: a default for absent input,
# an error for input that is present but wrong.
DEFAULT_LOAN_DAYS = 14

def loan_days_lenient(text) = text.to_i.nonzero? || DEFAULT_LOAN_DAYS

def loan_days_strict(text)
  return DEFAULT_LOAN_DAYS if text.nil? || text.strip.empty?

  days = Integer(text, 10, exception: false)
  raise ArgumentError, "SHELF_LOAN_DAYS must be a positive whole number, got #{text.inspect}" unless days&.positive?

  days
end

loan_days_lenient("21") # => 21
loan_days_lenient("3 weeks") # => 3
loan_days_lenient("three") # => 14
loan_days_strict("21") # => 21
loan_days_strict("08") # => 8
loan_days_strict(nil) # => 14
loan_days_strict("  ") # => 14
loan_days_strict("3 weeks") # raises ArgumentError: SHELF_LOAN_DAYS must be a positive whole number, got "3 weeks"
loan_days_strict("-2") # raises ArgumentError: SHELF_LOAN_DAYS must be a positive whole number, got "-2"
loan_days_strict(ENV.fetch("SHELF_LOAN_DAYS", nil)) # => 14

loan_days_lenient reads "3 weeks" as 3 days and "three" as 14 without a sound; both are wrong in a way nobody notices. loan_days_strict distinguishes absent input (use the default), valid input, and present-but-invalid input (fail fast, naming the variable and the value). The pattern is general:

Situation Use Reason

Input from a user, file or environment that must be a number

Integer(text, 10) or Float(text)

Rejects "12abc" and "1e3" instead of guessing, and the base avoids the octal trap.

Optional input with a documented default

Integer(text, 10, exception: false) || default

nil means "not usable"; a real 0 is not replaced, because 0 is truthy in Ruby.

Reading a number from the front of a longer text

to_i, to_f, or a Regexp

Lenient on purpose.

A value that is one thing or a list of things

Array(value)

Normalises nil, one value and many values.

Internal values of a known class

no conversion

Converting everything defensively hides bugs; validate at the edge only.

Guard clauses: class, capability or conversion

A method that needs a number of days can check its argument in three ways, and the choice decides which callers succeed.

# frozen_string_literal: true

module Shelf
  # Three ways to guard a parameter that must be a number of days.
  module Guards
    # By class: strict, rejects 14.0 and every duck.
    def self.by_class(days)
      raise ArgumentError, "days must be an Integer, got #{days.inspect}" unless days.is_a?(Integer)

      days
    end

    # By capability: accepts anything with an implicit integer conversion.
    def self.by_duck(days)
      raise ArgumentError, "days must respond to to_int, got #{days.inspect}" unless days.respond_to?(:to_int)

      days.to_int
    end

    # By conversion: accepts text as well and lets Ruby produce the error.
    def self.by_conversion(days) = Integer(days, exception: false) || raise(ArgumentError, "bad days: #{days.inspect}")
  end
end

Shelf::Guards.by_class(14) # => 14
Shelf::Guards.by_class(14.0) # raises ArgumentError: days must be an Integer, got 14.0
Shelf::Guards.by_duck(14.9) # => 14
Shelf::Guards.by_duck("14") # raises ArgumentError: days must respond to to_int, got "14"
Shelf::Guards.by_conversion("14") # => 14
Shelf::Guards.by_conversion(14.9) # => 14
Shelf::Guards.by_conversion("two") # raises ArgumentError: bad days: "two"
Shelf::Guards.by_conversion(nil) # raises ArgumentError: bad days: nil

is_a? is the strictest and the least Ruby-like: it refuses a Float and any user-defined integral type. respond_to?(:to_int) accepts everything the core library itself would accept as an integer, and its message names the missing capability. Integer() is the most permissive, since it also parses text, so it belongs at the edge of the program where input is text, not in the middle of it. In all three the guard raises ArgumentError with the offending value, because a wrong value for the right kind of parameter is an ArgumentError; the TypeError of the implicit conversions is reserved for a wrong class (see Error messages).

Conversions in your own classes

A class chooses which conversions to offer, and the convention is the same as in the core library.

  • to_s for the user-facing text, inspect for the debugging view, to_h for a Hash view, to_a for a list view, to_proc for a block-compatible object. Name them like the core methods or callers will not find them.

  • Data and Struct already provide them. Data has to_h (and deconstruct_keys), Struct has to_a and to_h; a Data has no to_a, so Array(data) is a one-element list holding the object, and it will not splat. See Struct, Data and Value Objects.

  • A converting constructor accepts the type itself, converts what it understands and raises TypeError for the rest. The core library names these with a capital letter (Integer(value)); RuboCop’s Naming/MethodName flags that spelling in application code, so the usual name is from, parse or coerce, and the Kernel-style name is kept for libraries that deliberately mimic it.

# frozen_string_literal: true

require "shelf"

module Shelf
  class Isbn
    # A converting constructor: accept what is already an Isbn, convert what can
    # be converted, and refuse the rest with a TypeError.
    def self.from(value)
      case value
      when Isbn then value
      when String, Integer then new(digits: value)
      else raise TypeError, "can't convert #{value.class} into Shelf::Isbn"
      end
    end
  end

  # One book or many; nil means none.
  def self.add_all(catalog, books)
    Array(books).each { |book| catalog.add(book) }
    catalog
  end
end

Shelf::Isbn.from("978-0-306-40615-7") # => #<data Shelf::Isbn digits="9780306406157">
Shelf::Isbn.from(9_780_306_406_157).to_s # => "9780306406157"
Shelf::Isbn.from(nil) # raises TypeError: can't convert NilClass into Shelf::Isbn
Shelf::Isbn.from("978-0-306-40615-8") # raises ArgumentError: invalid ISBN: 978-0-306-40615-8

book = Shelf::Book.new(Shelf::Isbn.from("978-0-306-40615-7"), "Notes", Shelf::Author.new("Ada Lovelace"))
Shelf.add_all(Shelf::Catalog.new(store: Shelf::Store::Memory.new), book).count # => 1
Shelf.add_all(Shelf::Catalog.new(store: Shelf::Store::Memory.new), [book, book]).count # => 2
Shelf.add_all(Shelf::Catalog.new(store: Shelf::Store::Memory.new), nil).count # => 0

# Data and Struct publish their state through different conversions.
author = Shelf::Author.new("Ada Lovelace")
author.to_h # => {name: "Ada Lovelace"}
author.respond_to?(:to_a) # => false
Struct.new(:name).new("Ada Lovelace").to_a # => ["Ada Lovelace"]
Array(author) # => [#<data Shelf::Author name="Ada Lovelace">]

Shelf.add_all accepts one book, a list of books or nothing, which is the Array() guard clause: the caller can pass whatever it has, and the method has one code path. The Isbn.from method follows the three-part shape of a converting constructor: return an instance unchanged, convert the types it understands (a String, an Integer), and raise TypeError with the class name for the rest. Invalid values still raise ArgumentError from Isbn.new, which keeps the two failure kinds apart: a wrong class is a TypeError, a wrong value is an ArgumentError.

Coercion

Arithmetic and comparison operators are methods of their left operand. 1 + x is 1.+(x), and Integer#+ knows only how to add another Integer, a Float, a Rational or a Complex. For anything else it asks the right operand to do the work: it calls x.coerce(1), expects a two-element Array [a, b] back, and evaluates a + b. The same protocol is used by -, *, /, **, %, divmod and the relational operators <, <=, > and >= of the numeric classes.

flowchart TD S(["left + right"]) --> Q1{"does Integer#+ know the class of right?"} Q1 -->|"Integer, Float, Rational, Complex"| R1["compute the sum"] Q1 -->|"anything else"| Q2{"does right respond to coerce?"} Q2 -->|no| E1["TypeError: right can't be coerced into Integer"] Q2 -->|yes| C["pair = right.coerce(left)"] C --> Q3{"is pair a two-element Array?"} Q3 -->|no| E2["TypeError: coerce must return [x, y]"] Q3 -->|yes| R2["pair[0] + pair[1]"]
# frozen_string_literal: true

module Shelf
  # A late fee in cents that takes part in Integer arithmetic.
  class Fine
    include Comparable

    attr_reader :cents

    def initialize(cents) = @cents = cents

    def +(other) = Fine.new(cents + cents_of(other))

    def -(other) = Fine.new(cents - cents_of(other))

    def *(other)
      raise TypeError, "a Fine cannot be multiplied by a Fine" if other.is_a?(Fine)

      Fine.new(cents * other)
    end

    def <=>(other) = other.is_a?(Fine) ? cents <=> other.cents : nil

    # Integer#+ and friends call this when the right operand is a Fine.
    # It returns a pair [left, right] of objects that know how to combine.
    def coerce(number) = [Fine.new(number), self]

    def to_s = format("%<euros>d.%<rest>02d", euros: cents / 100, rest: cents % 100)

    private

    def cents_of(other) = other.is_a?(Fine) ? other.cents : Integer(other)
  end
end

fine = Shelf::Fine.new(250)
(fine + 100).to_s # => "3.50"
(100 + fine).to_s # => "3.50"
(1000 - fine).to_s # => "7.50"
(fine * 3).to_s # => "7.50"
low = 100
high = 300
exact = 250
[low < fine, high < fine, exact == fine] # => [true, false, false]
floor = Shelf::Fine.new(200)
ceiling = Shelf::Fine.new(400)
[100, 250, 900].map { Shelf::Fine.new(it).clamp(floor, ceiling).to_s } # => ["2.00", "2.50", "4.00"]
p 3 * fine # raises TypeError: a Fine cannot be multiplied by a Fine
word = "x"
p fine + word # raises ArgumentError: invalid value for Integer(): "x"
p 1 + Object.new # raises TypeError: Object can't be coerced into Integer
one = 1
p one < Object.new # raises ArgumentError: comparison of Integer with Object failed
p 1 <=> Object.new # => nil

# What Numeric itself does.
1.coerce(2.5) # => [2.5, 1.0]
1.5.coerce(2) # => [2.0, 1.5]
Rational(1, 2).coerce(2) # => [(2/1), (1/2)]
1.coerce("2") # => [2.0, 1.0]
[1 + 1r, 1r + 1.0, 1.0 + 1i] # => [(2/1), 2.0, (1.0+1i)]

What the listing demonstrates:

  • coerce(number) returns a pair whose first element will be the new left operand. Returning [Fine.new(number), self] means that 100 + fine becomes Fine.new(100) + fine, and the Fine operators do the rest. This is also why 1000 - fine is correct without any special case: the pair keeps the operand order.

  • Relational operators use it too. Integer#< coerces and then compares the pair, so 100 < fine works as long as Fine has < (here from Comparable). The relational methods raise ArgumentError (comparison of Integer with Object failed) when the other side cannot take part, where the arithmetic ones raise TypeError.

  • == does not coerce. 250 == fine is false, because Integer#== asks fine == 250 and Fine#<=> returns nil for a non-Fine. <=> also returns nil for an operand it cannot compare instead of raising, which is how sort finds out that two things are not comparable.

  • Coercion is a contract about every operator, not only about +. 3 * fine becomes Fine.new(3) * fine, so Fine#* receives a Fine and the listing rejects it: a class that supports number * object must choose its coerce result to fit every operator it supports.

  • The numeric classes define the generality ladder. Integer coerces to Rational to Float to Complex: 1 + 1r is (2/1), 1r + 1.0 is 2.0, 1.0 + 1i is (1.0+1i). Integer#coerce with a non-numeric argument converts both sides with Float(), so 1.coerce("2") is [2.0, 1.0]; that never helps 1 + "2", because a String has no coerce of its own and the operator raises first.

A class derived from Numeric (the base of Integer, Float, Rational and Complex) inherits zero?, positive? and, through Comparable, clamp and between?, and must define its own coerce: the inherited Numeric#coerce tries to turn both sides into Floats and raises TypeError for an object that cannot become one. Keep a class that is not really a number, like a late fee, outside the hierarchy and give it coerce as above.

The commutative shortcut and its trap

A frequent first attempt at coerce returns [self, number], so that number + object becomes object + number. That is right for + and *, whose operands commute, and wrong for -, /, ** and <, whose operands do not.

# frozen_string_literal: true

# A coerce that returns [self, number] is only right for commutative operators.
Quantity = Struct.new(:value) do
  def -(other) = value - other

  def coerce(number) = [self, number]
end

quantity = Quantity.new(3)
p quantity - 10 # => -7
[10 - quantity] # => [-7]

Source: Numeric#coerce

10 - quantity should be 7 and the program prints -7 without any error. Return a pair of objects of the same class ([Quantity.new(number), self]) and implement every operator for that class, or return [number, value] if the object is really a wrapper around a plain number.

What a failed coercion looks like

# frozen_string_literal: true

# What Integer#+ does with a right operand that has a broken coerce.
not_a_pair = Object.new
def not_a_pair.coerce(_number) = :nope

wrong_size = Object.new
def wrong_size.coerce(number) = [number]

raises = Object.new
def raises.coerce(_number) = raise(ArgumentError, "no way")

returns_nil = Object.new
def returns_nil.coerce(_number) = nil

p 1 + not_a_pair # raises TypeError: coerce must return [x, y]
p 1 + wrong_size # raises TypeError: coerce must return [x, y]
p 1 + raises # raises ArgumentError: no way
p 1 + returns_nil # raises TypeError: coerce must return [x, y]
p 1 + Object.new # raises TypeError: Object can't be coerced into Integer

coerce must return exactly two elements, nil is not accepted, and an exception raised inside it is not swallowed. When the right operand has no coerce at all, the message names its class: Object can't be coerced into Integer, nil can't be coerced into Integer and String can't be coerced into Float.

Error messages

The messages are stable enough to recognise on sight. They name the class of the failing value in current Ruby (a nil receiver is called nil, not NilClass), and they separate the three kinds of failure.

# frozen_string_literal: true

def show
  yield
rescue StandardError => e
  puts "#{e.class}: #{e.message}"
end

text = "a"
digit = "2"

# Implicit conversion failures: a wrong class, never a wrong value.
show { text + :b }
show { [1] + nil }
show { Array.new("3") }
show { [3, 1].first("1") }
show { [1, 2, 3][Object.new] }
show { {}.merge([[1, 2]]) }
show { File.basename(42) }

# Coercion failures: the right operand cannot take part in the arithmetic.
show { 1 + digit }
show { 1 + nil }
show { 1 * Object.new }
show { 1.5 - "x" }
show { 1 < "2" }
show { 1.0 >= nil }

# Explicit parsing failures.
show { Integer("12abc") }
show { Integer(nil) }
show { Float("") }
show { format("%d", "1x") }
show { format("%d", nil) }
show { nil + 1 }
show { "5".to_int }

# Non-raising results.
p 1 <=> "2"
p 1 == "1"
TypeError: no implicit conversion of Symbol into String
TypeError: no implicit conversion of nil into Array
TypeError: no implicit conversion of String into Integer
TypeError: no implicit conversion of String into Integer
TypeError: no implicit conversion of Object into Integer
TypeError: no implicit conversion of Array into Hash
TypeError: no implicit conversion of Integer into String
TypeError: String can't be coerced into Integer
TypeError: nil can't be coerced into Integer
TypeError: Object can't be coerced into Integer
TypeError: String can't be coerced into Float
ArgumentError: comparison of Integer with String failed
ArgumentError: comparison of Float with nil failed
ArgumentError: invalid value for Integer(): "12abc"
TypeError: can't convert nil into Integer
ArgumentError: invalid value for Float(): ""
ArgumentError: invalid value for Integer(): "1x"
TypeError: can't convert nil into Integer
NoMethodError: undefined method '+' for nil
NoMethodError: undefined method 'to_int' for an instance of String
nil
false
Message Exception Meaning and fix

no implicit conversion of X into Y

TypeError

A method that needs a Y received an X that has no to_y. Convert explicitly (to_s, Integer()) or pass the right class.

can't convert X to Y (X#to_y gives Z)

TypeError

The object has the implicit method but it returned a Z. Fix the method.

X can't be coerced into Y

TypeError

An arithmetic operator found a right operand without coerce. Convert the operand or define coerce.

comparison of X with Y failed

ArgumentError

A relational operator, sort, min, max or clamp compared values that are not comparable. Convert or define <=>.

invalid value for Integer(): "..."

ArgumentError

Text that is not a valid number. Validate or use exception: false.

can't convert nil into Integer

TypeError

Integer(), Float() or format given nil. Supply a default or reject earlier.

undefined method 'x' for nil

NoMethodError

A conversion method (or any method) called on nil, usually a missing to_s or to_a when the value was optional. Use &., Array() or to_s deliberately.

FloatDomainError

RangeError subclass

Integer(), to_i or round on NaN or Infinity.

wrong number of arguments (given 1, expected 0)

ArgumentError

A conversion method that takes arguments, or a Hash passed where keywords were expected (since Ruby 3.0).

method_missing and conversion names

The core library calls implicit conversions through a probe: it first asks whether the object responds to to_ary or to_str, and only then calls it. An object that answers respond_to? for every name, which is what a dynamic proxy that implements method_missing and respond_to_missing? for everything does, looks convertible to everything. The effect is not an error but wrong data: flatten and puts quietly drop the proxy.

# frozen_string_literal: true

# A catch-all method_missing that also answers respond_to? for every to_* name.
class Ghost
  def method_missing(name, *) = name.start_with?("to_") ? [] : super

  def respond_to_missing?(name, include_private = false) = name.start_with?("to_") || super
end

ghost = Ghost.new
[ghost, 1].flatten # => [1]
prefix = "x"
p prefix + ghost # raises TypeError: can't convert Ghost to String (Ghost#to_str gives Array)

The Ghost returned [] for to_ary, so flatten replaced it with that empty list and the Array lost an element without a warning, and to_str returned an Array instead of a String, which raised the TypeError shown. The fix is to answer only the names the proxy really supports and to hand every other name to super, which raises NoMethodError; the conversion probes treat that as "not convertible".

# frozen_string_literal: true

# A proxy that forwards only the names it really knows.
class Careful
  FORWARDED = %i[title pages].freeze

  def initialize(target) = @target = target

  def method_missing(name, *, &) = FORWARDED.include?(name) ? @target.public_send(name, *, &) : super

  def respond_to_missing?(name, include_private = false) = FORWARDED.include?(name) || super
end

book = Struct.new(:title, :pages).new("Dune", 612)
careful = Careful.new(book)
careful.title # => "Dune"
careful.respond_to?(:to_ary) # => false
[careful, 1].flatten.size # => 2
prefix = "x"
p prefix + careful # raises TypeError: no implicit conversion of Careful into String

Careful forwards title and pages, reports so through respond_to_missing?, and stays a single element in a flatten. Rules for any method_missing: always define respond_to_missing? with the same list, never answer names that begin with to_ unless you mean the conversion, and call super for the rest. The details of method_missing itself are in Dynamic Dispatch and Method Definition.

Truthiness is not a conversion

There is no to_bool. A condition tests the object itself: only nil and false are falsy, and everything else, including 0, "", [] and {}, is truthy, with no method called. !!value produces true or false by applying ! twice and value.nil? or value.empty? ask the specific question. Code ported from languages where 0 or "" is false is a frequent source of bugs; see Control Expressions.

Guidelines

  • Convert at the edge of the program (command line, files, HTTP, the environment) with the strict functions, and keep the internals typed; do not sprinkle to_s and to_i through the logic.

  • Use Integer(text, 10) for user-visible numbers, Float(text) for decimals, Rational(text) or BigDecimal for money, and exception: false with || for optional input.

  • Use Array() for "one or many", Hash() for "a Hash or nothing", and know that Array() expands every object with to_a.

  • Implement to_s and inspect for every class that people will print; implement to_str, to_ary, to_hash or to_int only for true substitutes; implement to_path and to_proc freely.

  • Keep ==, eql? and hash consistent with your conversions; a class that converts to a String should usually also compare equal to it.

  • Raise TypeError for a wrong class and ArgumentError for a wrong value, with the offending value in the message.

  • For operators on your own types, define coerce returning a pair of your own class, and test number op object for every operator, not only +.

  • Do not override method_missing for names that start with to_ without an explicit allow list.

RuboCop cops for conversions

Cop What it reports

Style/StringConcatenation

"a" + b where interpolation would do. It looks only for a String literal operand, which is why the listings on this page put the String in a variable when the implicit conversion is the point.

Lint/SymbolConversion

"title".to_sym and :"title" where a plain Symbol literal is enough.

Style/RedundantArrayConstructor

Array([1, 2]) and Array.new([1, 2]) on something that is already an Array literal.

Style/EmptyLiteral

Hash([]), String.new, Array.new with no arguments in favour of {}, "" and [].

Style/ArrayCoercion

[*value] and explicit is_a?(Array) checks written to coerce to an Array, in favour of Array(value) (disabled by default).

Style/RedundantInterpolation

"#{value}" where value.to_s says the same.

Style/YodaCondition

1 < x written with the literal first, which is exactly the shape where coercion is used.

Naming/MethodName

A converting method named like a class, def Isbn(value).

Naming/BinaryOperatorParameterName

An operator method (+, *, <=>) whose parameter is not named other.

Pitfalls

  • to_i hides bad input. "12abc".to_i, "abc".to_i and nil.to_i return numbers. Use Integer() when the input must be valid.

  • The octal trap. Integer("08") and Integer("09") fail because a leading 0 selects octal. Pass the base: Integer("08", 10). "08".to_i is 8.

  • Array(object) is not "wrap in an Array". It expands a Hash into pairs, a Struct into values, a Time into its ten fields, a Range into members, and an open file into its lines. Array(nil) is [], which silently changes meaning.

  • [*obj] and Array(obj) differ. The splat uses to_a only; Array() tries to_ary first.

  • to_s that does not return a String is ignored by interpolation and gives #<Class:0x...>.

  • Defining to_str for convenience. It lets the object be added to Strings, joined, used as a file name and compared through String#==, so mistakes such as passing the object where a path or a name was meant succeed silently. Define it only when the object is a String stand-in.

  • to_ary on a wrapper makes flatten, puts and destructuring consume it.

  • method_missing that answers every to_* (see the section above).

  • A Hash is not keywords. Since Ruby 3.0, foo(hash) never turns a Hash into keyword arguments. Pass **hash.

  • Float to Integer is truncation, not rounding. Integer(3.99) is 3; use round, ceil or floor to choose.

  • == and eql? ignore conversion. 1 == 1.0 is true and 1.eql?(1.0) is false; Hash keys use eql? and hash, so { 1 => :a }[1.0] is nil.

  • coerce returning [self, number] is only right for commutative operators.

  • Rescuing TypeError to detect the type of an argument also hides genuine conversion bugs inside the call. Test with respond_to? or try_convert first.

  • Integer("1e3") fails but "1e3".to_f is 1000.0. Parsing a number written with an exponent needs Float() and then an explicit conversion with to_i or round.

  • Locale and Unicode digits. Integer() and to_i read ASCII digits only; Arabic-Indic digits such as "١٢" are an ArgumentError in Integer() and 0 in to_i. There is no locale-aware parsing in the core library.

Comparison with other languages

Language Model Difference from Ruby

Python and Python operators

int(x), str(x), __index__, __int__, and reflected operators __radd__.

Integer() is int(); Python’s reflected operator (__radd__) does what coerce does, but from the left operand’s point of view, and returns NotImplemented instead of a pair. Ruby has no implicit string conversion for numbers either.

PHP

Automatic juggling between strings and numbers ("5" + 1).

Ruby never converts a String to a number for you; 1 + "5" is a TypeError.

TypeScript and JavaScript

Loose coercion ("5" * 2), Number(), String().

Integer() and Float() are closest to Number(), but they throw instead of returning NaN.

Rust

From, Into, TryFrom, FromStr, and compile-time coercion.

TryFrom is Integer.try_convert plus a raising constructor; Ruby decides at run time, by method name, and an unimplemented conversion is a NoMethodError or TypeError, not a compile error.

Go

Explicit conversion T(x), strconv.Atoi returning an error.

Integer(text, 10, exception: false) is the nearest to strconv.Atoi; Go has no implicit conversions between numeric types.

Swift

Int("42") returns an optional; as? casts.

Integer("42", exception: false) returns nil like Swift’s failable initialiser.

C# and Kotlin

int.Parse/TryParse, toInt()/toIntOrNull().

Integer() is Parse, and exception: false is TryParse (toIntOrNull).

Where the books are outdated or wrong

The conversion idioms in older books are mostly still valid; these details changed or were never right.

Area Older material shows Current approach (Ruby 4.0)

Kernel#Array() as a guard clause

Advice to wrap "one or many" arguments with Array() and treat the result as a plain list.

Right for nil, a scalar and an Array; objects with to_a expand too (Hash, Struct, Range, Time, files). Use Array.try_convert(value) || [value] when only real Arrays should be flattened into the list.

Error messages

undefined method `to_a' for nil:NilClass with a backtick and the class name.

undefined method 'to_a' for nil (receiver format since 3.3, apostrophe since 3.4); other receivers show for an instance of Shelf::Isbn.

Hash in output

{:name=>"Ico"} from inspect.

{name: "Ico"} and {"name" => "Ico"} (Ruby 3.4).

Fixnum and Bignum

Fixnum, Bignum in coercion tables and error messages (String can't be coerced into Fixnum).

One class, Integer; the constants were removed in Ruby 3.2. Messages say Integer.

Last-argument Hash

A Hash passed as the last argument is converted to keywords.

Never since Ruby 3.0; **hash is required, and Hash#to_hash is used by **.

String#to_i for validation

to_i and to_f used to check numbers, with a to_s comparison.

Integer(), Float() with exception: false.

Number parsing

Float("1.") raises.

Accepted since Ruby 3.4 (1.0).

*nil

The splat calls nil.to_a.

Not since Ruby 4.0; the result is the same empty list.

Integer.try_convert

Not covered.

Added in Ruby 3.1.

Version notes

Version Change

3.0

Positional and keyword arguments are separated: a Hash is not converted to keywords, and ** calls to_hash explicitly.

3.1

Integer.try_convert.

3.2

Fixnum and Bignum constants removed; messages and coerce documentation say Integer.

3.3

Error messages name the receiver by class ("for an instance of").

3.4

Float("1.") and Float("1.E-1") accepted. Error messages and backtraces quote with an apostrophe. Hash#inspect prints {key: value}. bigdecimal becomes a bundled gem.

4.0

*nil no longer calls nil.to_a, like **nil with nil.to_hash.