ruk·si

🐍 Python
pathlib

Updated at 2020-07-24 13:39

pathlib allows representing file system paths as objects. Contrary to the older os.path that works with strings, pathlib returns objects. Full standard library support was added in Python 3.6.

You can still use pathlib on older Python versions. You just have to install it separately.

pip install pathlib

Path objects work on all systems. With os.path you have to deal with / vs \ and other nonsense.

You can use PurePath for path handling that doesn't touch the file system. PurePosixPath to emulate Unix, or PureWindowsPath to emulate Windows. These implement os.PathLike interface so can be used in wide standard library contexts. Path is the concrete subclass of the more abstract PurePath.

Path objects are slower than os.path strings. Not an actual issue if you are not working with tens of thousands of files.

Resolving simple paths.

from pathlib import Path

# resolve makes the path absolute, convertig symlinks and dot components
assert Path('.').resolve() == Path().cwd()
# resolves can also be strict, failing if it doesn't exist
assert Path('..').resolve(strict=True) == Path('.').resolve().parent
# sometimes you need to use other helpers to get where you want
assert Path('~').expanduser() == Path().home()

# __file__ points to current file the line is in
THIS_FILE = Path(__file__)
BASE_DIR = THIS_FILE.parent
TEMPLATES_DIR = BASE_DIR.joinpath('templates')
print(THIS_FILE)     # /tmp/pathlib-is-awesome.py
print(BASE_DIR)      # /tmp
print(TEMPLATES_DIR) # /tmp/templates

Creating directories and files.

from tempfile import TemporaryDirectory
from pathlib import Path

# tempfile context is unrelated to pathlib, they make these examples auto-cleaning
with TemporaryDirectory() as temp_dir:

    # existance, we create this directory above
    dir_path = Path(temp_dir)
    assert dir_path.exists()

    # creating directories
    deep_path = dir_path.joinpath('i-want/this/structure')
    assert not deep_path.exists()
    deep_path.mkdir(parents=True, exist_ok=True)
    assert deep_path.exists()

    # writing to files
    config_path = deep_path.joinpath('.my-config')
    config_path.write_text('# config goes here #')
    assert config_path.read_text() == '# config goes here #'

    # Path objects refer to PATHS, not directories/files (they or might not exist)
    new_path = deep_path.joinpath('.new-config')
    assert config_path.exists()
    assert not new_path.exists()
    config_path.rename(new_path)
    assert not config_path.exists()
    assert new_path.exists()

Context managers work just as well.

from tempfile import TemporaryDirectory
from pathlib import Path

with TemporaryDirectory() as temp_dir:
    config_path = Path(temp_dir, '.editorconfig')
    with open(config_path, mode='w') as config_file:
        config_file.write('# config goes here')
    with open(config_path, mode='r') as config_file:
        assert config_file.read() == '# config goes here'

Searching for files using name patterns.

from tempfile import TemporaryDirectory
from pathlib import Path

with TemporaryDirectory() as temp_dir:

    # creating 3 files in temp dir and 3 files one directory down
    for i in range(3):
        Path(temp_dir, f'file-{i}.csv').write_text(str(i))
        deep_path = Path(temp_dir, 'deeper')
        deep_path.mkdir(exist_ok=True)
        Path(deep_path, f'file-{i}.csv').write_text(str(i))

    assert len([x for x in Path(temp_dir).glob('*.csv')]) == 3
    assert len([x for x in Path(temp_dir).glob('**/*.csv')]) == 6
    assert len([x for x in Path(temp_dir).rglob('*.csv')]) == 6
    assert len([x for x in Path(temp_dir).rglob('doesnt-exist')]) == 0

You can cast between strings and Path objects when required.

import os
from pathlib import Path

p1 = os.path.join('src', 'my_package')
p2 = Path('src', 'my_package')
assert type(p1) == str
assert isinstance(p2, Path)
assert os.fspath(p1) == os.fspath(p2)  # converting to strings
assert Path(p1) == Path(p2)            # converting to objects

Sources