require loads once; load runs every time¶
Level: 201 · a Python programmer who knows import and wants Ruby's four loading verbs sorted out
One line: require and require_relative run a file once and remember it in $LOADED_FEATURES; load runs a file every time you ask; autoload postpones the load until a constant is first referenced — and import is the first of these, with importlib.reload and runpy.run_path standing in for the others.
Ruby has no import. A file is brought in with require "name", which searches the directories in $LOAD_PATH for name.rb (or a compiled name.so/name.bundle), runs it once, appends its full path to $LOADED_FEATURES, and returns true; asking again returns false and runs nothing. require_relative "lib/helper" is the same, except the name is resolved against the folder of the file that contains the call rather than against $LOAD_PATH or the working directory — which is why every non-gem project file uses it. load "file.rb" is the odd one out: it wants a real path (or a name on $LOAD_PATH), it runs the file every time, and it does not consult the once-list. autoload :Const, path registers a promise: the file is loaded the first time Const is referenced. A require that finds nothing raises LoadError with the message cannot load such file -- name. The program below writes a small tree of files into a temporary folder and exercises each verb, printing a line from inside each file whenever its body runs, so the count of those lines is the evidence.
Verified output of require_require_relative_and_load_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. require "json" twice first: true second: false
2. $LOADED_FEATURES (the once-list) grew: true holds json.rb: true
3. load main_part.rb, which require_relatives lib/helper against its own folder:
(helper body ran, from lib)
main_part sees Helper.hi = hi
4. load main_part.rb again main_part runs; helper is remembered:
main_part sees Helper.hi = hi
5. load lib/helper.rb twice load runs the file every time:
(helper body ran, from lib)
(helper body ran, from lib)
6. $LOAD_PATH is Array; after unshift(lib) require "helper2" (no extension):
(helper2 body ran)
6. found helper2.rb: true again: false
7. require "nope" LoadError: cannot load such file -- nope (e.path: "nope")
8. autoload :Greeter, path registered: true const_defined?: true body ran yet: no
8. first reference to Greeter loads it:
(greeter body ran)
8. Greeter is Class Greeter autoload? now nil: true
Reading the output¶
Row 1 is the whole contract of require: the first call answers true, the second false, and row 2 shows the memory it uses — $LOADED_FEATURES grew and now holds a path ending in /json.rb. Rows 3 and 4 load main_part.rb twice with load; main_part itself runs both times (its "sees Helper.hi" line prints twice), but the require_relative inside it runs helper.rb only once, because require_relative uses the same once-list as require. Row 5 is load on helper.rb itself: two calls, two "helper body ran" lines. Row 6 puts the lib folder at the front of $LOAD_PATH (an ordinary Array, also spelled $:) and then require "helper2" finds helper2.rb with no extension in the name — and a second require answers false. Row 7 is the error you will see most: LoadError, whose message names the feature and whose path method returns it. Row 8 is autoload: right after registering, Object.const_defined?(:Greeter) already answers true and no "greeter body ran" line has printed; the first reference to Greeter prints it, and afterwards autoload? returns nil because the promise has been kept.
Compared with Python¶
Python's import is Ruby's require: the module is executed once and cached in sys.modules, and every later import hands back the same object (row 1 prints same object: True). Search happens along sys.path, a plain list you may edit like $LOAD_PATH (row 6), and import helper2 finds helper2.py with no extension. The differences are in the other three verbs. Python's relative import (from . import helper) only works inside a package — a folder with an __init__.py — and it is a relative module name, not a relative file path; a plain script cannot say "the file next to me" the way require_relative can, which is why the twin builds a pkg/ package in row 3. (Not machine-checked here.) There is no load; the twin uses two stand-ins in row 5, importlib.reload(module) (re-executes an imported module in place) and runpy.run_path(file) (runs a file as a script and returns its globals). And there is no autoload; the closest built-in is importlib.util.LazyLoader, which registers the module immediately and defers executing its body until the first attribute access — row 8 shows the "greeter body ran" line appearing only then. Row 4 is a real difference in behaviour: importing pkg.main_part a second time runs nothing, because sys.modules remembers the whole module, whereas Ruby's load re-ran main_part.rb and only its inner require_relative was remembered. The missing-module error is ModuleNotFoundError (a subclass of ImportError), whose name attribute is the twin of LoadError#path.
Verified output of require_require_relative_and_load_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. import json twice first: loads (was cached: False) second: cached (same object: True)
2. sys.modules (the once-dict) grew: True holds json: True
3. import pkg.main_part, which does from . import helper inside the package:
(helper body ran, from pkg)
main_part sees helper.hi() = hi
4. import pkg.main_part again nothing runs; both are remembered:
4. (same module object: True)
5. reload(helper); run_path(helper.py) each runs the body again:
(helper body ran, from pkg)
(helper body ran, from pkg)
6. sys.path is a list; after insert(0, d) import helper2 (no extension):
(helper2 body ran)
6. found helper2.py: True again: cached (True)
7. import nope ModuleNotFoundError: No module named 'nope' (e.name: 'nope')
8. importlib.util.LazyLoader for greeter registered: True body ran yet: no
8. first attribute access loads it:
(greeter body ran)
8. Greeter is type Greeter
Try it¶
- Write your own
once_load(path): it should returntruethe first time a given file is loaded through it andfalseafterwards, keeping its own list of loaded paths — and then show thatloadon the same file ignores that list. Have the loaded file count how many times it ran. - Create
lib/a.rbcontainingrequire_relative "b"andlib/b.rbnext to it, thenDir.chdirsomewhere unrelated before loadinglib/a.rbby its absolute path. Print whetherb.rbwas found. Then try the same withrequire "b"insidea.rb(no$LOAD_PATHchange) and print the exception class.
Solution to 1
require_require_relative_and_load_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: a require of your own. `once_load(path)` returns true the first time a
# file is run through it and false afterwards, like `require`; `load` ignores
# that memory, so it runs the file every time.
require "tmpdir"
ONCE = []
def once_load(path)
path = File.expand_path(path)
return false if ONCE.include?(path)
ONCE << path
load path
true
end
Dir.mktmpdir do |dir|
file = File.join(dir, "counter.rb")
File.write(file, "$runs = ($runs || 0) + 1\nputs \" counter.rb ran (run \#{$runs})\"\n")
puts "1. once_load first time:"
puts " -> #{once_load(file)}"
puts "2. once_load second time:"
puts " -> #{once_load(file)}"
puts "3. load, twice, ignores the once-list:"
load file
load file
puts "4. runs in total: #{$runs} remembered by once_load: #{ONCE.size} file(s)"
puts "5. real require agrees on a real feature: #{require "json"} then #{require "json"}"
end
Verified output of require_require_relative_and_load_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Gems, Bundler and the Gemfile — where
$LOAD_PATHentries come from when a gem is installed - The command-line flags —
-r namerequires before the program starts and-I dirprepends to$LOAD_PATH - Modules as namespaces — what a required file usually defines, and why a Ruby module is not a file
- Common error messages —
LoadErroramong the errors a reader meets in the first week - Constants and lexical scope —
autoloadhooks into constant lookup, the same lookup this page explains - Python:
pyproject.toml↗ — how a Python project declares what itsimportlines will find Pathname#/joins, andDir.globis sorted —Pathnameis an object with/,basename,extname,parent,children,exist?andread;…- Twenty-three Ruby traps for a Python programmer — The twenty-three places where a Python reflex gives the wrong answer in Ruby
- There is more than one way, and one obvious way — Ruby's idioms