What does frozen_string_literal do in Ruby?

What Does the Frozen String Literal Comment Do in Ruby?

Frozen strings not only prevent unintended modifications, but also reduce the overhead of the garbage collector by eliminating unnecessary memory allocations, thus improving application performance. This post explains the concept of freezing along with the magic comment frozen_string_literal.

7 min read

When working with Ruby programs, you must have come across the following comment at the top of the file:

# frozen_string_literal: true

print "Let's learn Ruby on Rails"

# frozen_string_literal: true is a magic comment in Ruby, which tells the Ruby interpreter that all the string literals must be frozen and memory should be allocated only once for each string literal.

💡
What's a magic comment?

A magic comment is a special type of comment in Ruby that's defined at the top of the Ruby script. It affects how the Ruby code is executed. You can think of them as "directives" or "instructions" for the Ruby interpreter.

Magic comments must always be in the first comment section in the file. Their scope is limited only to the Ruby script in which they appear. You must put the magic comment in each file where you want the special behavior.

That was easy. Just add the magic comment at the top, and all string literals will be frozen. But what does it mean? Why do you need to freeze a string in the first place? What are the benefits? What are the costs?

Let's learn more.

What does it mean to freeze a String?

It's funny, I've been programming Ruby for the past two years and I had no idea what freezing did (or not do) in Ruby. So while researching this post, I decided to really learn it. Here is everything I know about freezing, so far.

a river running through a snow covered forest

So it turns out, a constant is not really a constant in Ruby...

LANGUAGE = "Ruby"

LANGUAGE << " on Rails"

puts LANGUAGE  # Ruby on Rails .. Whaaaat???

... unless you freeze it, by calling the freeze method on the object.

LANGUAGE = "Ruby".freeze

LANGUAGE << " on Rails"

puts LANGUAGE  # can't modify frozen String: "Ruby" (FrozenError) ... whewww

Calling freeze turns the string into a real constant. And if you try to modify it, Ruby will raise the FrozenError. Does that make sense? To prevent modifications to a string, we had to call the freeze method on it.

Let's read up the docs on the freeze method.

💡
Object.freeze

Prevents further modifications to obj.

FrozenError will be raised if modification is attempted.

There is no way to unfreeze a frozen object.

Calling freeze makes an object immutable, that is, its internal state cannot be changed. That means when a string is frozen, you cannot use the bang! version of methods on it, which modify the original strings.

framework = "Ruby on Rails".freeze

puts framework.upcase   # RUBY ON RAILS
puts framework.upcase!  # can't modify frozen String: "Ruby on Rails" (FrozenError)

Not only the freeze method works with a string, it also work on Arrays and Hashes (and a few other type of objects, but let's ignore them for now).

languages = [ "Rub", "C-Sharp" ].freeze

# can't modify frozen Array: ["Rub", "C-Sharp"] (FrozenError)
languages << "Java"


rails = {
  language: "Ruby",
  creator: "DHH"
}.freeze

# the `update` method modifies self, like merge! 

# can't modify frozen Hash: {:language=>"Ruby", :creator=>"DHH"}
rails.update(website: "https://rubyonrails.org/")

So one thing is clear. Freezing an object literally freezes it, you cannot modify it.

To check if an object is frozen or not, simply call the frozen? method on it.

language = "Ruby"
p language.frozen?  # false

framework = "Ruby on Rails".freeze
p framework.frozen?  # true

The next question you might have (I certainly did): Can we unfreeze an object?

The answer is NO. You can not unfreeze an object that is already frozen.

However, you can make a copy of the frozen object which will be unfrozen.

framework = "Ruby on Rails".freeze
p framework.frozen?  # true

new_framework = framework.dup
p new_framework.frozen?  # false

To unfreeze a string, you can also use the +string, which returns the same string if it's not frozen. Otherwise, it calls the dup method on string and returns a new string which is not frozen.

framework = "Ruby on Rails".freeze
p framework.frozen?  # true

new_framework = +framework
p new_framework.frozen?  # false

# By the way, did I mention the `-` operator which freezes a string?
# I think it's too much syntactic sugar, though. What do you think?

framework = -"Ruby on Rails"

puts framework.upcase!  # can't modify frozen String: "Ruby on Rails" (FrozenError)

Alright, so far we have learned that frozen objects cannot be modified.

The obvious use for freezing is to prevent unintended modifications to sensitive objects (and also to prevent the number of object allocations, which we'll learn later). However, it can be cumbersome to having to remember and call the freeze method every time you want to create an unmodifiable object.

Wouldn't it be nice if there was a way to do it automatically? It would.

The frozen_string_literal allows you to freeze all string literals by default, in a Ruby script.

💡
In fact, Matz (Ruby’s creator) decided to make all String literals frozen by default in Ruby 3.0. However, this was later reverted.

Update: Last month, Matz accepted the proposal to have the frozen_string_literal: true enabled by default.

"I agree with the proposal. It seems a well-thought process to migrate. The performance improvement was not as great as I had hoped for. But since I feel that the style of individually freezing strings when setting them to constants is not beautiful, and since I feel that magic comment is not a good style. I feel that making string literals frozen is the right direction to go in the long run." - Matz

Let's rewrite the same code above, with the magic comment in place.

# frozen_string_literal: true

framework = "Ruby on Rails"
p framework.frozen?  # true

As you can see, the string was frozen without us having to call the freeze method on it. However, it won't freeze other types of objects, such as arrays and hashes. It will only freeze strings.

# frozen_string_literal: true

framework = "Ruby on Rails"
p framework.frozen?  # true

lanugages = [ "Ruby", "Java" ]
p lanugages.frozen?  # false

h = { language: "Ruby" }
p h.frozen?  # false

So far, we've learned that the magic comment frozen_string_literal: true will freeze the strings and prevent them from modifications. That's nice.

However, there's another important reason for freezing strings, which has to do with improving the performance of your application, by avoiding the number of objects the Ruby interpreter has to create and allocate memory for.

Let's dig in to learn more.

Reducing Memory Allocations for String Literals

First, let's take a detour to understand the Object#object_id method. When called on an object, it returns a number.

puts "Ruby".object_id  # 60
puts ["Python", "Java"].object_id  # 80

However, it's not any ordinary number. No two active objects will share an ID. To keep things simple, you can think of object_id as that object's location in memory (if you're a Ruby expert, please correct me if this mental model is wrong).

If you call object_id multiple times on the same object, you will get the same number. However, calling object_id on different objects will return different numbers.

ruby_one = { name: "Ruby", creator: "Matz" }

puts ruby_one.object_id  # 60
puts ruby_one.object_id  # 60

ruby_two = { name: "Ruby", creator: "Matz" }

puts ruby_two.object_id  # 80
puts ruby_two.object_id  # 80

Even though the object structure was same, the Ruby interpreter created a new object for ruby_two and allocated new memory to it.

I hope that by now you understand how object_id works. Now let's get back to strings.

Consider the default behavior of Ruby strings, when they are not frozen:

puts "Ruby on Rails".object_id    # 60
puts "Ruby on Rails".object_id    # 80
puts "Ruby on Rails".object_id    # 100

Note that the object ids of three strings are different. What that means is the Ruby interpreter created a new string object whenever it came across the string "Ruby on Rails", even though it's one string.

Now imagine that you're using the same string literal hundreds or thousands of times in a loop. The interpreter will create a new object and allocate memory to it in each iteration of the loop.

5.times do
  puts "Ruby on Rails".object_id
end

# Output
60
80
100
120
140

That's a lot of overhead. It is quite inefficient.

Why? Because Ruby is a garbage-collected language.

The more memory is allocated for all these objects, the more work the garbage collector needs to do. Which sucks, as all the time Ruby interpreter spends collecting the garbage (which means "reclaiming the allocated memory"), it cannot spend executing your code.

Let's run the same code again, this time with the magic comment.

# frozen_string_literal: true

puts "Ruby on Rails".object_id    # 60
puts "Ruby on Rails".object_id    # 60
puts "Ruby on Rails".object_id    # 60

5.times do
  puts "Ruby on Rails".object_id
end

# Output
60
60
60
60
60
60
60
60

As you can see, when the string literal "Ruby on Rails" was frozen (because we used the magic comment), the Ruby interpreter did not create new objects for the same string. That means, the same object was reused, saving the space for two additional strings. Hence the garbage collector's work was significantly reduced.

💡
By freezing strings, the Ruby interpreter does not allocate new memory for the same string. It reduces the work of the garbage collector, since there's less garbage (memory) to clean.

That's a big reason you see the magic comment # frozen_string_literal: true in most Ruby projects. For example, the Ruby on Rails project uses it almost everywhere.

Let's test this by running some numbers with the benchmark-ips gem, which benchmarks a given blocks number of iterations per second.

require 'benchmark/ips'

def learn(obj)
end

frozen_rails = "Ruby on Rails".freeze

Benchmark.ips do |x|
  x.report("plain") { learn "Ruby on Rails" }
  x.report("frozen") { learn frozen_rails }
end


# Output:

ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-darwin21]
Warming up --------------------------------------
               plain     1.235M i/100ms
              frozen     1.972M i/100ms
Calculating -------------------------------------
               plain     12.335M (± 5.2%) i/s -     61.774M in   5.022481s
              frozen     20.203M (± 4.5%) i/s -    102.564M in   5.087328s

As you can see, the frozen version of the block is significantly faster (102.6 million iterations per second) than the plain one (61.7 million iterations per second). No wonder most Ruby projects freeze strings everywhere.

Note: It's important to remember that the scope of # frozen_string_literal: true is limited only to the current file. You must put it in each file where you want the intended behavior.

I wish there was some sort of global switch that turned it on everywhere. Although, there is a –enable frozen-string-literal flag that you can use while running Ruby scripts, which is kind of nice.

That's it for today.


That's a wrap. I hope you found this article helpful and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.