Constants & Freezing
Reviewed & published by Brayan K
A constant in Ruby is any name that begins with a capital letter, meant to hold a value you don't intend to change — and freeze is how you make that promise enforceable.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll learn how constants are named and scoped with ::, why reassignment only warns, and how freeze/frozen? protect against accidental mutation.
What You'll Learn in This Lesson
- How constants are named (capital first letter)
- Why reassigning a constant only warns
- Scoping constants with the :: operator
- freeze and frozen? for immutability
- Why freeze is shallow (only one level deep)
- The mutable-constant gotcha and how to avoid it
1️⃣ Naming Constants & the Reassignment Warning
Capitalise the first letter and you've made a constant. Ruby lets you reassign it, but it prints a warning so a real typo doesn't slip by silently. Treat that warning as an error in your own code.
# A constant is any name that STARTS WITH A CAPITAL LETTER.
# Convention: SCREAMING_SNAKE_CASE for values, CamelCase for classes/modules.
PI = 3.14159
MAX_USERS = 100
GREETING = "Hello"
puts PI
puts MAX_USERS
puts GREETING
# Reassigning a constant WORKS, but Ruby WARNS you:
PI = 3.14
puts PI2️⃣ Scoping Constants with ::
Constants belong to the class or module that defines them. From outside, reach them with the scope-resolution operator ::, as in Config::TIMEOUT. A leading :: (like ::GREETING) jumps straight to the top-level constant, skipping any nested one with the same name.
# Constants live inside the class/module where they're defined.
# Reach into them from outside with the :: scope operator.
module Config
TIMEOUT = 30
HOST = "localhost"
end
class Circle
PI = 3.14159
def initialize(r); @r = r; end
def area; PI * @r * @r; end
end
# Access nested constants with ::
puts Config::TIMEOUT
puts Config::HOST
puts Circle::PI
puts Circle.new(2).area
# Leading :: means the TOP-LEVEL constant
GREETING = "hi from top level"
module Inner
GREETING = "hi from Inner"
def self.both
[GREETING, ::GREETING]
end
end
p Inner.both3️⃣ freeze, frozen?, and the Mutable Gotcha
freeze makes an object immutable; any mutation afterwards raises FrozenError. Symbols, numbers, true, false and nil are always frozen. Use frozen? to check.
# freeze makes an object immutable. frozen? asks if it already is.
name = "Ruby".freeze
puts name.frozen? # => true
begin
name << "!" # try to mutate a frozen string
rescue => e
puts "#{e.class}: #{e.message}"
end
# Symbols, integers, true/false/nil are ALWAYS frozen:
puts :hello.frozen? # => true
puts 42.frozen? # => true
# Freezing an array stops you adding/removing elements:
nums = [1, 2, 3].freeze
begin
nums.push(4)
rescue => e
puts "#{e.class}: #{e.message}"
endTwo traps to remember: freeze is shallow (the container is frozen, the items inside are not), and a constant that points at a mutable object can be changed in place with no warning at all. Freezing the value closes that loophole.
# frozen? is SHALLOW: freezing an array doesn't freeze its elements.
list = ["a", "b"].freeze
puts list.frozen? # => true
list[0] << "X" # the STRING inside is still mutable!
p list # => ["aX", "b"]
# THE MUTABLE-CONSTANT GOTCHA:
# A constant holding a mutable object can be changed IN PLACE,
# which dodges the reassignment warning entirely.
SETTINGS = { retries: 3 }
SETTINGS[:retries] = 99 # no warning — we mutated, didn't reassign
p SETTINGS # => {:retries=>99}
# Fix: freeze the constant so accidental mutation raises.
LIMITS = { max: 10 }.freeze
begin
LIMITS[:max] = 20
rescue => e
puts "#{e.class}: #{e.message}"
endFor whole files, the frozen_string_literal magic comment freezes every string literal automatically — common in real projects and gems.
# Add this "magic comment" as the FIRST line of a file:
# frozen_string_literal: true
a = "hello"
b = "hello"
puts a.frozen? # => true (every string literal is frozen)
puts a.equal?(b) # => true (the same frozen object is reused)🎯 Your Turn
Replace the ___ so the constant is frozen and mutating it raises a FrozenError.
# 🎯 YOUR TURN — fill in the blanks (___) then run: ruby config.rb
DEFAULT_ROLE = "guest".___ # freeze the string constant
puts DEFAULT_ROLE.frozen? # ✅ Expected: true
begin
DEFAULT_ROLE << "!" # try to mutate it (should fail)
rescue => e
puts e.class # ✅ Expected: FrozenError
endCommon Errors (and the fix)
- ❌ "already initialized constant" warning: you assigned the same constant twice. ✅ Fix: assign each constant once; if you truly must change it, you likely want a variable instead.
- ❌ "can't modify frozen String/Array/Hash" (FrozenError): you mutated a frozen object. ✅ Fix: work on a copy with .dup, or don't freeze it if it must change.
- ❌ Thinking freeze is deep: the elements inside a frozen array are still mutable. ✅ Fix: freeze nested objects too, or build from frozen pieces.
- ❌ Mutating a constant in place: SETTINGS[:x] = 1 changes it with no warning. ✅ Fix: freeze the constant's value so accidental edits raise.
Mini-Challenge: A Frozen Config Module
Build a small configuration module whose constants are genuinely immutable. Run with ruby app_config.rb.
# 🎯 MINI-CHALLENGE: a frozen configuration module.
# Save as app_config.rb and run with: ruby app_config.rb
#
# 1. Make a module AppConfig with a frozen VERSION string ("1.0.0")
# and a frozen FEATURES array (%w[login search export]).
# 2. Add AppConfig.summary returning:
# "v1.0.0 with 3 features: login, search, export"
# 3. Prove the constants are frozen, and that pushing to FEATURES raises.
#
# ✅ Expected output:
# v1.0.0 with 3 features: login, search, export
# true
# true
# FrozenError
# your code herePro Tips
- 💡 Freeze config constants: any CONSTANT = [...] or {...} deserves a trailing .freeze.
- 💡 Use the magic comment: start files with # frozen_string_literal: true for free safety and memory savings.
- 💡 Treat warnings as errors: run with ruby -w while learning to surface constant-reassignment mistakes.
📋 Quick Reference — Constants & Freezing
| Concept | Ruby Syntax |
|---|---|
| Define a constant | MAX = 100 |
| Nested constant | Config::TIMEOUT |
| Top-level constant | ::GREETING |
| Make immutable | value.freeze |
| Check immutable | value.frozen? |
| Freeze all literals | # frozen_string_literal: true |
🎉 Lesson Complete!
- ✅ Constants start with a capital letter; use SCREAMING_SNAKE_CASE for values
- ✅ Reassigning a constant only warns — but mutation in place is silent
- ✅ Scope with ::; leading :: means top level
- ✅ freeze makes an object immutable; frozen? checks it
- ✅ freeze is shallow — freeze nested objects yourself
Practice quiz
What makes a name a constant in Ruby?
- Prefixing it with $
- Using the const keyword
- Beginning the name with a capital letter
- Wrapping it in ALL_CAPS quotes
Answer: Beginning the name with a capital letter. Any identifier starting with an uppercase letter is a constant — that's why class names are constants too.
What happens when you reassign an existing constant, like PI = 3.14?
- Ruby prints an 'already initialized constant' warning but allows it
- Ruby raises an error and stops
- It silently changes with no notice
- The program freezes
Answer: Ruby prints an 'already initialized constant' warning but allows it. Constants are constant by convention; reassigning only warns, it doesn't raise.
How do you access a constant TIMEOUT defined inside module Config from outside?
- Config.TIMEOUT
- Config->TIMEOUT
- Config@TIMEOUT
- Config::TIMEOUT
Answer: Config::TIMEOUT. Use the scope-resolution operator ::, as in Config::TIMEOUT.
What does a LEADING :: mean, as in ::GREETING?
- The nearest nested constant
- The top-level (global) constant, skipping nested ones
- A frozen constant
- A syntax error
Answer: The top-level (global) constant, skipping nested ones. A leading :: jumps straight to the top-level constant, bypassing any nested one of the same name.
After name = "Ruby".freeze, what does name << "!" raise?
- FrozenError
- NoMethodError
- ArgumentError
- Nothing — it appends fine
Answer: FrozenError. Mutating a frozen object raises FrozenError (can't modify frozen String).
Which of these is ALWAYS frozen automatically in Ruby?
- Every Array
- Every String
- Symbols, integers, true, false, and nil
- Every Hash
Answer: Symbols, integers, true, false, and nil. Symbols, integers, floats, true, false and nil are frozen by default; arrays/strings are not.
freeze is described as 'shallow'. What does that mean?
- It only works on small objects
- Freezing a container doesn't freeze the objects inside it
- It freezes only the first character of a string
- It can be undone with thaw
Answer: Freezing a container doesn't freeze the objects inside it. Freezing an array stops push/pop, but its elements stay mutable unless you freeze them too.
Why is SETTINGS = { retries: 3 }; SETTINGS[:retries] = 99 a dangerous 'gotcha'?
- It raises FrozenError
- It deletes the constant
- It prints two warnings
- It mutates the constant's value in place with NO warning at all
Answer: It mutates the constant's value in place with NO warning at all. Mutating (not reassigning) a constant's object dodges the warning entirely — freeze the value to prevent it.
What does the # frozen_string_literal: true magic comment do?
- Freezes every variable in the file
- Makes every string literal in that file frozen by default
- Disables warnings
- Speeds up file loading only
Answer: Makes every string literal in that file frozen by default. Placed on line one, it freezes all string literals in the file, saving memory and catching mutation.
Which method checks whether an object is already frozen?
- object.freeze?
- object.is_frozen
- object.frozen?
- frozen(object)
Answer: object.frozen?. frozen? returns true if the object has been frozen.
Continue this course
- Previous: self, Class Methods & Class Variables
- Next: Pattern Matching (case/in) — Deconstruct arrays and hashes with case/in, guards, and find patterns
- Quick reference: Ruby cheat sheet
Frequently asked questions
What makes a name a constant in Ruby?
Capitalisation, nothing more. Any identifier that begins with an uppercase letter is a constant — that's why class and module names (which start with a capital) are themselves constants. The community convention is SCREAMING_SNAKE_CASE for fixed values like MAX_RETRIES and CamelCase for classes and modules. Lowercase names are local variables, instance/class variables use @/@@, and globals use $.
If constants can be reassigned, are they really constant?
Ruby constants are 'constant by convention, enforced by a warning'. You CAN reassign one, but Ruby prints 'warning: already initialized constant' to remind you it's probably a mistake. More importantly, reassignment is different from mutation: a constant pointing at a mutable object (an Array or Hash) can be changed in place with no warning at all. To get real immutability you freeze the object the constant refers to.
What does freeze do, and is it deep?
freeze flips an object's frozen flag so any attempt to mutate it raises a FrozenError. It is shallow: freezing an Array prevents push/pop on the array, but the elements inside are not frozen, so a mutable string element can still be changed. Symbols, integers, floats, true, false and nil are frozen automatically. For deep immutability you must freeze each nested object yourself, or build the structure from already-frozen pieces.
What is the frozen_string_literal magic comment?
Adding '# frozen_string_literal: true' as the first line of a file makes every string literal in that file frozen by default. This saves memory (identical literals share one object) and catches accidental mutation early. It is widely used in production Ruby and gems. If you genuinely need a mutable string in such a file, create one explicitly with String.new or +"".dup.