Pathlib absolute() vs. resolve()

The top search engine result for “pathlib absolute” is this StackOverflow question. The top answers given are:

  1. "use absolute()"
  2. "use resolve(), do not use absolute()".

Trying both (in C:\example\) I get:

>>> from pathlib import Path
>>> print(Path('file.txt').absolute())
C:\example\file.txt
>>> print(Path('file.txt').resolve())
file.txt

So absolute() works as expected, resolve() does not work.

But apparently absolute() is undocumented and https://bugs.python.org/issue29688 seems stuck, and very negative to absolute(). This is very confusing.

How is resolve() supposed to work?

I want to write a simple command line tool that works for both tool.py file.txt and tool.py C:\example\file.txt. How can resolve() turn both into C:\example\file.txt, like absolute() does? Or is it fine to use absolute()?

2 Likes

Path.absolute() is undocumented and thus unsupported, so use at your own peril.

What version of Python are you on? It works for me on macOS Mojave on Python 3.8:

Python 3.8.0 (tags/v3.8.0:fa919fdf25, Nov  1 2019, 15:59:17) 
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pathlib
>>> import os
>>> os.getcwd()
'/private/tmp'
>>> pathlib.Path('file.txt').resolve()
PosixPath('/private/tmp/file.txt')

Probably tangent, but one gripe I’ve had with resolve() is it returns UNC paths on Windows network drives, but there are cases you just can’t use those (e.g. handing them to other tools that except a local path), and there’s no built in way to convert a UNC path back to local path. I would very much like to avoid absolute() (os.path.abspath() is even more awkward), but sometimes I feel there really is something missing.

Thanks for the replies.

I now tried:

Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:37:50) [MSC v.1916 64 bit (AMD64)] on win32

Python 3.7.1 (v3.7.1:260ec2c36a, Oct 20 2018, 14:57:15) [MSC v.1915 64 bit (AMD64)] on win32

Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32

Python 3.5.4 (v3.5.4:3f56838, Aug 8 2017, 02:17:05) [MSC v.1900 64 bit (AMD64)] on win32

Python 3.4.0 (v3.4.0:04f714765c13, Mar 16 2014, 19:25:23) [MSC v.1600 64 bit (AMD64)] on win32

The results are as shown above in all versions: absolute() works as expected, resolve() does not work. (In 3.4 and 3.5 resolve() raises FileNotFoundError and in 3.6, 3.7 and 3.8 it appears to do nothing at all.)

I don’t like using undocumented and unsupported features. What is the alternative?

I can replicate this on Windows, it looks like a bug to me. I submitted bpo-38671.

2 Likes

From my naive point of view as a (Windows) user it appears very surprising that absolute() remains undocumented. So far my impression is:

  • absolute() works in all versions of Python.
  • There is no alternative.
  • The commonly recommended alternative is not working in any version of Python. Even if the bugs get fixed it will remain a quite problematic API due to these (current and past) problems.
  • The term absolute() is much clearer and to the point anyway. It is self-explanatory, fits the user intent, widely known standard terminology and other (documented) API methods like is_absolute().
  • The term resolve() seems comparatively exotic. The documentation gives the impression it is mainly about something related to symlinks, and getting an absolute path seems to be a vague side-effect at best (if it worked). For Windows I would not have considered this at all if it had not been mentioned on Stack Overflow.
  • It seems unlikely / inadvisable to remove or change the undocumented absolute() method. It is the top search engine result, the intuitive self-evident solution, it works and thus undoubtedly already in wide real world use.

Is this impression wrong? Is there a working alternative to absolute()? Thanks.

Path.absolute() is redundant. Currently it is exactly equivalent to Path.cwd() / path. Having one obvious way we do not need other way for getting the same.

1 Like

The fact that none of cited discussions discovered Path.cwd() / path seems like incontrovertible evidence that it’s not obvious.

10 Likes

Thanks for the suggestion! It is not very intuitive to me (e.g. in case path might already be an absolute path) but it does seem to work. (I would have expected path if path.is_absolute() else Path.cwd() / path was needed.)

The not obvious part IMO is to realise that Path.absolute() is actually not comparable to os.path.abspath (despite the similar name). absolute does not try to clean up .. like abspath, which is usually what the user wants but not really. Path.resolve() is the best function in design, but it suffers from suboptimal implementations in various versions that makes it less useful than it should be.

Path normalisation is one of those topics that’s intrinsically more complex than most people expect. It’d be wonderful if there’s a cohesive write-up explaining what the problem is, why Python has so many similar options, and why the best is not the best right now but the others are worse although they work now. But it’s also one of those topics that people who understand don’t find it interesting to talk about, and those who don’t understand have little interest in listening (because they think they get it) :stuck_out_tongue:

5 Likes

Yup, this is explained in the documentation:

When several absolute paths are given, the last is taken as an anchor (mimicking os.path.join() ’s behaviour)

So if you pass an absolute path as the operand to / (or argument in joinpath), it causes all previous path contents to be discarded. It’s a very handy behaviour.

1 Like

(I’m aware that there are many subtle complexities like .., UNC, junctions etc. In my current use case luckily none of these matter. A good overview of such subtleties and how the various APIs handle them would indeed be a nice addition to the documentation.)

If Path.cwd() / path is the recommended pattern, it would be good to add it explicitly to the documentation as well. Perhaps in the table at the end, but also under the documentation of Path.is_absolute() and Path.cwd().

The explanation about “when several absolute paths are given, the last is taken as an anchor” does not make it obvious this also applies to the / operator.

I also noticed that currently the word “working” and the term “current working directory” do not appear at all in the documentation. This makes discovering Path.cwd() even harder.

Personally to me path.absolute() is far more readable and intuitive than Path.cwd() / path but I realize this is maybe a matter of taste and habit.

Thanks again to all for taking the time to respond.

I will also admit that maintaining the API for pathlib is tough. There’s currently no assigned expert for the module (I have contemplated signing myself up), and everyone wants everything solved by this module when it wasn’t meant to do that (e.g. we constantly get asked to add stuff from shutils into the pathlib.Path). So it’s a hard balance to strike of not bloating the class out with every variance while still being pragmatic with things.

I have opened https://bugs.python.org/issue39090 to track the idea of simply documenting what has been said here and other places about calculating the absolute path and why you may choose one approach over the other.

4 Likes
>>> Path.cwd() / Path("D:x")
WindowsPath('D:x')
>>> Path("D:x").resolve()
WindowsPath('D:/x')
>>> (Path.cwd() / Path("D:x")).is_absolute()
False
>>> Path("D:x").resolve().is_absolute()
True

I know this comes under the heading of “it’s complicated”, but I’d consider “returns an absolute path” to be a key requirement of any request to make a path absolute :slight_smile:

4 Likes

In the common non-exotic case (where file x does not exist yet) absolute() still works fine, while resolve() is still broken:

Python 3.9.0b4 (tags/v3.9.0b4:69dec9c, Jul 2 2020, 21:46:36) [MSC v.1924 64 bit (AMD64)] on win32

>>> (Path.cwd() / "x").is_absolute()
True
>>> Path("x").absolute().is_absolute()
True
>>> Path("x").resolve().is_absolute()
False
>>> Path("D:x").resolve().is_absolute()
False

(I’m not sure D:/x would necessarily even be the best / correct answer for the exotic case D:x. There may be no good answer for that considering "each drive had its own current directory, but now they don’t, but it looks like they do")

D:/x would necessarily even be the best / correct answer for the exotic case D:x

For example os.path.abspath observes the “current directory of the drive”:

>>> os.chdir('D:/sub')
>>> os.chdir('C:')
>>> p = Path('D:x')
>>> p.resolve()
WindowsPath('D:x')
>>> os.path.abspath(p)
'D:\\sub\\x'
>>> p.touch()
>>> p.resolve()
WindowsPath('D:/sub/x')
>>> list(Path('D:').iterdir())
[WindowsPath('D:x')]

The issue you’re describing has been confirmed as a bug. There’s a link to the corresponding issue above.

Raymond Chen is wrong when we says that the conventionally hidden “=X:” environment variables for per-drive working directories are just set and used by the CMD shell. The C runtime’s _[w]chdir sets them, as does Python’s own implementation of os.chdir. WinAPI SetCurrentDirectoryW normally doesn’t set them, but it uses them if the drive isn’t the working drive, as does GetFullPathNameW (and thus ntpath.abspath) and the equivalent internal RTL routine that normalizes paths for all file API calls that accept file paths.

The API will reset a per-drive environment variable to the drive root path if the current value is an invalid path at the time of use. For example:

>>> SetCurrentDirectory('D:\\')
>>> SetEnvironmentVariable('=C:', r'C:\path\to\nowhere')
>>> GetEnvironmentVariable('=C:')
'C:\\path\\to\\nowhere'
>>> GetFullPathName('C:spam')
'C:\\spam'
>>> GetEnvironmentVariable('=C:')
'C:\\'

>>> SetEnvironmentVariable('=C:', r'C:\path\to\nowhere')
>>> SetCurrentDirectory('C:Windows')
>>> GetEnvironmentVariable('=C:')
'C:\\'
2 Likes