Add floordiv to pathlib.Path for strict append

Hi! After a recent bug in a project, I felt it would be helpful to have Path // str operate as a “strict” alternative, which would raise a ValueError if the str included the separator in it.

It could be as simple as:

diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index 911b774b56..7739860982 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -903,6 +903,15 @@ class PurePath(object):
     def __rtruediv__(self, key):
         return self._from_parts([key] + self._parts)
 
+    def __floordiv__(self, key):
+        """A strict verison of div that does not allow separators in the value.
+        This is to prevent accidentally adding sub-directories to a path.
+        """
+        f = self._flavour
+        if f.sep in key:
+            raise ValueError(f'Found {f.sep} in path value: {key}')
+        return self.__truediv__(self, key)
+
     @property
     def parent(self):
         """The logical parent of the path."""

Although, perhaps it ought check if key is a string… Then again, if you’re using this, you may reasonably be expected to ensure yourself to only use it with strings.

2 Likes

Thanks for the idea but I don’t think this is worth the overhead of needing to remember what // represents for paths since it’s non-obvious. Basically tests should cover this use-case.

7 Likes

A proper named method, like path.join_segment or something, seems more likely to find support. Python style tends to prefer words over operators, except for the very most common cases.

3 Likes

I was about to suggest this, before seeing this.

To me it is much nicer to have

Path.home() // "my_file"

than

(Path.home() / "my_file").resolve(strict=True)

or similar

It is nicer once you know what it does, but is anyone going to know that without looking it up?

2 Likes

I’m not even sure if I like this idea or not, but what about defining __abs__ on Path so you can use: abs(Path.home() / "my_file") ?

7 Likes

I’m sure. I don’t like it :slight_smile: The use of division as a way to join path components is cute and handy, but it doesn’t imply that paths are numbers. You wouldn’t expect cout * 2 to have the same effect as cout << 1 in C++.

2 Likes

I don’t have a problem with using abs with paths (just like + is used for integer addition and string concatenation), but Path.__abs__ = Path.absolute makes more sense.

All of mathematics is built by extending a concept beyond where it is intrinsically defined. If you start with counting numbers 1, 2, 3, 4… [1], you can define addition, and then define subtraction as “find the number which, when added to X, gives Y”. At that point, you need to define negative numbers, which solve problems like “find the number which, when added to 5, gives 2”. Then you can define multiplication as repeated addition; but it doesn’t make sense to add together a negative number of things, so multiplication by negative numbers is defined in the most logical way.

It’s the same when we step outside of pure mathematics. We define that adding two strings is concatenating them. This violates some of the key principles of arithmetic (x + y isn’t always the same as y + x for example), and it becomes harder to define subtraction (although personally I think that x - y is best defined as x.replace(y, "") as that’s both sensible and useful). Similarly, we can define that "spam" * 3 is "spamspamspam", which, again, makes perfect sense; but what is "spam" * 2.5 ? (Again, there IS a sensible and sometimes-useful definition, which is "spamspamsp", but at very least, it’s clear that this isn’t as simple as plain mathematics would imply.) What about "spamspamspamspam" / 2 ? I would accept, as sensible answers, "spamspam" but also ["spamspam", "spamspam"], and actually, neither of those is as practially useful as ["sp", "am", "sp", "am", "sp", "am", "sp", "am", "sp", "am"] (splitting into two-character units), even though that one makes a lot less sense.

So, what is the absolute value of a number? Mathematically, it’s the distance from the origin to that number. Algebraically, that can be defined by Pythagoras; square each component in the number, and take the square root. (For real numbers, you can optimize this down to -x if x < 0 else x but for complex numbers you need the full calculation.) So is an “absolute path” the same as the distance from the origin to that path? … Kinda. I mean, the root directory is kinda the origin, and you could say that the most direct path to a node is its “distance”. But once again, we’re really stretching things.

So in terms of logic and sensibility, this could really go either way. It’s not VERY logical, but it is at least a little bit logical. Which leaves us with the third criterion: Practicality. How useful is this? Is it convenient to be able to say path / "filename" ? You bet it is!! It reads well, and does a very common job. But is it useful to write abs(path)? Ehhhhhh… not so much. Maybe? Maybe if you’re doing a huge number of path manipulations and need to absolutify them all? But it’s not really all that big a deal for most programs.

(Side note: If it weren’t for the fact that paths are traditionally separated by slashes, and division is also traditionally written with a slash, there’d be no value in making path / "fn" work. Instead, it would be much better written as path + "fn" as it’s much more closely related to string concatenation. I don’t think that affects decisions about abs but it’s worth keeping in mind; for example, multiplying paths wouldn’t really be the inverse of dividing them, since dividing is really concatenating.)


  1. or start with set cardinality and define counting numbers from there ↩︎

Something to consider: when folks often reach for path.absolute() when path.resolve() is actually the tool they need.

When I use path.absolute() it’s usually by mistake because I’d forgotten about path.resolve().

>>> from pathlib import Path
>>> (Path() / "..").absolute()
PosixPath('/home/trey/..')
>>> (Path() / "..").resolve()
PosixPath('/home')

Setting aside the “is this a good idea” (I’m torn)… I’d want abs(path) to be equivalent to path.resolve(). Though I’m not sure if abs(path) being equivalent to path.resolve() would lead to more or less confusion between the path.absolute() and path.resolve() difference.

__abs__() might be useful for exposing a version of absolute() that can eliminate “..” segments at the beginning of a relative path but not elsewhere, as described in this thread:

On the original topic of joining a string that mustn’t contain a separator, I’ve found this idiom does the trick:

path.joinpath('_').with_name('your_name_here')

with_name() will raise a ValueError if the given name is empty, a single dot, or contains a separator.

__abs__() might be useful for exposing a version of absolute() that can eliminate “..” segments at the beginning of a relative path but not elsewhere, as described in this thread

Resolving initial “..” components would be reasonable for the absolute() method on both POSIX and Windows, for different reasons. On POSIX, the result of os.getcwd() is required to be a real path. On Windows, os.getcwd() doesn’t necessarily return a real path, but it doesn’t matter because the API logically resolves “..” components when opening a path.

1 Like