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 projRemember
- Since 3.12, Path subclasses survive /, parent, iterdir and glob
- Override
with_segmentsto carry extra state into derived paths - Call
super().__init__(*args)and avoid private pathlib internals
Watch out for
- Adding state in
__init__but not overridingwith_segments, so derived paths lose it and raise TypeError or AttributeError.