Skip to content

String literals are chilled, not frozen

Level: 201 · anyone who has read "frozen string literals" in a Ruby changelog

One line: In Ruby 4.0 a string literal is still mutable by default and mutating it prints a deprecation warning only under -W:deprecated, while # frozen_string_literal: true or --enable=frozen-string-literal makes the same mutation a FrozenError; +"...", dup and String.new give strings you may change under every setting.

Ruby 2.3 added the magic comment # frozen_string_literal: true. Ruby 3.4 made every literal in a file without the comment chilled — still mutable, but flagged, so that mutating it emits a deprecation warning when those warnings are on (Feature #20205 ↗). Ruby 4.0 kept it there: literals are not frozen by default. The program below appends to a literal and runs itself five ways.

Verified output of literals_are_chilled_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

$ ruby greet.rb
  greeting.frozen?  false
  after <<          "hello, world"

$ ruby -W:deprecated greet.rb
  greeting.frozen?  false
  after <<          "hello, world"
  stderr: greet.rb:4: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)

$ ruby --enable=frozen-string-literal greet.rb
  greeting.frozen?  true
  FrozenError: can't modify frozen String: "hello"

$ ruby greet.rb    # line 1: # frozen_string_literal: true
  greeting.frozen?  true
  FrozenError: can't modify frozen String: "hello"

$ ruby -W:deprecated greet.rb    # line 1: # frozen_string_literal: false
  greeting.frozen?  false
  after <<          "hello, world"

Strings you are allowed to change, with no warning:
  (+"hello").frozen?            false
  "hello".dup.frozen?           false
  String.new("hello").frozen?   false

Strings that are shared on purpose:
  (-"hello").equal?(-("hel" + "lo"))   true
  :hello.name.equal?(:hello.name)       true
  :hello.to_s.equal?(:hello.to_s)       false

Reading the output

  • By default, nothing is said. frozen? is false, the append works, and there is no warning, because Warning[:deprecated] is off unless you turn it on.
  • Under -W:deprecated the warning names the file and line, and the mutation still happens. The text promises the literal "will be frozen in the future", without naming a version.
  • The two ways to freeze, the magic comment and the command-line flag, make frozen? true and the append a FrozenError.
  • # frozen_string_literal: false opts a file out — no warning even under -W:deprecated.
  • Unfrozen copies: +"hello" (which, since 3.4, returns a copy of a chilled literal instead of the literal itself), dup, String.new.
  • Shared on purpose: -"hello" returns one frozen, deduplicated string, so two different expressions give the same object. Symbol#name returns the same frozen string every time; Symbol#to_s returns a new one, which Ruby 3.4 also started warning about when it is mutated.

What to do

  • Run the test suite with -W:deprecated, or set Warning[:deprecated] = true in the test helper, to find every mutation of a literal now rather than on the release that freezes them.
  • In code you control, put # frozen_string_literal: true on line 1, and build strings from +"" or String.new when you mean to append.

If you are coming from another language

Python and Java strings are immutable, literal or not; a Rust string literal is a &'static str, a borrow of bytes in the binary — the Rust library's &'static str. Perl strings are mutable values with no notion of a frozen literal. (Not machine-checked here.)