Schemes
Capability matrix for every built-in Path/UriPath implementation. A
"No" means the method raises NotImplementedError (or, for http:, isn't
meaningful for a read-only scheme) -- everything else (name/suffix parsing,
glob(), walk(), copy(), ...) is derived from these primitives and
works identically across all of them.
| Capability | LocalPath |
file: (FileUri) |
mem: (MemPath) |
http(s): (HttpPath) |
sftp: (SftpPath) |
data: (DataUri) |
ftp(s): (FtpPath) |
zip: (ZipUri) |
tar: (TarUri) |
archive:/archive+<fmt>: (ArchiveUri) |
dav(s): (DavPath) |
s3: (S3Path) |
gs: (GsPath) |
az: (AzPath) |
github: (GitHubPath) |
gitlab: (GitLabPath) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Read (read_text/read_bytes/open) |
Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
Write (write_text/write_bytes/open("w")) |
Yes | Yes | Yes | Yes (PUT, configurable) | Yes | No | Yes | New/overwritten entries, local outer archive only | No | Same as zip:, only if the outer resolves/is detected as zip |
Yes | Yes | Yes | Yes | No | No |
List (iterdir) |
Yes | Yes | Yes | Yes (scrapes an HTML index) | Yes | No (NotADirectoryError) |
Yes (MLSD, falls back to NLST) | Yes | Yes | Yes | Yes (PROPFIND) | Yes (prefix + delimiter emulation) | Yes (prefix + delimiter emulation) | Yes (prefix + delimiter emulation) | Yes (contents API) | Yes (tree API) |
Stat (stat, exists, is_dir, is_file, ...) |
Yes | Yes | Yes | Yes (via HEAD, falls back to GET) |
Yes | Yes (st_size from decoded payload) |
Yes (MLSD, falls back to SIZE for files) | Yes | Yes | Yes | Yes (PROPFIND) | Yes (is_dir is prefix emulation) |
Yes (is_dir is prefix emulation, no mtime) |
Yes (is_dir is prefix emulation, no mtime) |
Yes (no mtime) | Yes (no mtime) |
mkdir |
Yes | Yes | Yes | No | Yes | No | Yes | Local outer archive only (zero-length name/ entry) |
No | Same as zip:, zip-detected only |
Yes (MKCOL) | Yes (zero-byte key/ marker object) |
Yes (zero-byte key/ marker object) |
Yes (zero-byte key/ marker object) |
No | No |
Delete (unlink/rmdir/rm) |
Yes | Yes | Yes | Yes (DELETE) | Yes | No | Yes | Yes (local outer archive only) | No | Same as zip:, zip-detected only |
Yes (rmdir requires empty, see notes) |
Yes (rmdir requires empty prefix) |
Yes (rmdir requires empty prefix) |
Yes (rmdir requires empty prefix) |
No | No |
rename |
Yes | Yes | No (move() falls back to copy+unlink) |
No | Yes | No | Yes | Yes (local outer archive only) | No | Same as zip:, zip-detected only |
Yes (MOVE) | Yes (server-side copy_object+delete, same bucket) |
Yes (server-side copy+delete, same bucket) | Yes (server-side copy+delete, same container) | No | No |
chmod |
Yes | Yes | No | No | Yes (follow_symlinks=False works on the asyncssh backend, not paramiko -- see notes) |
No | Yes (SITE CHMOD, server-dependent) |
No | No | No | No | No | No | No | No | No |
| Extra required | none | none | none | http |
sftp |
none (stdlib) | none (stdlib ftplib) |
none (stdlib zipfile) |
none (stdlib tarfile) |
none (reuses zip:/tar:) |
http (reused) |
s3 |
gs |
az |
http (reused) |
http (reused) |
Notes:
file:is a thinUriPathwrapper aroundLocalPath-- it has identical capabilities, just addressed by URI instead of a native path.mem:(MemPath) isn't aUriPathat all -- it's a plainPathsubclass (Track A of Extending), backed by nested dicts (MemPathBackend). Noas_uri()scheme is registered for it; construct it directly viaMemPath(...).http(s):supports writing viaPUT(default, configurable toPOSTor other verbs viawith_session(..., write_method=...)) and deleting viaDELETE(wherermdir()checks empty status first). Directory listing parses Apache/nginx-style HTML indexes using a fast, zero-dependency parser. Append mode (open("a")) is supported via two strategies: the default "rewrite" mode (GET existing + PUT full body, safe on any server but non-atomic) and an opt-in "patch" mode using HTTP'sContent-RangePATCH verb for real appends (seewith_session(..., append_mode="patch")). Patch mode raisesPermissionErrorif the server rejects it, never silently falls back to rewrite.sftp:has the fullest capability set of the URI schemes (it's a real remote filesystem protocol), and is the one scheme with two selectable backends: paramiko (sync, thesftpextra) and asyncssh (async internally, bridged to a sync API via one shared background event loop, thesftp-asyncextra -- also works on Python 3.9 via a version-pinned release,asyncssh<2.22). Selection precedence, highest to lowest: an explicitbackend=constructor kwarg > aSftpPath._default_backend_clssubclass override > thePATHLIB_NEXT_SFTP_BACKENDenv var ("paramiko"/"asyncssh"/"auto", default"auto": asyncssh if importable, else paramiko) > auto-detect.PATHLIB_NEXT_SFTP_BACKEND=asyncsshwith the package not installed raises immediately rather than silently falling back to paramiko. The asyncssh backend caches connections per(backend, source)(no thread dimension needed, unlike paramiko's(backend, source, thread)-- one shared connection serves concurrent calls from any calling thread). Both backends implementreadlink()/symlink_to()(core SFTPv3 operations);hardlink_to()andchmod(follow_symlinks=False)work on the asyncssh backend only (paramiko'sSFTPClienthas no hard-link orlchmodequivalent at all -- both raiseNotImplementedErrorimmediately, no server round trip). Seepathlib_next.uri.schemes.sftp.data:(RFC 2397) has no server or connection at all -- the entire "file" content lives in the URI string itself (data:[<mediatype>][;base64],<data>). It's always a single file, never a directory.ftp(s):uses the same connection-cache pattern assftp:(stdlibftplib, no extra required). Prefers MLSD (RFC 3659) for listing/stat -- gives type/size/modify in one round trip -- and falls back to NLST/SIZE on servers that don't support it (that fallback path can't distinguish "file doesn't exist" from "is a directory" forstat(), since SIZE only works on files).zip:/tar:address an entry inside an archive:zip:<archive-uri>!/<inner-path>(Java-style!/separator, as in JAR URLs / NIOZipFileSystem). The<archive-uri>half is itself any absolute URI with an explicit scheme --file:,http:,sftp:,ftp:, evendata:-- so an archive is readable straight off any other backend;zip:file:///backups/site.zip!/index.htmlandzip:sftp://host/nightly.zip!/index.htmlboth work the same way.segments/name/parent/glob()/... all operate on the inner path. Writing (new entries, overwriting/deleting/renaming existing ones -- the latter three via a full-archive rewrite to a temp file, then an atomicos.replace) only works when the outer archive is itself a localfile:URI; every other outer scheme is read-only.tar:(with transparent.tar.gz/.tar.bz2/.tar.xzdecompression) is read-only regardless of the outer scheme.archive:/archive+<fmt>:is a convenience catch-all overzip:/tar:: barearchive:<archive-uri>!/<inner-path>auto-detects the format (outer filename extension first, then aPK-header magic-byte sniff), whilearchive+zip:/archive+tar:pin the format explicitly and skip detection. Resolves to the same shared backend (and registry key) as the dedicatedzip:/tar:schemes --archive:...!/xandzip:...!/xon the same outer archive share one open handle.zip:/tar:remain the primary, explicit schemes;archive:exists for callers that don't know (or don't care about) the format ahead of time.dav(s):is WebDAV (RFC 4918) layered onHttpPath-- PROPFIND replaces HTML-index scraping for real stat/listdir metadata, and PUT/ DELETE/MKCOL/MOVE give it full write support (unlike plainhttp(s):). Requests go out over the equivalenthttp:/https:URL;as_uri()still reportsdav:/davs:. Reuses thehttpextra, no new dependency.rmdir()enforces pathlib's "must be empty" contract with a depth-1 PROPFIND before issuingDELETE; the native recursiveDELETE(RFC 4918) is still available, and cheaper than a client-side walk, viarm(recursive=True).s3:(s3://bucket/key/path) has no real directories:is_dir()is prefix emulation (any object key under"<path>/"), andmkdir()creates a zero-byte"<path>/"marker object (the same convention the AWS console itself uses for an empty "folder") --rmdir()requires no other keys under that prefix (pathlib's "must be empty" semantics, same asdav:'srmdir()above). A singleboto3client is cached per backend (documented thread-safe, unlikesftp:/ftp:'s per-thread connection pools).gs:(gs://bucket/key/path, Google Cloud Storage) andaz:(az://account/container/key/path, Azure Blob Storage) follow the same prefix-emulation directory model ass3:--is_dir()checks for any blobs under"<path>/",mkdir()creates a zero-byte marker blob,rmdir()enforces empty (same semantics). Each backend caches one service client per instance (thread-safe for both GCS and Azure SDKs). Both report no mtime (would require metadata-only calls;st_mtimeis 0). Seepathlib_next.uri.schemes.gsandpathlib_next.uri.schemes.az.github:/gitlab:(<scheme>://host/owner/repo/path/in/repo?ref=<ref>) are read-only views of a git-hosting repository tree over plainrequests(no PyGithub/python-gitlab SDK).ref(branch/tag/SHA) is always optional in the?ref=query string; omitted,github:falls back to the repo's default branch server-side for free, whilegitlab:resolves and caches the default branch itself via one extraGET /projects/:idcall (GitLab's file-content endpoints 400 ifrefis omitted entirely, unlike its tree endpoint -- seedocs/divergences.md).hostdefaults togithub.com/gitlab.com; any other host is treated as GitHub Enterprise (API athttps://{host}/api/v3) or a self-hosted GitLab (https://{host}/api/v4). Auth: a bearer token viaRepoBackend(token=...)or embedded as URI userinfo (github://TOKEN@github.com/owner/repo).GitHubPathgets full listing metadata (type/size) from one contents-API call per directory and fetches file bodies via therawmedia type (skips base64 and its ~1MB inline-content cap);GitLabPath's tree API has no size field, so only directory entries get a pre-seededstat()hint, file entries fall back to a real per-file lookup rather than guessing a size. Both are read-only (writing goes through an entirely different commits API, out of scope) and report no mtime (would need a separate, expensive commits-history call). Seepathlib_next.uri.schemes.githubandpathlib_next.uri.schemes.gitlab.git:is a convenience catch-all over the same providers. It auto-detects only the public SaaS hosts (github.com/gitlab.com), sogit://github.com/...andgit://gitlab.com/...work, but self-hosted or enterprise hosts must use the explicitgithub:/gitlab:schemes or the pinnedgit+github:/git+gitlab:forms.git:keeps the same?ref=and auth behavior as the provider schemes; it only changes the entry point.- See Divergences from pathlib for the "explicitly out
of scope" list (
resolve,symlink_to,owner,expanduser, ...) that applies uniformly across every non-LocalPathimplementation.