Variable Scope and Access in Ruby: The Important Parts

Ethan Weiner
13 min readFeb 4, 2022

--

Perhaps more than any other subject matter, I’ve had to refine my previous assumptions and mental models on the topic of scope. Upon completing Launch School’s RB120 course, I’ve gained a more holistic view of scope and its importance.

Please note that this article is not an introduction to scope. I think it’s worthwhile to first develop an intuition of scope through practice and experimentation before reworking that intuition into a bigger-picture mental model.

In this article, we will focus on the concept of scope, and touch upon its related counterpart: name resolution. These concepts can be analyzed at many levels of detail; I will attempt to provide a fairly detailed mental model of scope and cover how it applies to different variable types. This article is appropriate for those who want to clarify or deepen their understanding of scope fundamentals. Readers of this article might also be introduced to some useful terminology they are yet to encounter.

Despite spending a considerable amount of time pondering these topics, I am by no means a scope expert. Therefore, I do not strive for perfect accuracy in this article nor dive into the much lower-level details of scope and variable access. Rather, this article is an accumulated set of mental models based on Launch School’s key points on scope, forums, discussions with others, a variety of articles, and my own perspectives and ideas.

One final thing to note, for those in RB130: this article does not go into any detail about the scoping rules of closures. Perhaps I will post a follow-up article on this.

What is Variable Scope? How about Name Resolution?

Scope refers to visibility: what things are visible where in your program. In practice, the term “scope” tends to be used in two different contexts:

  1. What is the “scope” of a variable? Where is a given variable accessible in a program?
  2. What is “in scope” at a particular point in a program? What are the visible variables, methods, and classes currently accessible?

In a way, these two interpretations are inverses. If a variable is “in scope” at some location, the scope of the variable must somehow include that location. Both meanings of scope will be used throughout this article. Throughout most of the article, we will focus more on the perspective offered by the first interpretation of scope, and end with the second interpretation.

Name resolution (closely related to “lookup”) is the process by which some reference to a “name” in your code (such as a variable name) is linked to an actual variable and its value. Just like how different variable types have differing scoping rules, references to these variables may resolve in differing ways. Note that name resolution is highly dependent on scope, but the two terms are by no means synonymous.

The resolution process for a given variable name primarily depends on two things:

  1. What is in scope (can an accessible variable be found?)
  2. When the resolution occurs

I won’t go into too much detail about the when aspect of name resolution because it’s beyond what the majority of Rubyists need to understand. However, it will be relevant when we arrive at the discussion of constant resolution, which differs notably from other variables. Hang tight.

Scope and Name Resolution: A Visualization

Before diving any deeper, let’s walk through a visualization to build a clearer picture of the two interpretations of scope, along with name resolution.

Imagine a set of bubbles, each of which represents a “scope”. Some overlap, some are enclosed by others, some are fully isolated. Each bubble may contain some variables (local, instance, class, etc.). Don’t think of the bubbles in terms of the positional structure of your code — while the structure of your code might correspond to the bubbles, it's more accurate to think of them in terms of environments: methods, class/modules definitions, objects, blocks, etc.

Now consider yourself to be the running program. As you move along, you enter a variety of method invocations, class definitions, and other environments, transitioning from one bubble to another and changing what variables are currently in your scope. The objects, methods, classes, modules, and/or blocks within which you operate at any point define your execution context: a broad term describing the circumstances that exist at a given moment in your so-called “runtime”.

In Ruby, the scope-related Binding object describes this context precisely. Within any given context, you (the program) have a binding object that signifies the currently accessible variables and method. Later, we will discuss how the binding changes throughout the program’s execution, but for now, understand that the binding tracks the current:

  • Available local variables
  • Receiver or “calling object” (the value of self)

Since the binding tracks this so-called receiver, it implicitly gives us access to the instance and class variables on that receiver — thus determining the instance and class variables in scope.

Try to guess what’s output by the representative example below. No worries if you can’t yet — by the end of this article, you will likely be able to. Disclaimer: the bubbles diagram is not an illustration for the below code snippet.

Say you want to retrieve a variable, and request for local variable a from inside your bubble(s). Here’s where name resolution comes in. Typically, referencing the name will try to find the corresponding variable in those bubble(s).

Constants don’t quite fit this analogy, because all their scope and resolution work is done before the first bubble is even entered — before program execution even occurs.

Photo by Marc Sendra Martorell on Unsplash

Why Does Scope Matter?

To understand why scope matters, consider a program’s experience without scope or resolution rules: everything would be accessible everywhere. Programs and their variables would operate in one giant bubble. This might not be a problem for minuscule scripts, but for anything of substantial size, this has tremendous implications on:

  1. Naming: Without scope, we’d have to name everything uniquely, or else Ruby wouldn’t know which variable/method/class to resolve to. With scope, variables of the same name can exist across a program without experiencing name collisions.
  2. Data Protection and Security: Scope restricts accidental or intentional access of variables in different parts of a program.
  3. Object-Oriented Programming: Scope is foundational to OOP’s encapsulating wonders. Without scope boundaries, encapsulation would be impossible. In fact, OOP introduces variable types (instance and class variables) with specific scoping rules designed to encapsulate state. For those in RB120: if the primary mechanism for encapsulating methods is method access control, we can think of scope as variable access control.

Aside from scope’s importance in programs, understanding the foundations of scope will give you more control and precision over code you write, and more clarity over code you mess up.

How does Ruby Set a Variable’s Scope?

We know that variables have scopes, but how do variables get their scopes in the first place? I’ve come across different semantics surrounding Ruby’s scoping behaviors. Regardless of specific terminology, I have deduced three ways that the scope of a variable is determined:

  1. The variable’s type: is it a local_variable, @instance_variable, @@class_variable, CONSTANT, or $global_variable?
  2. The execution context (binding) of the program when the variable is initialized: you (the program) can add new variables into the bubbles you currently reside in. Local, class and instance variables use this criterion to determine their scope.
  3. The mere structure of the code surrounding the variable where it is initialized. This sounds similar to the previous point but is subtly different. This type of scope depends solely on location. The program doesn’t even need to run to evaluate variables’ scope like this; it can be figured out beforehand. A variable scoped in this way is said to have pure lexical scope. Constants use this criterion to determine their scope.

So, what bubble (scope) is a given variable part of? It all depends on its variable type and its context upon initialization.

Furthermore, once a variable’s scope is set in Ruby, it doesn’t change. Variables can’t migrate bubbles. This is called “static scope”, as opposed to “dynamic scope”, which is rarely used in modern programming languages.

Scoping and Resolution: From the Variable’s Perspective

Now we will take a deeper dive into the particular rules governing each variable type’s scope, and how those variables are accessed when they’re needed.

Local Variables

  • Scope: The class/module definition, method, or block in which it is initialized; local variables have the most narrow scope out of all variable types.
  • Resolution: Given a name with all lowercase characters, the current binding is searched for both local variables and methods (think binding.local_variables).

Note that the scope of a local variable initialized in a method extends to blocks that the method calls, but the scope of a local variable initialized in a class, module, or at the top level does not extend to nested classes/modules/methods.

Examples:

Four different local_variables exist, each with a separate scope. Note the tightness of the scopes; for example, the local_variable initialized in module A is inaccessible from class B.

The scope of local_variable1 is anywhere in the method invocation, including the block passed to each. local_variable2 is scoped only to the block.

Instance Variables

  • Scope: In Ruby, all code executes in the context of some calling object, otherwise known as the “receiver”. Every method call has some receiver: the receiver of an instance method is either explicitly defined: receiver.method; otherwise, it is implied — the receiver of method without anything prepended is self. The point is that when any method is invoked, a specific calling object is executing. If an instance variable is initialized under an object’s execution, it is scoped to that object.
  • Resolution: A name prepended with @ signals Ruby to search for an instance variable in the scope of the currently executing object: which can be thought of as self or binding.receiver.

Examples:

self stores the currently executing object. In this case, it is the main object (the top-level object in Ruby). Therefore the scope of @instance_variable is the main object. instance_method is defined on main so, the calling object is still main in instance_method, making @instance_variable accessible.

Upon the call to print_name on line 12, @name is not yet in the scope of person because it is yet to be initialized. In Ruby, referencing an uninitialized instance variable produces nil.

Upon construction of each Person object, two separate instance variables @name are scoped to their respective objects.

Despite it occupying a module, Nameable#print_name is called on the object person, so the invocation of Nameable#print_name has access to person instance variables, including @name.

Person::print_name is invoked on the class Person, so the scope inside this method invocation is the Person class. @name is scoped to person, not Person.

Class Variables

  • Scope: Class variables can be thought of as global variables in the context of classes. A class variable initialized anywhere in a class definition has a scope that includes the class in which it’s defined, any instances of that class, any subclasses of that class, and any instances of those subclasses — a pretty wide scope indeed.
  • Resolution: A name prepended with @@ will search for a class variable in class-level scope. Because of their broad scoping rules, only one shared copy of a class variable with some name can exist among a class and all its subclasses. Therefore, if @@class_variable is initialized in a superclass, references to it in any subclasses will resolve to the same variable.

Even though Ruby scans (parses) a class before runtime, the actual execution of the class definitions and creation of class variables happens during runtime — meaning that class variables are dynamic entities during execution.

Examples:

Despite being initialized inside an object, @@type is scoped to the Person class, and is accessible anywhere in Person or subclasses of Person.

Note that class variables are definable and accessible at both the class-level and object-level.

The Runner class definition on lines 9–11 overrides the value of the single class variable @@type scoped to Person and Runner. This wacky behavior dissuades Rubyists from using class variables with inheritance, utilizing instance variables or constants instead.

Constants (Constant Variables)

Constants are where things get fun. They might seem unimportant in comparison to other variable types until you learn that module and class names are themselves constants — and module and class names are referenced all the time. Since constants are intended to be static entities that don’t change during runtime, Ruby assigns them some special scoping and resolution rules.

  • Scope: Technically, constants are accessible anywhere. Paradoxically, this does not mean their scope is everywhere. A constant’s scope only consists of the lexical (positional) scope in which it is declared — the enclosing module or class definition. Constants are accessible from outside their scope using a namespacing resolution technique discussed below.
  • Resolution: Unlike other variables, constants are resolved before runtime, before any notion of an “execution context”. This might seem strange, but Ruby achieves this with a step-by-step procedure after scanning for constant references (any name beginning with an uppercase letter). For each reference to a constant that’s needed at runtime, the following sequence of steps takes place:
  1. The immediate lexical scope surrounding the name is searched, starting at the innermost lexical scope moving outwards (excluding the top-level, which is not considered part of the lexical scope).
  2. The inheritance hierarchy of the innermost currently open class/module is searched.
  3. The top-level is searched (any constants defined outside of a class/method definition, at the “top-level”).

Note that these steps serve as a conceptual model of constant lookup; the actual process adds in many more technicalities and quirks. The main idea is that constant resolution must use a completely different set of criteria from other variable types because of when it occurs.

Finally, for constant names prepended with a namespace (namespace::CONSTANT), the constant resolution process is first redirected to the namespace (class/module definition)— as if the constant name itself sits inside of it.

Examples:

Upon the reference to CONSTANT on line 9, Ruby searches the immediate lexical scope, the Person class, and then the next enclosing lexical scope, the Earth module, where Earth::CONSTANT is found.

To really get a feel for how constants are different, uncomment line 8: an error message about “dynamic constant assignment” is output. Methods are executed at runtime, but Ruby wants to evaluate all constants before runtime — and can’t do this if a constant is assigned a value in a method at runtime. Assigning constants in class/module definitions is fine because these definitions are parsed before runtime.

Ruby deduces that Constantable is an ancestor of Person. Therefore, Constantable::CONSTANT is spotted in the inheritance hierarchy of the enclosing class Person.

Even though the scope of the print_constant invocation is the Person object during execution, the constant resolution process doesn’t care, because constants’ values are determined before execution. On line 3, no CONSTANT is found in the lexical scope, nor the inheritance hierarchy of the enclosing module,Constantable, nor the top level.

The namespacing operator :: on line 3 redirects Ruby to a different place (in this case, the class of self) to lookup CONSTANT. Additionally, unlike class variables, the scope of a constant initialized in a class does not extend to its subclasses. As such, separate constants with the same name can separately exist in a single inheritance hierarchy, which is generally a good thing.

Fully understanding constant lookup is far beyond the needs of most developers, but having a general sense of it is useful.

Global Variables

I won’t discuss global variables in detail here because using them is widely considered malpractice, and their scoping rules are pretty simple: the scope of a global variable is the entire program.

Scoping and Name Resolution: From the Program’s Perspective

Going back to our two interpretations of the term “scope”, we will now take a moment to look at the second interpretation. Since the two interpretations are inverses (in a way), this section will provide an alternate perspective and serve as a mental reinforcement of the scoping rules defined in the last section. We will be focusing on answering the following question: how do variables become in scope during program execution?

Note that this section will ignore constants specifically. It doesn’t make sense to ask whether a constant is “in scope at a certain point during program execution” — because they are resolved before program execution!

The simple answer to the posed question is that the value of the current binding object tells us what’s in scope at any moment in time. Of course, what’s in this fluctuating binding depends on so many things — all the rules we discussed in the previous section!

So, how do you (the program) change what variables are visible to you? The short answer: you move around to different bubbles, simply by doing things. Out of all the things that could help you transition scopes, I’ve categorized them into two departments:

  1. Scope gates: Keywords that delineate a new scope boundary for local variables only
  2. The currently executing object (the “calling object”): Value of self at a given point

Scope Gates

Ruby has a set of keywords that serve as [local variable] scope boundaries: module, class, def, and end. The first three signal Ruby to exit the current scope and enter a new “inner” scope, while end tells Ruby to exit the “inner” scope and enter the immediate “outer” scope. These keywords are known as scope gates, and understanding them is quite simple. Picture some bubbles as having strictly defined surfaces: these surfaces are scope gates.

The above example shows that we can figure out what local variables are available at any point in your program by analyzing scope gates. Doing this might be overboard in practice, but it helps discern the true nature of local variable accessibility.

The Calling Object

Secondly, the currently executing object, self, strongly relates to the accessible instance and class variables. To provide a rough approximation, all instance variables defined on the current self are accessible, and all class variables defined on self or the class/superclasses of self are accessible.

Ruby provides us with two handy methods to see this behavior in action: Object#instance_variables and Module#class_variables.

The same rules apply from before, just presented inversely.

In Conclusion: Scope is Complicated

That dive may have felt a bit too deep for our purposes here at Launch School. And if we wanted to, we could dive 300 meters deeper. But as a programmer, this mental model, combined with your own ideas, will help you gain more confidence and clarity on how scoping rules arise from what may have seemed like nothingness. You now see how scope relates to its counterpart, name resolution. You might not fully understand these mental models and rules after one, two, or [insert finite number here] reads, so use this article as a reference for when you need it.

If you found anything inaccurate or unclear in this article, feel free to let me know by commenting or messaging me on Slack (@Ethan Weiner).

I’d like to shout out a few individuals (by way of Slack names) who helped inspire and critique this article. My countless study sessions with @Ryan DeJonghe helped me form mental models that backed this article, and his detailed feedback on the article was paramount to its final version. I had a helpful discussion with @Fred Durham about the two interpretations of scope, and he also provided me with some helpful insights about my article — and about many other things too. Last but not least, discussions back in RB101 with @Joel (Joel Barton) about the true nature of scope and his near professional-level feedback on my article both went a long way into making this article whole. And of course, thank you Launch School.

--

--

Ethan Weiner

Full stack software engineer - interested in learning, web development, and psychology