# pathlib.Path.joincomponent()

**URL:** <https://discuss.python.org/t/pathlib-path-joincomponent/42908>\
**Category:** Ideas\
**Created:** [January 10, 2024, 7:08pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908 "2024-01-10T19:08:55Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![herbalmist](https://avatars.discourse-cdn.com/v4/letter/h/a6a055/32.png) [@herbalmist](https://discuss.python.org/u/herbalmist)\
**Post date:** [January 10, 2024, 7:08pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/1 "2024-01-10T19:08:55Z")

</div>

Hello,

pathlib.PurePath.joinpath() uses os.path.join() to join new path components to the target path and returns a new path. An unfortunate side effect of this is that it interprets the characters os.sep and os.altsep in a special way, e.g.:

`from pathlib import PurePath

a = PurePath(“/foo/bar”)  
b = a.joinpath(“/baz/quux”) # → results in PurePath(“/baz/quux”)`

This requires users of PurePath to intentionally check for os.sep and os.altsep when joining paths that come from untrusted sources. This is awkward for a few reasons but it’s also an insecure default. I propose a new method tentatively called `joincomponent()` that does no special interpretation of characters within the path components being joined and interprets them purely as components along the path.

Thank You,  
Herbal Mist

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [January 10, 2024, 7:54pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/2 "2024-01-10T19:54:29Z")

</div>

This has nothing to do with special casing `os.sep` or using `os.path.join`. This is an intentional design choice: Absolute paths reset the root of the path currently being build. If you don’t want that, you will need to manually special case absolute paths in the arguments list. This is not an extra security risk, if you want to make sure that a path is within a specific folder, you need to check that. After all, users could also provide `..` in the given path.

---

<div class="post-metadata">

**Author:** ![herbalmist](https://avatars.discourse-cdn.com/v4/letter/h/a6a055/32.png) [@herbalmist](https://discuss.python.org/u/herbalmist)\
**Post date:** [January 10, 2024, 8:35pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/3 "2024-01-10T20:35:51Z")

</div>

> This is an intentional design choice: Absolute paths reset the root of the path currently being build.

Yes, that’s why I’m suggesting the design of a new method with new semantics.

It’s not just the handling of absolute paths, it’s also the interpretation of os.sep as a path separator and adding multiple components in a single call. e.g.:

a = PurePath(“/foo/bar”)  
a.joinpath(“baz/quux”) # this call adds two path components instead of one  
a.parts # == (‘/’, ‘foo’, ‘bar’, ‘baz’, ‘quux’)

I’m suggesting that adding multiple components in a single call to a.joinpath() is more surprising than not doing that. It’s fine to preserve that behavior but it would be nice if there were a method that didn’t do that.

> If you don’t want that, you will need to manually special case absolute paths in the arguments list.

Yes but that requires users of PurePath to explicitly do that, as I mentioned in my original post.

> After all, users could also provide `..` in the given path.

Yes but you can check for `..` by using the `parts` property and disallow that specifically before interpreting the path. You cannot do that for components that contain os.sep or os.altsep. Right now there is an inconsistency between how `..` is treated and how `os.sep` is treated.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [January 10, 2024, 8:39pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/4 "2024-01-10T20:39:10Z")

</div>

> [@herbalmist](#):
>
> It’s not just the handling of absolute paths, it’s also the interpretation of os.sep as a path separator and adding multiple components in a single call. e.g.:

Well… yes. That is what `/` means in paths. `baz/quux` _is not_ a valid path component. `a.parts` will never be `('/', 'foo', 'bar', 'baz/quux')`. That just isn’t valid. If you don’t want this behavior, IDK what you are trying to do, but `pathlib` is not the correct library for you (since you aren’t working with paths apparently)

---

<div class="post-metadata">

**Author:** ![jamestwebber](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jamestwebber/32/12799_2.png) [@jamestwebber](https://discuss.python.org/u/jamestwebber)\
**Post date:** [January 10, 2024, 8:41pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/5 "2024-01-10T20:41:01Z")

</div>

edit: never mind, misread what you were saying in the second post

---

<div class="post-metadata">

**Author:** ![herbalmist](https://avatars.discourse-cdn.com/v4/letter/h/a6a055/32.png) [@herbalmist](https://discuss.python.org/u/herbalmist)\
**Post date:** [January 10, 2024, 8:50pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/6 "2024-01-10T20:50:38Z")

</div>

> [@MegaIng](#):
>
> `a.parts` will never be `('/', 'foo', 'bar', 'baz/quux')`. That just isn’t valid.

I understand that it’s not valid. Right now the caller of PurePath is forced to check if path components that are to be added to joinpath contain os.sep or os.altsep beforehand to avoid unintended behavior. This creates a tight binding between the semantics of PurePath and os.sep/os.altsep.

If the path object is PureWindowsPath and I’m on a POSIX system, I have no way of knowing that I should check if the path component contains “\”, since that is not contained in os.sep or os.altsep on POSIX systems. This means that PurePath objects cannot be used polymorphically or as duck types because there is not enough information exposed to use them securely. That is the crux of the issue.

If a method existed like `.joincomponent()` that didn’t add any special handling to components that contained path separators, then I could look at `path.parts` before path was used to see if any component contained any invalid character and fail at that point.

Alternatively PurePath objects could expose os.sep/os.altsep as properties.

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [January 10, 2024, 8:55pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/7 "2024-01-10T20:55:26Z")

</div>

/cc @barneygale

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [January 10, 2024, 9:20pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/8 "2024-01-10T21:20:44Z")

</div>

> [@herbalmist](#):
>
> This means that PurePath objects cannot be used polymorphically or as duck types because there is not enough information exposed to use them securely.

You can do a few checks purely in pathlib (check that the resulting path is a subpath of the expected parent and doesn’t resolve to somewhere else via attacking a symlink you didn’t anticipate)

but I think there’s room for better here. I don’t think this needs to be a different method though, is there a reason this shouldn’t raise? joining a path here implies to me that one path must be relative to another, and I can’t think of an _intended_ use here that raising on this would break.

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [January 10, 2024, 9:23pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/9 "2024-01-10T21:23:27Z")

</div>

Oh, quick meaningful clarification: even with fixing this, if the input is untrusted you _Still_ need to check that the path ends up where expected (`..` use, symlinks…) I only mean is there a good reason why join should allow completely resetting the root?

---

<div class="post-metadata">

**Author:** ![barneygale](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/barneygale/32/1882_2.png) [@barneygale](https://discuss.python.org/u/barneygale)\
**Post date:** [January 10, 2024, 9:28pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/10 "2024-01-10T21:28:43Z")

</div>

A couple of ideas for workarounds. I haven’t yet thought about the proposed new API (but I will do).

If you need to check that the user has supplied a purely relative path:

```python
if user_path.anchor:
    raise ValueError('path is not relative!')
path / user_path

```

If you need to check that the user path doesn’t contain any drives, separators, etc:

```python
path.joinpath('_').with_name(user_path)

```

---

<div class="post-metadata">

**Author:** ![herbalmist](https://avatars.discourse-cdn.com/v4/letter/h/a6a055/32.png) [@herbalmist](https://discuss.python.org/u/herbalmist)\
**Post date:** [January 10, 2024, 9:58pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/11 "2024-01-10T21:58:37Z")

</div>

> [@barneygale](#):
>
> If you need to check that the user path doesn’t contain any drives, separators, etc:
> 
> ```auto
> path.joinpath('_').with_name(user_path)
> 
> ```

That idiom is essentially what I’m looking for. Doing a sort of faux dimensional analysis, of the current source code, that’s one of the few methods that looks at `sep` and `altsep` in the implementation, so it makes sense.

The semantics I was suggesting wouldn’t throw. In my head there should be an “AbstractPurePath” that has no concept of platform-specific separators or invalid characters. It just carries path components. Since PurePath is always either an alias to PureWindowsPath or PurePosixPath, then throwing makes sense. No abstract PurePath is exposed by pathlib right now anyway, (i.e. something that could be used as an parent class for a new file system type that used completely different path separators, etc.).

I still would suggest directly addressing the issue here though. I’m happy to use this workaround but it’s not ideal due to being wordy. Any combination of either adding a `.joincomponent()` or exposing `.sep` and `.altsep` properties or both would be sufficient. I think it also makes sense to add a note to the documentation that `joinpath()` shouldn’t be used with untrusted input since that seems like an easy error to make.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [January 10, 2024, 10:02pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/12 "2024-01-10T22:02:58Z")

</div>

> [@herbalmist](#):
>
> I think it also makes sense to add a note to the documentation that `joinpath()` shouldn’t be used with untrusted input since that seems like an easy error to make.

This is true for _all_ pathlib operations. You should always make sure that the end result matches whatever security concerns you have. I don’t know what makes `joinpath` any more dangerous that anything else.

---

<div class="post-metadata">

**Author:** ![herbalmist](https://avatars.discourse-cdn.com/v4/letter/h/a6a055/32.png) [@herbalmist](https://discuss.python.org/u/herbalmist)\
**Post date:** [January 10, 2024, 10:10pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/13 "2024-01-10T22:10:22Z")

</div>

> [@MegaIng](#):
>
> This is true for _all_ pathlib operations.

Of all the methods on PurePath, I think `joinpath()` is more likely by a large amount than any of them to be mistakenly used with untrusted input. `with_stem()`, `with_suffix()`, `relative_to()`, ` __truediv__ ` all seem like they would ordinarily be used with programmer supplied constants. Additionally `joinpath()` (and `with_pathsegments()`) are unique in that they silently interpret their input with special processing, which is easy to miss in the documentation.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [January 10, 2024, 10:16pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/14 "2024-01-10T22:16:57Z")

</div>

> [@herbalmist](#):
>
> Additionally `joinpath()` (and `with_pathsegments()`) are unique in that they silently interpret their input with special processing, which is easy to miss in the documentation.

No: ` __truediv__ ` behaves just like `joinpath` (`PurePath('/foo/bar') / '/a/b'` results in `PurePath('/a/b')`. Same with ` __init__ ` (`PurePath('/foo/bar', '/a/b')` results in the same).

You seem to be of the opinion that `/` and/or `\\` should not be treated specially in paths. This is just wrong: They are special operators, basically by definition of path. If you don’t want to allow them, that is extra user pre-processing that you have to do.

In fact, it is quite reasonable for programmers to want to allow their users to specify sub folders. So this “special casing” (i.e. treating strings representing paths as paths) is to 100% the expected behavior

---

<div class="post-metadata">

**Author:** ![herbalmist](https://avatars.discourse-cdn.com/v4/letter/h/a6a055/32.png) [@herbalmist](https://discuss.python.org/u/herbalmist)\
**Post date:** [January 10, 2024, 10:23pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/15 "2024-01-10T22:23:21Z")

</div>

> [@MegaIng](#):
>
> No: ` __truediv__ ` behaves just like `joinpath` (`PurePath('/foo/bar') / '/a/b'` results in `PurePath('/a/b')`. Same with ` __init__ ` (`PurePath('/foo/bar', '/a/b')` results in the same).

I think you misinterpreted what I meant when I said that ` __truediv__ ` is mostly used with programmer supplied constants. I am aware that ` __truediv__ ` is a synonym for `a.joinpath(b)` but in most code that I see where ` __truediv__ ` is used, it’s used like this:

```
path / "foo" / "bar" / "baz"

```

Not:

```
path / "foo/bar/baz"

```

We see this type of usage in the [documentation](https://docs.python.org/3/library/pathlib.html#operators):

```
p / 'init.d' / 'apache2'

```

I can’t address the rest of your comment because it is projecting a position onto me that I did not express here.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [January 10, 2024, 10:34pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/16 "2024-01-10T22:34:36Z")

</div>

> [@herbalmist](#):
>
> I think you misinterpreted what I meant when I said that

You said “`joinpath()` is unique”: No, it’s not. The default behavior for methods on Path objects is that they “silently interpreter their input with special processing”. This is the default since, you know, `pathlib` works with paths.

The only methods that don’t treat the seperator as path separator in their string arguments are `with_suffix` (which errors) and `write_name` (which errors).

---

<div class="post-metadata">

**Author:** ![herbalmist](https://avatars.discourse-cdn.com/v4/letter/h/a6a055/32.png) [@herbalmist](https://discuss.python.org/u/herbalmist)\
**Post date:** [January 10, 2024, 10:40pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/17 "2024-01-10T22:40:15Z")

</div>

> You said “`joinpath()` is unique”:

In the context in which I was explaining my reasoning for why it may be a good idea to add a warning in the documentation when using `joinpath`, it is unique. In any case, it seems to be a case of a misunderstanding, thanks for your input.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [January 10, 2024, 10:45pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/18 "2024-01-10T22:45:34Z")

</div>

Well, if you are going to make false statements and dismiss people who call you out, then yes, I don’t see any value in continuing this discussion with you.

---

<div class="post-metadata">

**Author:** ![herbalmist](https://avatars.discourse-cdn.com/v4/letter/h/a6a055/32.png) [@herbalmist](https://discuss.python.org/u/herbalmist)\
**Post date:** [January 10, 2024, 10:54pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/19 "2024-01-10T22:54:21Z")

</div>

I’m sorry, I didn’t mean to dismiss you nor did I realize you were calling me out. It just seemed like the conversation turned into clarifying past comments. Your feedback has been appreciated. It’s important to look at the documentation as a whole before making any isolated change and indeed that caused me to assess the the other methods of `PurePath`. Thanks! If you have more opinions on this topic, please don’t feel discouraged from voicing them.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [January 10, 2024, 11:00pm UTC](https://discuss.python.org/t/pathlib-path-joincomponent/42908/20 "2024-01-10T23:00:06Z")

</div>

I think my preferred solution is for `PurePath` gain a `classmethod` `is_valid_part_name` (name pending) which checks if the passed in string contains the separator, if it’s equal to `.` or `..` or if it’s any of the illegal names in Windows Paths. The intended code path for users where they only want to allow users to specify a single path component would be to first run the input through this function. That would be a more clean alternative to the `with_name` pattern above.

[Next page](https://discuss.python.org/t/pathlib-path-joincomponent/42908.md?page=2)
