Skip to content

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

  1. Write your own once_load(path): it should return true the first time a given file is loaded through it and false afterwards, keeping its own list of loaded paths — and then show that load on the same file ignores that list. Have the loaded file count how many times it ran.
  2. Create lib/a.rb containing require_relative "b" and lib/b.rb next to it, then Dir.chdir somewhere unrelated before loading lib/a.rb by its absolute path. Print whether b.rb was found. Then try the same with require "b" inside a.rb (no $LOAD_PATH change) 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.

1. once_load first time:
    counter.rb ran (run 1)
   -> true
2. once_load second time:
   -> false
3. load, twice, ignores the once-list:
    counter.rb ran (run 2)
    counter.rb ran (run 3)
4. runs in total: 3   remembered by once_load: 1 file(s)
5. real require agrees on a real feature: true then false

See also