Skip to content

Core Path API

pathlib_next.path

Object-oriented filesystem paths.

This module provides classes to represent abstract paths and concrete paths with operations that have semantics appropriate for different operating systems.

FsPathLike

Bases: Protocol

Anything implementing __fspath__ -- registered with os.PathLike so os.fspath() and friends accept it.

Path

Bases: Pathname, Chmod, Stat, BinaryOpen

Base class for manipulating paths with I/O.

__init_subclass__(**kwargs)

Guarantee pathlib_next operation precedence in every subclass.

Concrete path classes are routinely built by mixing a pathlib class with this one -- our own LocalPath does it, and the documented downstream recipe (class X(PosixPathname, Path)) does it transitively. Python's MRO then resolves a name to whichever base declares it first, which for those classes can be pathlib rather than pathlib_next -- and which one wins changes with the interpreter version, because stdlib pathlib keeps gaining and changing methods (see _OPERATION_NAMES).

Rather than making every downstream implementer rediscover this and hand-write forwarding methods, re-assert our implementations here for any subclass that would otherwise inherit a non-pathlib_next one. A subclass (or an intermediate mixin) that defines the method itself is always left alone -- this only displaces implementations coming from outside this library.

Source code in src/pathlib_next/path.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
def __init_subclass__(cls, **kwargs):
    """Guarantee pathlib_next operation precedence in every subclass.

    Concrete path classes are routinely built by mixing a `pathlib`
    class with this one -- our own `LocalPath` does it, and the
    documented downstream recipe (`class X(PosixPathname, Path)`) does
    it transitively. Python's MRO then resolves a name to whichever
    base declares it first, which for those classes can be `pathlib`
    rather than `pathlib_next` -- and which one wins changes with the
    interpreter version, because stdlib `pathlib` keeps gaining and
    changing methods (see `_OPERATION_NAMES`).

    Rather than making every downstream implementer rediscover this and
    hand-write forwarding methods, re-assert our implementations here
    for any subclass that would otherwise inherit a non-pathlib_next
    one. A subclass (or an intermediate mixin) that defines the method
    *itself* is always left alone -- this only displaces implementations
    coming from outside this library.
    """
    super().__init_subclass__(**kwargs)
    for name in _OPERATION_NAMES:
        # A class that defines the operation in its OWN body is always
        # authoritative -- never displace a deliberate override (this
        # also covers `LocalPath.copy`/`move`'s explicit routing).
        if name in vars(cls):
            continue
        # Find which class in the MRO actually supplies the inherited
        # implementation. Checking the resolved function's `__module__`
        # is not enough: a downstream mixin may legitimately define the
        # method in its own module, and that must be honored too.
        owner = next((base for base in cls.__mro__[1:] if name in vars(base)), None)
        if owner is None:
            continue
        # Only stdlib `pathlib` is displaced. Anything else -- a
        # downstream mixin, a user base class, or pathlib_next itself --
        # is a deliberate implementation and is left alone. Matching on
        # stdlib specifically (rather than "not pathlib_next") is what
        # keeps this guard from hijacking third-party code.
        owner_module = getattr(owner, "__module__", "") or ""
        if owner_module != "pathlib" and not owner_module.startswith("pathlib."):
            continue
        ours = getattr(Path, name, None)
        if ours is None:
            continue
        setattr(cls, name, ours)

copy(target, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None)

Copy this file's content to target.

follow_symlinks/preserve_metadata are named to match CPython 3.14's Path.copy() (added after this method); overwrite is our own extension (3.14 has no equivalent -- it always raises if the destination exists). preserve_metadata defaults to True here (unlike 3.14's False) to match this method's pre-existing behavior of always propagating st_mode; only st_mode is preserved, not timestamps/xattrs -- full metadata preservation is not implemented. ignore_error accepts a bool or a callable, matching Path.rm()'s bool-or-callable contract. True ignores every error; False and None (the default) fail on the first error. A callable is invoked as ignore_error(error) -- this call site's own arity -- and, as it always has here, is a notification hook: the error is suppressed regardless of what it returns, so handlers like errors.append (returning None) keep working. Only errors from child copies during a recursive=True copy are routed here.

progress, when given, is called as progress(path, bytes_copied, total_size) for every chunk written during each file copy (path is the source Path being streamed -- self for a single-file copy, or the relevant child during a recursive=True copy). bytes_copied increases monotonically per file and reaches total_size (or None if the size couldn't be determined) at the end of that file. Directories themselves don't get a progress call (only the files inside them do). With progress=None (the default), behavior is unchanged -- no per-chunk overhead. Native backend transfers that bypass the generic streaming copy (e.g. SftpPath's asyncssh concurrent fan-out) do not invoke progress; see docs/divergences.md's "Deliberate extensions" section.

Source code in src/pathlib_next/path.py
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
def copy(
    self,
    target: "Path | str",
    *,
    overwrite=False,
    follow_symlinks=True,
    preserve_metadata=True,
    recursive=False,
    ignore_error=None,
    progress: "_ty.Callable[[_ty.Self, int, _ty.Optional[int]], None]" = None,
):
    """Copy this file's content to `target`.

    `follow_symlinks`/`preserve_metadata` are named to match CPython
    3.14's Path.copy() (added after this method); `overwrite` is our
    own extension (3.14 has no equivalent -- it always raises if the
    destination exists). `preserve_metadata` defaults to True here
    (unlike 3.14's False) to match this method's pre-existing behavior
    of always propagating st_mode; only st_mode is preserved, not
    timestamps/xattrs -- full metadata preservation is not implemented.
    `ignore_error` accepts a bool or a callable, matching `Path.rm()`'s
    bool-or-callable contract. `True` ignores every error; `False` and
    `None` (the default) fail on the first error. A callable is invoked
    as `ignore_error(error)` -- this call site's own arity -- and, as it
    always has here, is a *notification* hook: the error is suppressed
    regardless of what it returns, so handlers like `errors.append`
    (returning None) keep working. Only errors from *child* copies
    during a `recursive=True` copy are routed here.

    `progress`, when given, is called as `progress(path, bytes_copied,
    total_size)` for every chunk written during each *file* copy (`path`
    is the source `Path` being streamed -- `self` for a single-file
    copy, or the relevant child during a `recursive=True` copy).
    `bytes_copied` increases monotonically per file and reaches
    `total_size` (or `None` if the size couldn't be determined) at the
    end of that file. Directories themselves don't get a progress call
    (only the files inside them do). With `progress=None` (the
    default), behavior is unchanged -- no per-chunk overhead. Native
    backend transfers that bypass the generic streaming copy (e.g.
    `SftpPath`'s asyncssh concurrent fan-out) do not invoke `progress`;
    see `docs/divergences.md`'s "Deliberate extensions" section.
    """
    if isinstance(target, str):
        target = type(self)(target)
    src = self

    if recursive and src.is_dir():
        if target.exists():
            if not target.is_dir():
                raise FileExistsError(target)
            if not overwrite:
                raise FileExistsError(target)
        else:
            target.mkdir()
        for child in src.iterdir():
            try:
                child.copy(
                    target / child.name,
                    overwrite=overwrite,
                    follow_symlinks=follow_symlinks,
                    preserve_metadata=preserve_metadata,
                    recursive=True,
                    ignore_error=ignore_error,
                    progress=progress,
                )
            except Exception as e:
                # A callable stays a notify-and-suppress hook (its return
                # value was never consulted here, and callers such as
                # `errors.append` rely on that). Bools are new: True
                # suppresses, False/None raise -- matching rm()'s bool
                # semantics without changing the callable contract.
                if callable(ignore_error):
                    ignore_error(e)
                elif not ignore_error:
                    raise
        return

    if target.exists():
        if target.is_dir():
            raise IsADirectoryError(target)
        if overwrite:
            target.unlink()
        else:
            raise FileExistsError(target)
    if progress is None:
        BinaryOpen.copy(src, target)
    else:
        BinaryOpen.copy(
            src,
            target,
            progress=lambda copied, total: progress(src, copied, total),
        )

    if preserve_metadata:
        try:
            stat = src.stat(follow_symlinks=follow_symlinks)
            target.chmod(stat.st_mode)
        except NotImplementedError:
            pass

glob(pattern, *, case_sensitive=None, include_hidden=False, recursive=None, dironly=None)

Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern.

If pattern contains a "" component, recursion is auto-enabled (pathlib parity). Pass recursive=False explicitly to disable recursion even when "" is present, or recursive=True to force it for a pattern that doesn't contain "**". Note for remote schemes (http/sftp): a recursive glob walks the whole remote subtree, one request/roundtrip per directory.

Source code in src/pathlib_next/path.py
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
def glob(
    self,
    pattern: str | _ty.Self,
    *,
    case_sensitive: bool = None,
    include_hidden: bool = False,
    recursive: bool = None,
    dironly: bool = None,
):
    """Iterate over this subtree and yield all existing files (of any
    kind, including directories) matching the given relative pattern.

    If `pattern` contains a "**" component, recursion is auto-enabled
    (pathlib parity). Pass `recursive=False` explicitly to disable
    recursion even when "**" is present, or `recursive=True` to force
    it for a pattern that doesn't contain "**".
    Note for remote schemes (http/sftp): a recursive glob walks the
    whole remote subtree, one request/roundtrip per directory.
    """
    if recursive is None:
        if isinstance(pattern, str):
            segments = pattern.split("/")
        elif hasattr(pattern, "segments"):
            segments = pattern.segments
        else:
            segments = ()
        recursive = _glob.RECURSIVE in segments
    yield from _glob.glob(
        self / pattern,
        case_sensitive=case_sensitive,
        include_hidden=include_hidden,
        recursive=recursive,
        dironly=dironly,
    )

is_hidden()

Return True if the final path component is a hidden file/directory.

Source code in src/pathlib_next/path.py
379
380
381
def is_hidden(self):
    """Return True if the final path component is a hidden file/directory."""
    return self.name.startswith(".")

iterdir()

Yield path objects of the directory contents.

The children are yielded in arbitrary order, and the special entries '.' and '..' are not included.

Source code in src/pathlib_next/path.py
404
405
406
407
408
409
410
411
@_utils.notimplemented
def iterdir(self) -> "_ty.Iterator[_ty.Self]":
    """Yield path objects of the directory contents.

    The children are yielded in arbitrary order, and the
    special entries '.' and '..' are not included.
    """
    ...

mkdir(mode=511, parents=False, exist_ok=False)

Create a new directory at this given path.

Source code in src/pathlib_next/path.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
    """
    Create a new directory at this given path.
    """
    try:
        self._mkdir(mode)
    except FileNotFoundError:
        if not parents or self.parent == self:
            raise
        # Parents may already exist (e.g. a sibling branch created them)
        # -- mirror CPython: exist_ok=True for parents, original
        # exist_ok only for the final retry of self.
        self.parent.mkdir(parents=True, exist_ok=True)
        self.mkdir(mode, parents=False, exist_ok=exist_ok)
    except FileExistsError:
        if not exist_ok or not self.is_dir():
            raise

move(target, *, overwrite=False)

Move this file or directory to target, falling back to copy+unlink/rm if rename is unsupported.

Source code in src/pathlib_next/path.py
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
def move(self, target: "Path|str", *, overwrite=False):
    """Move this file or directory to target, falling back to copy+unlink/rm if rename is unsupported."""
    if isinstance(target, str):
        target = type(self)(target)
    src = self
    if src is None:
        return

    if target.exists():
        if overwrite:
            target.rm(recursive=True, missing_ok=True)
        else:
            raise FileExistsError(target)

    try:
        return src.rename(target)
    except NotImplementedError:
        pass

    if src.is_dir():
        src.copy(target, overwrite=overwrite, recursive=True)
        src.rm(recursive=True)
    else:
        src.copy(target, overwrite=overwrite)
        src.unlink()

rename(target)

Rename this file or directory to the given target.

Source code in src/pathlib_next/path.py
685
686
687
688
@_utils.notimplemented
def rename(self, target: "_ty.Self | str"):
    """Rename this file or directory to the given target."""
    ...

rglob(pattern, *, case_sensitive=None, include_hidden=False, recursive=True, dironly=None)

Equivalent to glob(f"**/{pattern}", recursive=True).

Source code in src/pathlib_next/path.py
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def rglob(
    self,
    pattern: str,
    *,
    case_sensitive: bool = None,
    include_hidden: bool = False,
    recursive: bool = True,
    dironly: bool = None,
):
    """Equivalent to `glob(f"**/{pattern}", recursive=True)`."""
    yield from self.glob(
        f"**/{pattern}",
        case_sensitive=case_sensitive,
        include_hidden=include_hidden,
        recursive=recursive,
        dironly=dironly,
    )

rm(recursive=False, missing_ok=False, ignore_error=False)

Remove this file or directory, optionally recursively and ignoring errors.

Source code in src/pathlib_next/path.py
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
def rm(
    self,
    /,
    recursive=False,
    missing_ok=False,
    ignore_error: bool | _ty.Callable[[Exception, _ty.Self], bool] = False,
):
    """Remove this file or directory, optionally recursively and ignoring errors."""
    # Same bool-or-callable normalization as copy()/PathSyncer, via the
    # shared helper. A supplied callable keeps rm()'s own `(error, path)`
    # arity -- arities differ per call site by design, see the helper.
    _onerror = _utils.as_error_handler(ignore_error)

    def _handle(error, path):
        if not _onerror(error, path):
            raise error

    def _scan_entries(path):
        for entry in path._scandir():
            if isinstance(entry, tuple) and len(entry) == 2:
                yield entry
                continue
            try:
                stat = FileStat.from_stat(entry.stat(follow_symlinks=False))
            except OSError:
                stat = None
            yield entry.name, stat

    def _remove_tree(path):
        try:
            entries = list(_scan_entries(path))
        except Exception as error:
            _handle(error, path)
            return

        for name, child_stat in entries:
            child = path / name
            try:
                if child_stat is None:
                    child_stat = FileStat.from_path(child, follow_symlink=False)
                if child_stat is not None and child_stat.is_dir():
                    _remove_tree(child)
                else:
                    child.unlink()
            except Exception as error:
                _handle(error, child)

        try:
            path.rmdir()
        except Exception as error:
            _handle(error, path)

    try:
        stat = FileStat.from_path(self, follow_symlink=False)
    except Exception as error:
        _handle(error, self)
        return
    if stat is None:
        if not missing_ok:
            _handle(FileNotFoundError(self), self)
    elif stat.is_dir():
        if recursive:
            _remove_tree(self)
        else:
            try:
                self.rmdir()
            except Exception as error:
                _handle(error, self)
    else:
        try:
            self.unlink()
        except Exception as error:
            _handle(error, self)

rmdir()

Remove this directory. The directory must be empty.

Source code in src/pathlib_next/path.py
605
606
607
608
609
@_utils.notimplemented
def rmdir(self):
    """
    Remove this directory.  The directory must be empty.
    """

samefile(other_path)

Return whether other_path is the same or not as this file, by comparing (st_dev, st_ino) when the backend's stat() provides them. Raises NotImplementedError otherwise (e.g. our own FileStat doesn't carry st_dev/st_ino) -- LocalPath gets a real implementation from pathlib.Path via MRO instead of this one.

Source code in src/pathlib_next/path.py
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def samefile(self, other_path: str | _ty.Self):
    """Return whether other_path is the same or not as this file, by
    comparing (st_dev, st_ino) when the backend's stat() provides them.
    Raises NotImplementedError otherwise (e.g. our own FileStat doesn't
    carry st_dev/st_ino) -- LocalPath gets a real implementation from
    pathlib.Path via MRO instead of this one.
    """
    other = other_path if isinstance(other_path, Path) else type(self)(other_path)
    st1 = self.stat()
    st2 = other.stat()
    ident1 = (getattr(st1, "st_dev", None), getattr(st1, "st_ino", None))
    ident2 = (getattr(st2, "st_dev", None), getattr(st2, "st_ino", None))
    if None in ident1 or None in ident2:
        raise NotImplementedError(
            "samefile() requires stat() to provide st_dev/st_ino"
        )
    return ident1 == ident2

Make this path a symlink pointing to target.

Signature-compatible with pathlib.Path.symlink_to(); force is a pathlib_next extension (see docs/divergences.md).

With force=True, an existing entry at this path is removed first. No filesystem or transport offers an atomic "replace a symlink" operation, so this is an unlink-then-symlink sequence and is therefore not atomic: between the two steps the path does not exist, and a concurrent writer can win the race. It is a convenience, not a locking primitive.

Only a non-directory entry is removed -- an existing directory at the link path is left alone and the underlying FileExistsError (or the backend's equivalent) propagates. Silently deleting a directory tree is never what force= on a symlink call is asking for.

Source code in src/pathlib_next/path.py
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
def symlink_to(
    self,
    target: "_ty.Self | str",
    target_is_directory: bool = False,
    *,
    force: bool = False,
) -> None:
    """Make this path a symlink pointing to `target`.

    Signature-compatible with `pathlib.Path.symlink_to()`; `force` is a
    pathlib_next extension (see `docs/divergences.md`).

    With `force=True`, an existing entry at *this* path is removed
    first. No filesystem or transport offers an atomic "replace a
    symlink" operation, so this is an unlink-then-symlink sequence and
    is therefore **not** atomic: between the two steps the path does not
    exist, and a concurrent writer can win the race. It is a
    convenience, not a locking primitive.

    Only a non-directory entry is removed -- an existing *directory* at
    the link path is left alone and the underlying `FileExistsError`
    (or the backend's equivalent) propagates. Silently deleting a
    directory tree is never what `force=` on a symlink call is asking
    for.
    """
    # Normalize a str target to a path object, so the primitive only
    # ever handles one type -- same `type(self)(target)` form as
    # copy()/move() use for their destination, for consistency.
    if isinstance(target, str):
        target = type(self)(target)
    if force:
        try:
            self.unlink(missing_ok=True)
        except (IsADirectoryError, PermissionError, OSError) as error:
            # A directory at the link path (POSIX: IsADirectoryError;
            # Windows and several remote backends: PermissionError or a
            # plain OSError) is not something force= should remove.
            # Let the symlink attempt below raise the error that
            # actually describes the conflict.
            if not self.is_dir():
                raise error
    return self._symlink_to(target, target_is_directory)

touch(mode=438, exist_ok=True)

Create this file with the given access mode, if it doesn't exist.

Raises FileExistsError if exist_ok is False and the file already exists (pathlib parity) instead of silently truncating it.

Source code in src/pathlib_next/path.py
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
def touch(self, mode=0o666, exist_ok=True):
    """
    Create this file with the given access mode, if it doesn't exist.

    Raises FileExistsError if exist_ok is False and the file already
    exists (pathlib parity) instead of silently truncating it.
    """
    if exist_ok:
        if self.exists():
            return
        with self.open("w"):
            ...
    else:
        try:
            with self.open("x"):
                ...
        except NotImplementedError:
            # _open() doesn't support "x" (optional per the mode
            # contract) -- emulate exclusivity best-effort. Small
            # TOCTOU window between the check and the open() below.
            if self.exists():
                raise FileExistsError(self)
            with self.open("w"):
                ...
    try:
        self.chmod(mode)
    except NotImplementedError:
        pass

Remove this file or link. If the path is a directory, use rmdir() instead.

Source code in src/pathlib_next/path.py
598
599
600
601
602
603
@_utils.notimplemented
def unlink(self, missing_ok=False):
    """
    Remove this file or link.
    If the path is a directory, use rmdir() instead.
    """

walk(top_down=True, on_error=None, follow_symlinks=False)

Walk the directory tree from this directory, similar to os.walk().

Uses _scandir() rather than iterdir() + a stat() per entry -- for schemes whose listing already carries type metadata (HTTP/DAV indexes, SFTP listdir_attr, FTP MLSD, S3 list pages), this turns a remote-tree walk from O(entries) round trips into O(dirs). The pre-seeded stat is trusted only when follow_symlinks is False (matching walk()'s own default and the lstat-like semantics of those listing calls); an explicit follow_symlinks=True always re-stat()s each entry so a symlink is still resolved.

Source code in src/pathlib_next/path.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
def walk(
    self,
    top_down=True,
    on_error: _ty.Callable[[OSError], None] = None,
    follow_symlinks=False,
):
    """Walk the directory tree from this directory, similar to os.walk().

    Uses `_scandir()` rather than `iterdir()` + a `stat()` per entry --
    for schemes whose listing already carries type metadata (HTTP/DAV
    indexes, SFTP `listdir_attr`, FTP MLSD, S3 list pages), this turns a
    remote-tree walk from O(entries) round trips into O(dirs). The
    pre-seeded stat is trusted only when `follow_symlinks` is False
    (matching `walk()`'s own default and the lstat-like semantics of
    those listing calls); an explicit `follow_symlinks=True` always
    re-`stat()`s each entry so a symlink is still resolved.
    """
    paths: "list[_ty.Self|tuple[_ty.Self, list[str], list[str]]]" = [self]

    while paths:
        path = paths.pop()
        if isinstance(path, tuple):
            yield path
            continue
        try:
            # `_scandir()` is a generator -- listing this directory may
            # not actually happen (and so may not raise) until the first
            # `next()`, not at this call. Materializing it here (rather
            # than iterating lazily below) keeps any such error inside
            # this try, matching the on_error contract regardless of
            # whether a given implementation happens to fail eagerly or
            # lazily.
            entries = list(path._scandir())
        except OSError as error:
            if on_error is not None:
                on_error(error)
            continue

        dirnames: "list[str]" = []
        filenames: "list[str]" = []
        for name, stat in entries:
            try:
                if stat is None or follow_symlinks:
                    stat = FileStat.from_path(
                        path / name, follow_symlink=follow_symlinks
                    )
                is_dir = stat.is_dir() if stat is not None else False
            except OSError:
                # Carried over from os.path.isdir().
                is_dir = False

            if is_dir:
                dirnames.append(name)
            else:
                filenames.append(name)

        if top_down:
            yield path, dirnames, filenames
        else:
            paths.append((path, dirnames, filenames))

        paths += [path / d for d in reversed(dirnames)]

Pathname

Bases: FsPathLike, Generic[_P]

Base class for manipulating paths without I/O.

anchor property

drive + root.

drive property

No generic concept of a drive; always "". LocalPath gets a real one from pathlib on Windows.

name property

The final path component, if any.

parent abstractmethod property

The logical parent of the path.

parents property

An immutable sequence providing access to the logical ancestors of the path.

parts abstractmethod property

The individual components/parts of the path.

root property

The root of the path, if any (e.g. "/"). See docs/divergences.md for how this is derived generically vs. LocalPath's real one.

segments abstractmethod property

The sequence of path component strings.

stem property

The final path component, minus its last suffix.

suffix property

The final component's last suffix, if any.

This includes the leading period. For example: '.txt'

suffixes property

A list of the final component's suffixes, if any.

These include the leading periods. For example: ['.tar', '.gz']

as_posix()

Return the string representation of the path with forward slashes.

Source code in src/pathlib_next/path.py
258
259
260
def as_posix(self) -> str:
    """Return the string representation of the path with forward slashes."""
    return "/".join(self.segments)

as_uri() abstractmethod

Return the path as a URI string.

Source code in src/pathlib_next/path.py
86
87
88
89
@_abc.abstractmethod
def as_uri(self) -> str:
    """Return the path as a URI string."""
    ...

full_match(pattern, *, case_sensitive=None)

Return True if this path matches the glob-style pattern against the whole path (3.13 parity). Unlike match(), this isn't a right-anchored partial match, and "**" matches any number of path segments (including zero).

Source code in src/pathlib_next/path.py
248
249
250
251
252
253
254
255
256
def full_match(self, pattern: str, *, case_sensitive: bool = None) -> bool:
    """Return True if this path matches the glob-style `pattern`
    against the whole path (3.13 parity). Unlike match(), this isn't a
    right-anchored partial match, and "**" matches any number of path
    segments (including zero).
    """
    if case_sensitive is None:
        case_sensitive = self._is_case_sensitive
    return _glob.full_match(self.segments, pattern, case_sensitive)

has_glob_pattern()

Return True if any of the path segments contain glob wildcards.

Source code in src/pathlib_next/path.py
262
263
264
265
266
267
def has_glob_pattern(self):
    """Return True if any of the path segments contain glob wildcards."""
    for segment in self.segments:
        if _glob.WILDCARD_PATTERN.search(segment) != None:
            return True
    return False

is_absolute()

True if the path is absolute

Source code in src/pathlib_next/path.py
231
232
233
234
@_utils.notimplemented
def is_absolute(self) -> bool:
    """True if the path is absolute"""
    ...

is_relative_to(other)

Return True if the path is relative to another path or False.

Source code in src/pathlib_next/path.py
187
188
189
190
191
def is_relative_to(self, other: _ty.Self | str):
    """Return True if the path is relative to another path or False."""
    cls = type(self)
    other = other if isinstance(other, cls) else cls(self, other)
    return other == self or other in self.parents

joinpath(*args)

Combine this path with one or more paths/segments.

Source code in src/pathlib_next/path.py
199
200
201
def joinpath(self, *args: str | _ty.Self) -> _ty.Self:
    """Combine this path with one or more paths/segments."""
    return type(self)(self, *args)

match(path_pattern, *, case_sensitive=None)

Return True if this path matches the given pattern.

Source code in src/pathlib_next/path.py
236
237
238
239
240
241
242
243
244
245
246
def match(self, path_pattern: str | _re.Pattern, *, case_sensitive=None):
    """
    Return True if this path matches the given pattern.
    """
    if case_sensitive is None:
        case_sensitive = self._is_case_sensitive
    # as_posix(), not str(self): str() of a Uri includes scheme/host.
    path = self.as_posix()
    if not isinstance(path_pattern, _re.Pattern):
        path_pattern = _glob.compile_pattern(path_pattern, case_sensitive)
    return path_pattern.match(path) is not None

relative_to(other) abstractmethod

Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not related to the other path), raise ValueError.

Source code in src/pathlib_next/path.py
179
180
181
182
183
184
185
@_abc.abstractmethod
def relative_to(self, other: _ty.Self | str) -> _ty.Self:
    """Return the relative path to another path identified by the passed
    arguments.  If the operation is not possible (because this is not
    related to the other path), raise ValueError.
    """
    ...

with_name(name)

Return a new path with the name changed.

Source code in src/pathlib_next/path.py
151
152
153
154
155
def with_name(self, name: str) -> _ty.Self:
    """Return a new path with the name changed."""
    if not self.name:
        raise ValueError("%r has an empty name" % (self,))
    return self.with_segments(*self.segments[:-1], name)

with_segments(*segments) abstractmethod

Construct a same-type path instance from new segments.

Source code in src/pathlib_next/path.py
146
147
148
149
@_abc.abstractmethod
def with_segments(self, *segments: str) -> _ty.Self:
    """Construct a same-type path instance from new segments."""
    ...

with_stem(stem)

Return a new path with the stem changed.

Source code in src/pathlib_next/path.py
157
158
159
def with_stem(self, stem: str) -> _ty.Self:
    """Return a new path with the stem changed."""
    return self.with_name(stem + self.suffix)

with_suffix(suffix)

Return a new path with the suffix changed or added.

Source code in src/pathlib_next/path.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def with_suffix(self, suffix: str) -> _ty.Self:
    """Return a new path with the suffix changed or added."""
    name = self.name
    if (
        suffix
        and not suffix.startswith(".")
        or (suffix == "." and _sys.version_info < (3, 14))
    ):
        raise ValueError("Invalid suffix %r" % (suffix))
    if not name:
        raise ValueError("%r has an empty name" % (self,))
    old_suffix = self.suffix
    if not old_suffix:
        name = name + suffix
    else:
        name = name[: -len(old_suffix)] + suffix
    return self.with_name(name)

pathlib_next.fspath

LocalPath

Bases: WindowsPath if name == 'nt' else PosixPath, Path, _BaseFSPathname

The real local filesystem path: pathlib.WindowsPath/PosixPath with this library's Path mixed in via MRO. Behaves exactly like pathlib.Path for anything not explicitly overridden here (see docs/divergences.md).

glob(pattern, *, case_sensitive=None, include_hidden=False, recursive=None, dironly=None)

Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern.

A "**" pattern component auto-enables recursion (pathlib parity); see Path.glob()'s docstring for the recursive= override rules.

Source code in src/pathlib_next/fspath.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def glob(
    self,
    pattern: str | _proto.FsPathLike,
    *,
    case_sensitive: bool = None,
    include_hidden: bool = False,
    recursive: bool = None,
    dironly: bool = None,
):
    """Iterate over this subtree and yield all existing files (of any
    kind, including directories) matching the given relative pattern.

    A "**" pattern component auto-enables recursion (pathlib parity);
    see Path.glob()'s docstring for the `recursive=` override rules.
    """
    if not isinstance(pattern, (str, _re.Pattern)):
        pattern = _os.fspath(pattern)
    if dironly is None:
        dironly = (
            isinstance(pattern, str)
            and pattern
            and pattern[-1] in self._path_separators
        )
    yield from _proto.Path.glob(
        self,
        pattern,
        case_sensitive=case_sensitive,
        include_hidden=include_hidden,
        recursive=recursive,
        dironly=dironly,
    )

PosixPathname

Bases: PurePosixPath, _BaseFSPathname

Pure (no I/O) POSIX-flavour path, implementing the Pathname protocol on top of pathlib.PurePosixPath.

WindowsPathname

Bases: PureWindowsPath, _BaseFSPathname

Pure (no I/O) Windows-flavour path, implementing the Pathname protocol on top of pathlib.PureWindowsPath.