In Ruby 4.0, what does Pathname's / operator do, and why does Pathname("uploads") / "/etc/hosts" differ from File.join("uploads", "/etc/hosts")?
answer
- / is an alias of +
- returns a new Pathname
- absolute right side wins
- File.join only glues strings
- core class in 4.0; rmtree still needs require
basics
~20 sPathname#/ is an alias of Pathname#+: it appends a relative fragment and returns a new Pathname, but an absolute right-hand side replaces the left entirely. So Pathname("uploads") / "/etc/hosts" is /etc/hosts, while File.join gives "uploads/etc/hosts".
solid answer
~40 s`Pathname` wraps a path string in an immutable-style object with file-system methods. `/` is an alias of `+`: `Pathname("uploads") / "2026" / "cat.jpg"` returns a new `Pathname`, and leading `..` segments of the right side are resolved lexically against the left without touching the disk. The right side is treated as relative to the left **unless it is absolute**, in which case the result is just the right side, so `Pathname("uploads") / "/etc/hosts"` is `/etc/hosts`. `File.join` does no such interpretation: it concatenates with `/`, collapsing the duplicate separator, giving `"uploads/etc/hosts"`. Pathname methods return Pathnames (`basename`, `dirname`, `children`, `glob`) and it implements `to_path`, so `File.open` accepts it. In Ruby 4.0 `Pathname` is a core class; only `rmtree`, `find` and `Pathname.mktmpdir` still need `require "pathname"`.
code
ruby · 15 linesroot = Pathname("uploads")
thumb = root / "2026" / "09" / "cat_thumb.jpg"
thumb.class # => Pathname
thumb.dirname # => #<Pathname:uploads/2026/09>
thumb.extname # => ".jpg" (a String)
root / "/etc/hosts" # => #<Pathname:/etc/hosts>
File.join("uploads", "/etc/hosts") # => "uploads/etc/hosts"
Pathname("/srv/app/public/img").relative_path_from(Pathname("/srv/app"))
# => #<Pathname:public/img>
require "pathname" # still needed for rmtree, find and Pathname.mktmpdir
(root / "tmp").rmtree if (root / "tmp").directory?go deeper
Recall that Pathname wraps a path, that / joins a segment and returns a new Pathname, and that File.open accepts one.
Explain the absolute-right-side rule, lexical .. resolution, how that differs from File.join, and which methods return Pathnames versus Strings.
Notice when path fragments come from outside and neither File.join nor Pathname#/ confines them, and know which Pathname methods still need a require on 4.0.
Choose a single path representation for a codebase, Pathname or String, and enforce it at boundaries so mixed APIs do not multiply conversions.
## What Pathname is `Pathname` is Ruby's object-oriented path type. A `Pathname` holds a path string and offers the path operations of `File`, `Dir` and `FileTest` as instance methods: `exist?`, `file?`, `read`, `mtime`, `children`, `glob` and more. Create one with `Pathname.new("uploads")` or the `Kernel#Pathname("uploads")` shorthand, which returns its argument unchanged if it is already a `Pathname`. Two properties make it pleasant to use: - **Methods return Pathnames.** `path.dirname`, `path.basename`, `path.children` and `Pathname.glob` give you Pathnames back, so chains keep their type; `extname`, by contrast, returns a `String`. - **It is accepted where a path is expected.** `Pathname` defines `to_path`, so `File.open(pathname)`, `File.read(pathname)` and `Dir.glob` work with it directly. ## The / operator `Pathname#/` is an alias of `Pathname#+`, defined in `pathname_builtin.rb`. It appends a fragment and returns a **new** `Pathname`; the receiver is not modified. ```ruby root = Pathname("uploads") root / "2026" / "cat.jpg" # => #<Pathname:uploads/2026/cat.jpg> Pathname("/usr/lib") / "../bin" # => #<Pathname:/usr/bin> Pathname("uploads") / "/etc/hosts" # => #<Pathname:/etc/hosts> ``` The rules, from the method's rdoc and implementation: 1. The right side is treated as **relative to the left**. 2. If the right side is **absolute**, the result is just the right side — the left is discarded. 3. Leading `..` segments in the right side are resolved **lexically** against the left, without checking the file system. 4. `Pathname#join(*parts)` applies the same rule across several arguments. ## Why File.join behaves differently `File.join` is a string function. It inserts `/` between segments and, where one ends with a separator and the next begins with one, keeps a single separator. It never interprets absoluteness or `..`: | Expression | Result | |---|---| | `File.join("uploads", "/etc/hosts")` | `"uploads/etc/hosts"` | | `Pathname("uploads") / "/etc/hosts"` | `#<Pathname:/etc/hosts>` | | `File.join("/usr/lib", "../bin")` | `"/usr/lib/../bin"` | | `Pathname("/usr/lib") / "../bin"` | `#<Pathname:/usr/bin>` | Neither behaviour is "safe" for a path segment that arrives from outside: one lets an absolute value replace the base, the other keeps `..` segments for the operating system to follow. Confining such input to a directory is a separate check, and a topic of its own. ## Everyday Pathname methods | Method | Returns | Example result | |---|---|---| | `parent` | `Pathname` | `Pathname("a/b/c.jpg").parent` → `a/b` | | `basename` | `Pathname` | `c.jpg` | | `extname` | `String` | `".jpg"` | | `sub_ext(".png")` | `Pathname` | `a/b/c.png` | | `children` | `Array` of `Pathname` | entries without `.` and `..` | | `glob("*.jpg")` | `Array` of `Pathname` | matches under the receiver | | `exist?`, `file?`, `directory?` | `true`/`false` | file-system checks | | `read`, `write`, `mtime` | same as `File.*` | delegated to `File` | Everything in the left column that is a pure path operation (`parent`, `basename`, `sub_ext`, `+`) works on strings only; the rest ask the file system. ## Pathname in Ruby 4.0 Ruby 4.0's NEWS records that **`Pathname` was promoted from a default gem to a core class**. The core part lives in `pathname.c` and `pathname_builtin.rb`, so `Pathname("x")`, `/`, `children`, `glob`, `relative_path_from` and `mkpath` work without any `require`. Three methods stayed in `lib/pathname.rb`, whose comments still say "you need to require 'pathname' to use this method": - `Pathname#rmtree` (recursive delete via `FileUtils.rm_rf`), - `Pathname#find` (recursive walk via `Find.find`), - `Pathname.mktmpdir`. Code that must also run on 3.x should keep `require "pathname"`, which is harmless on 4.0. ## When to choose Pathname - Code that builds and inspects many paths reads better as `config_dir / "app.yml"` than nested `File.join` calls. - `relative_path_from` answers "how do I get from here to there" without string slicing. - For a single join in a hot loop, plain strings avoid allocating extra objects.
- In Ruby 4.0, do you still need require "pathname"?Not for the core: `Pathname(...)`, `/`, `children`, `glob`, `exist?` and `relative_path_from` work out of the box because `Pathname` is now a core class. `Pathname#rmtree`, `Pathname#find` and `Pathname.mktmpdir` still live in `lib/pathname.rb`, so code that calls them keeps the `require`.
- What does Pathname#relative_path_from return, and does it touch the disk?It returns a `Pathname` describing how to reach the receiver from the argument, for example `public/img` from `/srv/app` to `/srv/app/public/img`. It is computed from the strings alone, so both paths must be the same kind (both absolute or both relative), and symlinks are not resolved.
saying these in an interview costs you the question
- Pathname#/ modifies the receiver in place.
- Pathname("uploads") / "/etc/hosts" gives uploads/etc/hosts.
- Pathname#/ checks the file system to resolve .. segments.
- File.open cannot take a Pathname; you must call to_s first.
- In Ruby 4.0 every Pathname method works without require "pathname".