Two splits, each taking the *last* separator. Watch what happens to the two awkward names.
'a/b/c.txt'.rpartition('/') # -> ('a/b', '/', 'c.txt')
'c.txt'.rpartition('/') # -> ('', '', 'c.txt') no slash, still three
'archive.tar.gz'.rpartition('.') # -> ('archive.tar', '.', 'gz')
'archive.tar.gz'.partition('.') # -> ('archive', '.', 'tar.gz') wrong
# The two names that need a rule, not a split
'.gitignore'.rpartition('.') # -> ('', '.', 'gitignore') suffix? no.
'trailing.'.rpartition('.') # -> ('trailing', '.', '') suffix? no.
- rpartition always returns three pieces, even with no separator
split() gives one piece or two depending on the input, so the caller has to check the length. rpartition gives head, separator, tail every time — with the head empty when nothing matched. That predictability is what lets you unpack it into three names on one line without a guard.
- Extensions come from the right
partition takes the first separator and rpartition the last, and for file extensions the last one is the answer: archive.tar.gz has the suffix .gz and the stem archive.tar. Getting this backwards produces a stem of 'archive', which then loses the .tar when something rebuilds the name.
- The position of the dot is the rule
A dot at position 0 makes a hidden file, and a dot at the very end has nothing after it, so neither is a suffix. Both are decided by asking where the dot is rather than whether there is one — which is why str.rfind, returning an index, fits better here than a split that throws the position away.
Split the directory off first, then work on the name alone. The extension rule is about where the last dot is inside the name, so you want its position rather than just its presence.
path.rpartition('/') gives the directory and the name in one step. Then name.rfind('.') gives the position of the last dot, or -1 when there is none.
A dot counts as a suffix only when its index is greater than 0 (not a hidden file) and less than len(name) - 1 (something follows it). When it does not count, the stem is the whole name and the suffix is empty.
def path_parts(path): directory, _, name = path.____('/') dot = name.____('.') if dot > ____ and dot < len(name) - 1: stem, suffix = name[:dot], name[dot:] else: stem, suffix = name, '____' return { 'directory': directory, 'name': name, 'stem': stem, 'suffix': suffix, }
def path_parts(path):
directory, _, name = path.rpartition("/")
dot = name.rfind(".")
# A dot at the start is a hidden file and a dot at the end has
# nothing after it — neither is an extension.
if 0 < dot < len(name) - 1:
stem, suffix = name[:dot], name[dot:]
else:
stem, suffix = name, ""
return {
"directory": directory,
"name": name,
"stem": stem,
"suffix": suffix,
}
Python’s chained comparison reads as the sentence it encodes: the dot is after the start and before the end. Written as two separate conditions joined by `and` it says the same thing and invites somebody to delete the half they do not understand. Chaining is worth using precisely when the two bounds are one idea.
rpartition would split the name and then leave you reconstructing it to decide the split was wrong — because the decision depends on *where* the dot was, and partitioning throws that away. When the position is the rule, ask for the position. rfind returning -1 for 'not found' also falls neatly outside the valid range, so the no-dot case needs no separate branch.
'.txt' rather than 'txt' means the name can be rebuilt as stem + suffix with no conditional — and an empty suffix concatenates correctly too, which the version without the dot cannot manage. It matches pathlib, which is a good enough reason on its own: matching the standard library’s conventions means less surprise for everyone who reads it.
Use pathlib. Path('a/b/c.txt') gives you .parent, .name, .stem and .suffix — plus .suffixes for the .tar.gz case, the right separator on Windows, drive letters, symlink resolution and the awkward truths about '..' and '.'. This function is a string exercise, not a path library: it has no opinion about absolute paths, backslashes, or a name that is only '..'. Write it when you are parsing something path-shaped that is not a real path — an S3 key, a URL route, an archive member name — where pathlib’s filesystem assumptions would be wrong.
- Add a suffixes key holding every extension of archive.tar.gz as a list, and decide what it should be for .gitignore.
- Handle an absolute path so that '/a' reports a directory of '/' rather than '', and see how many cases that one change disturbs.
- Write the inverse — build a path back from the four parts — and check the round trip against every case here.