How does pathlib.Path use the / operator to join paths, and what do .name, .stem and .parent return?
answer
- Paths are objects, not strings
- One operator replaces os.path.join
- Division builds, properties take apart
- name minus one suffix is stem
- parent drops the last component
basics
~10 spathlib.Path overloads the division operator, so Path("exports") / "schedule.json" builds a new path object with the right separator. On that result .name is "schedule.json", .stem is "schedule", .suffix is ".json" and .parent is Path("exports").
solid answer
~40 s`pathlib.Path` implements `__truediv__`, so `Path("exports") / "2026-03-01" / "schedule.json"` joins segments with the platform separator and returns a **new** immutable path — nothing is mutated, and a plain `str` may sit on either side of the operator. Decomposition is by property: `.name` is the final component, `.suffix` is the last dot-extension, `.stem` is `.name` minus that one suffix, `.parent` drops the last component, `.parts` is the whole tuple, and `.suffixes` lists every extension of a multi-dotted name. The `os.path` equivalents are `os.path.join`, `os.path.basename`, `os.path.splitext` and `os.path.dirname`, all of which operate on raw strings. All of these are pure string algebra — they never touch the disk. Because `Path` satisfies the `os.PathLike` protocol, you can pass it straight to `open()` or to any `os` function without calling `str()` on it.
code
pycon · 10 lines>>> from pathlib import Path
>>> p = Path("exports") / "2026-03-01" / "schedule.json"
>>> p
PosixPath('exports/2026-03-01/schedule.json')
>>> p.name, p.stem, p.suffix
('schedule.json', 'schedule', '.json')
>>> p.parent
PosixPath('exports/2026-03-01')
>>> p.parts
('exports', '2026-03-01', 'schedule.json')go deeper
Be ready to build a path with / and name what .name, .stem, .suffix and .parent give back, without reaching for os.path.join or string concatenation. Knowing that a Path can go straight into open() is expected.
Explain the mechanics: truediv returning a new immutable object, PurePath versus Path as the line between string algebra and syscalls, and the edge cases — one suffix only, dotfiles, and an absolute right-hand segment discarding the left.
Show the judgement of when path handling is a correctness risk: lexical equality that does not mean same file, .parent leaving '..' in place, and joining a caller-supplied segment proving nothing about where the result lands.
Own the convention across a codebase: paths as objects at every boundary rather than strings converted back and forth, str() pushed to the last possible moment, and PurePosixPath used so path handling can be tested on any host.
`pathlib` has been in the standard library since Python 3.4, and it exists because a filesystem path is not really a string: it has structure, its separator is platform-dependent, and almost every bug in hand-rolled path code comes from treating it as text. ### Pure paths and concrete paths `Path("exports")` returns a concrete path for the host you are running on — a `PosixPath` on Unix and macOS, a `WindowsPath` on Windows. Both inherit from `PurePath`, which holds every operation that is pure string algebra: joining, splitting, comparing, matching. `PurePosixPath` and `PureWindowsPath` let you manipulate a foreign-flavoured path on any host, which is the only sane way to parse a Windows path inside a test running on Linux. The split matters because it tells you which calls hit the filesystem: anything on `PurePath` cannot, anything added by `Path` (`exists`, `is_file`, `read_text`, `mkdir`, `glob`, `resolve`) can. ### Joining with / `PurePath.__truediv__` is why `/` works, and `__rtruediv__` is why `"exports" / Path("schedule.json")` works too. Each `/` returns a brand-new path object; paths are immutable and hashable, so you can use them as dict keys and share them freely. `joinpath("a", "b")` is the method form, useful when the segments arrive as a list you want to splat. One rule surprises people, and it matches `os.path.join`: **an absolute right-hand segment discards everything on the left.** `Path("/srv/data") / "/etc/hosts"` is `Path("/etc/hosts")`, not a path under `/srv/data`. So concatenating a caller-supplied segment does not by itself keep the result inside your base directory. Joining normalises separators but not semantics: `Path("a//b")` and `Path("a/b/")` both collapse to `a/b`, so `Path("a/b/").name` is `"b"` and not `""`. But `..` and `.` are left alone — `Path("a/b/..").parent` is `Path("a/b")`, not `Path(".")`. Only `resolve()` eliminates `..`. ### Decomposing - `.parts` — the components as a tuple, e.g. `('exports', '2026-03-01', 'schedule.json')`. - `.name` — the last component. It is `''` for a root path such as `Path("/")`. - `.suffix` — the **last** dot-extension only. For `schedule.tar.gz` that is `'.gz'`. - `.suffixes` — the full list, `['.tar', '.gz']`. - `.stem` — `.name` with exactly one suffix removed, so `'schedule.tar'` for that file. - `.parent` — the path minus its last component; it never climbs above what it was given, so `Path("schedule.json").parent` is `Path(".")` and `Path(".").parent` is `Path(".")`. - `.parents` — a lazy sequence of ancestors, and the classic off-by-one lives here: `parents[0]` is the parent, not the path itself. Since 3.10 it accepts slices and negative indices. - `.anchor`, `.drive`, `.root` — the platform prefix, mostly interesting on Windows. A leading dot is not a suffix: `Path(".gitignore").suffix` is `''` and its `.stem` is `'.gitignore'`. That single rule fixes the most common naive `splitext` bug. For rewriting a name there are three helpers rather than string surgery: `with_name`, `with_suffix` (replaces the last one, or appends if there is none) and `with_stem` (added in 3.9). ### Equality is lexical, not filesystem truth Two paths compare equal when their string forms match under their flavour — case-sensitively on POSIX, case-insensitively on Windows — and paths of different flavours are never equal. `Path("a/b")` and `Path("a/c/../b")` are different objects even though they name one file: redundant separators and single-dot components are dropped when the path is parsed, but `..` never is. `==` never performs a syscall. To ask whether two paths point at the same file, call `Path.samefile()`, which stats both and compares device and inode. ### Interoperating with older APIs `Path` implements the `os.PathLike` protocol, so `open(p)`, `os.stat(p)`, `os.scandir(p)` and essentially every stdlib function that takes a filename accepts one directly; `os.fspath(p)` is the explicit conversion to `str`. Only a third-party library that predates that protocol, or code that does string formatting on the value, needs `str(p)` — and `as_posix()` is the right call when you need forward slashes in output regardless of host.
- What does Path("schedule.tar.gz").stem return, and why?`'schedule.tar'`. `.suffix` is only the last dot-extension, `'.gz'`, and `.stem` is `.name` with that single suffix removed. The full list is `.suffixes`, which gives `['.tar', '.gz']`. A leading dot is deliberately not a suffix, so `Path('.gitignore').suffix` is `''` and its `.stem` is the whole name — which is exactly the case a naive `os.path.splitext` wrapper usually gets wrong.
- Does comparing two Path objects with == touch the filesystem?No. Equality and hashing are lexical, on the string form under the path flavour: `Path('a/b')` is not equal to `Path('a/c/../b')`, even though both name the same file, because `..` is never collapsed at parse time. Paths of different flavours never compare equal. Comparison is case-sensitive on POSIX and case-insensitive on Windows. To ask whether two paths refer to the same file, call `Path.samefile()`, which stats both and compares device and inode.
- What happens when you join an absolute segment onto a Path with /?The left operand is thrown away: `Path('/srv/data') / '/etc/hosts'` is `Path('/etc/hosts')`, matching `os.path.join`'s rule. So building a path by concatenating a caller-supplied segment gives you no guarantee that the result sits under your base directory — that has to be established afterwards, by resolving the candidate and comparing it against the resolved base.
- When do you still need str() around a Path?Rarely. `Path` implements the `os.PathLike` protocol, so `open()`, the `os` and `shutil` functions and most of the stdlib accept it directly, and `os.fspath()` is the explicit conversion. You need a string for old libraries that predate the protocol, and for string formatting or serialisation — where `as_posix()` is usually the better choice, because it gives forward slashes regardless of host.
A string path is a sentence about a file; a Path object is a parsed sentence — the separator is punctuation the object already understands, so you ask for the last word rather than counting characters.
saying these in an interview costs you the question
- Thinks / mutates the left-hand Path in place
- Says .suffix of archive.tar.gz is '.tar.gz'
- Builds paths with string concatenation and a hardcoded separator
- Believes == on two Paths asks the filesystem
- Assumes .parent collapses '..' segments
- Calls str() on every Path before passing it to open()