skip to content

Version Line & Upgrades

Ruby ships a new minor version every year, and 3.0 to 4.0 brought keyword separation, Prism and a core Set. Interviewers ask how you upgrade safely and surface deprecations early.

on this pageshow

explore

questions

5

In Ruby, how do you check which version is running, and why is comparing RUBY_VERSION strings with < unreliable?

level: juniorimportance: must knowfreq 55%

answer

  1. a frozen String, not a number
  2. character-by-character comparison
  3. "3.4.10" vs "3.4.9"
  4. Gem::Version compares numeric segments
  5. prereleases sort before the release

basics

~10 s

RUBY_VERSION holds the running version as a String such as "4.0.7". Comparing it with < compares characters, so "3.4.10" < "3.4.9" is true; wrap both sides in Gem::Version, which compares numeric segments.

solid answer

~30 s

`RUBY_VERSION` is a frozen String like `"4.0.7"` (major.minor.teeny); `ruby -v` prints it with build details. Strings compare **character by character**, so `"3.4.10" < "3.4.9"` is `true` because `"1"` sorts before `"9"`, and 3.4 really has shipped teeny releases past 9. The fix is `Gem::Version`, which splits the string into segments and compares them as numbers: `Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4")`. `Gem.ruby_version` already returns the running Ruby as a `Gem::Version`. Gem::Version also treats a segment with letters as a **prerelease**, so `4.0.0.preview2` sorts before `4.0.0`. For a single feature, checking that the feature exists can be clearer than any version check.

code

ruby · 10 lines
ruby
RUBY_VERSION                      # => "4.0.7"
RUBY_VERSION.frozen?              # => true

"3.4.10" < "3.4.9"                # => true, string order

running = Gem.ruby_version        # a Gem::Version
running >= Gem::Version.new("3.4") # => true

Gem::Version.new("3.4.10") > Gem::Version.new("3.4.9")        # => true
Gem::Version.new("4.0.0.preview2") < Gem::Version.new("4.0.0") # => true

go deeper

for a junior

Recall that RUBY_VERSION is a String such as "4.0.7", that ruby -v prints it, and that version comparisons go through Gem::Version.

for a middle

Explain why String#<=> gets "3.4.10" versus "3.4.9" wrong, how Gem::Version compares numeric segments, and how prereleases sort.

for a senior

Prefer detecting a feature over checking a version when possible, and keep unavoidable version guards in one place so an upgrade removes them in one change.

for a principal

Treat scattered RUBY_VERSION guards as upgrade debt: set a supported-version floor per codebase and delete branches for versions below it.

## What RUBY_VERSION is Every Ruby process defines the constant **`RUBY_VERSION`**: a frozen `String` holding the version of the Ruby language being run, in the form **major.minor.teeny**, for example `"4.0.7"`. `ruby -v` prints the same number together with the release date, commit, platform and markers such as `+PRISM`; the full line is also available as `RUBY_DESCRIPTION`. The three parts mean different things: - **major.minor** (`4.0`) - the yearly feature release. Ruby's own headers note that the minor version "changes annually", and that a major bump such as 3.4 to 4.0 is not a rewrite; for compatibility purposes a major and a minor bump are treated the same. - **teeny** (`7`) - bug-fix and security releases on that branch, `4.0.1`, `4.0.2` and so on. Ruby 4.0 also exposes the value as `Ruby::VERSION`, but `RUBY_VERSION` works on every version and is what most code uses. ## Why string comparison is wrong `RUBY_VERSION` is a String, so `<`, `>=` and `sort` use `String#<=>`, which compares **character by character**: ```ruby "3.4.10" < "3.4.9" # => true ("1" sorts before "9") "10.0.0" < "9.0.0" # => true "4.0.7" >= "3.4" # => true, but only by luck ``` This is not hypothetical: the 3.4 branch has shipped teeny releases 3.4.10 and 3.4.11, so a guard such as `RUBY_VERSION >= "3.4.9"` silently fails on a newer 3.4. Converting to a number does not help either: `RUBY_VERSION.to_f` drops the teeny part, and `to_i` keeps only the major number, so `"3.4.1".to_i >= 3.4` is `false`. ## Gem::Version: the comparison RubyGems itself uses RubyGems, which every normal Ruby process loads, provides **`Gem::Version`**. It splits a version string into segments and compares them one by one as numbers, so `3.10` is greater than `3.2`: 1. Build both sides: `Gem::Version.new(RUBY_VERSION)` and `Gem::Version.new("3.4")`. 2. Compare with `<`, `>=` and friends; `Gem::Version` includes `Comparable`. 3. Or start from `Gem.ruby_version`, which already returns the running Ruby as a `Gem::Version`. Details worth knowing: - **Prereleases.** A segment containing letters marks a prerelease, which sorts before the final release: `4.0.0.preview2 < 4.0.0`. A dash is normalised, so `"4.0.0-preview2"` becomes `4.0.0.pre.preview2`. - **Trailing zeros.** `Gem::Version.new("4.0") == Gem::Version.new("4.0.0")` is `true`, although `eql?` is `false` because the precision differs. - **Bad input.** `Gem::Version.new("abc")` raises `ArgumentError` ("Malformed version number string"). - **Development builds.** On a preview or trunk build, `Gem.ruby_version` appends the prerelease tag from `RUBY_DESCRIPTION`, so a preview of 4.0.0 compares below the final 4.0.0 while `RUBY_VERSION` alone reads `"4.0.0"`. | Expression | Result | Why | |---|---|---| | `"3.4.10" < "3.4.9"` | `true` | String comparison, character by character | | `Gem::Version.new("3.4.10") < Gem::Version.new("3.4.9")` | `false` | numeric segments: 10 > 9 | | `Gem::Version.new("4.0.0.preview2") < Gem::Version.new("4.0.0")` | `true` | prerelease sorts first | | `Gem.ruby_version >= Gem::Version.new("3.4")` | `true` on 4.0.7 | already a Gem::Version | ## Reading ruby -v The command-line view carries more than the number. A CRuby 4.0.7 build prints a line of the form `ruby 4.0.7 (<release date> revision <commit>) +PRISM [x86_64-linux]`: the version, the release date and the source revision, markers for active options such as `+PRISM` (the default parser since 3.4) or `+YJIT` when a JIT is on, and the platform in brackets. When a bug report says "Ruby 4.0", ask for this line; it answers which teeny release, which build and which options at once. Common mistakes in version checks: - Comparing `RUBY_VERSION` with a String literal, as above. - Checking `RUBY_ENGINE_VERSION` for language features; on JRuby or TruffleRuby that is the engine's own release number. - Scattering the same guard across many files, so an upgrade has to hunt for all of them. ## When not to compare versions at all A version check encodes an assumption: "3.4 has X". When the question really is "does this method or class exist?", testing for it states the intent directly and keeps working on engines whose `RUBY_VERSION` lags CRuby. Version checks fit best for **behaviour changes** that cannot be detected, such as a changed default or message format. ## What interviewers listen for They want `RUBY_VERSION` named as a String, the `"3.4.10"` trap explained, and `Gem::Version` or `Gem.ruby_version` as the fix.

  • Why might Gem.ruby_version differ from Gem::Version.new(RUBY_VERSION)?
    On release builds they are equal. On a development or preview build, where `RUBY_PATCHLEVEL` is -1, `Gem.ruby_version` appends the prerelease tag it reads from `RUBY_DESCRIPTION`, so a preview of 4.0.0 compares below the final 4.0.0. `RUBY_VERSION` alone would read `"4.0.0"` for both.
  • Does Gem::Version.new("4.0") equal Gem::Version.new("4.0.0")?
    With `==`, yes: comparison ignores trailing zero segments, so `<=>` returns 0. With `eql?`, no: RubyGems documents that a version is `eql?` only to one written at the same precision, so the two would be separate Hash keys.

saying these in an interview costs you the question

  • RUBY_VERSION >= "3.4.9" is a safe guard for every later 3.4 release
  • RUBY_VERSION.to_f is an accurate way to compare Ruby versions
  • RUBY_VERSION is an Integer or Float you can compare numerically
  • Gem::Version sorts 4.0.0.preview2 after the final 4.0.0
  • Gem::Version compares versions as plain strings
open as a page

In Ruby 3.x and 4.0, why are deprecation warnings hidden by default, and how do you surface them with -W:deprecated or Warning[:deprecated]?

level: middleimportance: must knowfreq 45%

basics

~20 s

Since Ruby 2.7.2, deprecation warnings are off by default so end users are not flooded by library internals. Developers turn them on with ruby -W:deprecated, -w, RUBYOPT or Warning[:deprecated] = true, ideally in tests and CI.

open as a page

After moving to Ruby 4.0, why can require "logger" or "ostruct" raise LoadError under Bundler when it worked on 3.3, and what fixes it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Ruby 4.0 turned logger, ostruct, benchmark and others from default gems into bundled gems. Bundled gems ship with Ruby, but under Bundler they load only if the Gemfile or a gemspec lists them; adding them fixes the LoadError.

open as a page

How would you upgrade a Ruby payroll service from 3.1 to 4.0, and which headline changes from 3.2 through 4.0 would you expect to break it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Fix deprecations on 3.1 first, then move through the versions with CI on each: expect 3.2's removed methods, 3.4's bundled bigdecimal and csv plus new Hash#inspect and error-message formats, and 4.0's bundled logger and removed APIs.

open as a page

With Ruby shipping a new minor version every year, how would you set an upgrade cadence and policy for dozens of Ruby services?

level: principalimportance: should knowfreq 20%

basics

~20 s

Plan one upgrade per year per service instead of multi-version jumps: test every service against the new release early, adopt it within a set window after a patch release, and never let a service fall off security maintenance.

open as a page