Subclassing Path and with_segments

pathlib · Expert · 11 min

Custom path types (project roots, virtual file systems, paths carrying context) become clean and type-safe.

Before 3.12, subclassing Path was awkward because it dispatched to PosixPath/WindowsPath internally. Since 3.12, Path and PurePath can be subclassed directly, and the subclass is preserved through derived paths: p / "x", p.parent, p.iterdir() and p.glob() all return instances of your class.

They do this through **with_segments(*pathsegments)**, which pathlib calls whenever it constructs a new path from an existing one. The default creates type(self)(*pathsegments). If your subclass carries extra state (pick attribute names that don't clash with existing ones like root, name or parts), such as a project base folder or a session object, accept it in __init__ and override with_segments to pass it along:

def with_segments(self, *segments):
    return type(self)(*segments, base=self.base)

Since 3.12, __init__ receives the path segments and you must call super().__init__(*args); there's no need to override __new__.

Good uses: methods like project_path.is_test_file(), or a path type for a virtual file system. Keep subclasses thin; behaviour that doesn't depend on the path itself belongs elsewhere.

pathlib's internals changed significantly across 3.12 to 3.14 (pathlib._abc, pathlib.types in 3.14), so stick to documented hooks rather than private attributes.

Example

from pathlib import Path

class ProjectPath(Path):
    def __init__(self, *args, base=None):
        super().__init__(*args)
        self.base = base if base is not None else Path(*args)

    def with_segments(self, *segments):
        return type(self)(*segments, base=self.base)   # keep the context

    def rel(self):
        return self.relative_to(self.base)

    def is_test(self):
        return self.name.startswith("test_") and self.suffix == ".py"

Path("proj/tests").mkdir(parents=True)
Path("proj/tests/test_api.py").touch()
root = ProjectPath("proj")
f = next((root / "tests").iterdir())
print(type(f).__name__, f.rel(), f.is_test())   # ProjectPath tests/test_api.py True
print(type(f.parent).__name__, f.parent.base)   # ProjectPath proj

Remember

  • Since 3.12, Path subclasses survive /, parent, iterdir and glob
  • Override with_segments to carry extra state into derived paths
  • Call super().__init__(*args) and avoid private pathlib internals

Watch out for

  • Adding state in __init__ but not overriding with_segments, so derived paths lose it and raise TypeError or AttributeError.

Check yourself

  1. In Python 3.12+, what does (MyPath('a') / 'b') return when MyPath subclasses Path?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/pathlib.html#pathlib.PurePath.with_segments