Skip to main content

xrdclient

A pure-Python client for XRootD. root://, roots://, https://, HEP WebDAV and s3://, spoken by the same objects, with no compiled extension, no libXrdCl, and no third-party import in the core.

import xrdclient

for path in xrdclient.ls("root://eos.example.org//store/user/me"):
    print(path.name, xrdclient.human_bytes(xrdclient.size(path)))

with xrdclient.open("root://eos.example.org//store/data.root", "rb") as fh:
    header = fh.read(1024)

xrdclient.copy("root://a.example.org//store/f.root", "davs://b.example.org/store/f.root")

It is a Python library first and an XRootD binding second: files are real io objects, errors are OSError subclasses, paths are PurePath-shaped, and nothing returns a (status, result) pair - except where you ask for exactly that. Code written for the official bindings runs by changing one import:

from xrdclient.compat import client        # was: from XRootD import client

status, info = client.FileSystem("root://eos.example.org").stat("/store/f.root")

xrdclient.compat.client has every class, method, keyword, flag and response field of XRootD.client, and a parity suite holds its answers equal to the official bindings' on a real server - see docs/compat.md. Code you cannot edit, such as uproot's XRootD handler, runs on it after xrdclient.compat.install(). The guide to switching a codebase over is docs/porting.md, with a method-by-method reference, a cookbook, troubleshooting, and runnable examples checked against the official bindings.

Install

$ pip install xrdclient                 # the whole library, and nothing else

Requires Python 3.9+, which is what RHEL 9 and AlmaLinux 9 ship, so the system interpreter on a grid login node is enough. Almost nothing needs an extra: http://, https:// and WebDAV are http.client, S3 is that plus hmac, and GSI/X.509 proxies and Kerberos are pure Python down to the AES and RSA.

What it does

One-liners. xrdclient.ls, xrdclient.glob, xrdclient.stat, xrdclient.exists, xrdclient.size, xrdclient.checksum, xrdclient.read_text, xrdclient.read_bytes, xrdclient.write_text, xrdclient.write_bytes, xrdclient.mkdir, xrdclient.remove, xrdclient.move, xrdclient.stage and xrdclient.is_online each take a URL and answer one question, with nothing to build and nothing to close.

if not xrdclient.is_online("root://tape.example.org//store/f.root"):
    xrdclient.stage("root://tape.example.org//store/f.root")

No bit algebra. Every flag answers to its own name, and the common choices are keyword arguments: fh.open("r"), fh.open("new makepath"), fs.scandir(path, stat=False), fs.prepare(paths, evict=True), fs.query("checksum", path), fs.chmod(path, "rw-r-----"). A misspelling says what you probably meant. Printing a flag prints its name, printing a stat prints the line ls -l would have.

Files. xrdclient.open(url, mode) returns something from the io stack: seekable, buffered, iterable, context-managed, read/write/readinto, text mode when you ask for it. Vector reads (kXR_readv), paged I/O with CRC32c verification, checkpointed writes, server-side range copies (kXR_clone), and sendfile-shaped bulk copies are on the underlying object when you want them.

with xrdclient.open("root://host//store/f.root", "rb") as fh:
    for line in fh:            # buffered, like any other file
        ...
    fh.seek(-4096, 2)
    tail = fh.read()

Namespaces. xrdclient.FileSystem covers stat, statx, statvfs, scandir, walk, glob, mkdir, makedirs, rename, remove, rmtree, truncate, chmod, touch, checksum, locate, deep_locate, prepare (with query_prepare for how the staging is going and archive_info for where a file is now), query_config, extended attributes, and - where a server has been taught the vendor opcodes - symlink, link, readlink, lstat, is_symlink, utime, chown and listxattr_tree, which extensions() asks about before sending.

fs = xrdclient.FileSystem("davs://dav.example.org")
fs.makedirs("/store/user/me/2026", exist_ok=True)
print(fs.checksum("/store/user/me/f.root"))     # adler32:1a0b045d

Paths. xrdclient.Path is a PurePosixPath that knows its endpoint:

p = xrdclient.Path("root://host//store") / "user" / "me"
p.mkdir(parents=True, exist_ok=True)
(p / "note.txt").write_text("hello")
sizes = {child.name: child.stat().st_size for child in p.iterdir()}

Copies. xrdclient.copy, xrdclient.copy_tree and xrdclient.third_party move data between any two endpoints, local paths included, with checksum verification on by default and a progress= callback that takes (done, total). A tree can be filtered (include=, exclude=), brought up to date rather than recopied (sync="size" | "mtime" | "checksum"), pruned (delete=True), rehearsed (dry_run=True) or moved (remove_source=True). Every transfer keeps config.in_flight chunks read ahead of the write it is waiting on, so the two ends overlap instead of taking turns. An interrupted transfer is continued rather than restarted with resume=True, or xrd-cp -c, and a file long enough to be worth it is moved by config.parallel_chunks connections at once, one span of the file each. A tree of small files copies workers= of them in parallel, xrd-cp -r --parallel N.

A download runs on the bulk data plane: several reads in flight per connection, each received straight into the buffer it will be written from, across config.bulk_workers connections. On a stock xrootd that is 1.5 GiB/s to a local file where xrdcp does 0.37 GiB/s, with no C extension involved — see docs/performance.md. A worker that loses its server re-opens and resumes from where it got to, and a transfer that ends short of the file's length is an error rather than a truncated file.

A second measurement, further from the ideal case: the same download against a GSI-authenticated xrootd 5.9.7 in a container rather than a bare daemon on loopback, 1 GiB, median of seven runs each. The harness is examples/gsi_copy_benchmark.py, which mints its own CA and proxy, starts the server, and times whichever clients this machine has.

Client Median Range vs xrdcp
xrdclient, bulk data plane 305.8 MiB/s 296–320 1.56×
brix-xrdcp, BriX, pure C 292.1 MiB/s 266–322 1.49×
xrdclient, one connection 259.7 MiB/s 218–276 1.32×
XRootD Python bindings, official 205.0 MiB/s 156–210 1.04×
xrdcp, official C++ v6.1.1 196.2 MiB/s 159–204 1.00×

Read that as two findings and one caveat. Pure Python is level with a pure-C client — 5% apart, with overlapping ranges, because neither is bound by the language on a copy: both are bound by how many reads they keep in flight. And the official client's own ceiling is about two thirds of either, which is a pipelining difference rather than a language one; its Python bindings sit with it, as the same engine underneath should. The caveat is that this was loopback through a container's NAT on one laptop, so it measures a client's protocol efficiency and not a network — on a link with real latency the pipelining matters more, not less, but the numbers would be that link's.

Objects. A bucket is one more endpoint: s3://bucket/key reads, writes, lists and copies through the same xrdclient.open, xrdclient.FileSystem and xrdclient.copy, signed with AWS SigV4 out of hmac and hashlib — no boto3, in the dependency tree or the import graph. Credentials come from the environment or ~/.aws/credentials, or are left out entirely for a public bucket; an object too long to hold goes up as a multipart upload, and a failed one is aborted rather than left in the bucket. Ceph RGW, MinIO and anything else with an endpoint are addressed path-style, AWS virtual-hosted.

fs = xrdclient.FileSystem("s3://my-bucket", endpoint="https://rgw.example.org")
fs.listdir("/store/user/me")
xrdclient.copy("root://eos.example.org//store/f.root", "s3://my-bucket/store/f.root")

Built on this. Three packages of their own, each depending on the one before it, so a client install stays a client install:

xrdroot the ROOT file format in pure Python — trees read a basket at a time over any URL here, histograms, graphs, and a writer
xrdml a URL in, minibatches of (inputs, answers) out; trees into a PyTorch DataLoader or a tf.data.Dataset, with nothing downloaded
xrddatasets more than fourteen hundred open datasets converted into streamable ROOT, and the xrd-datasets command that publishes the catalogue serving them
$ pip install xrdml          # brings xrdroot and this client with it

Async. xrdclient.aio mirrors the whole surface — same names, same arguments, await in front. import xrdclient does not import asyncio; the facade is resolved on first use.

import asyncio, xrdclient.aio

async def main():
    async with xrdclient.aio.FileSystem("root://eos.example.org") as fs:
        async for entry in fs.iterdir("/store"):
            print(entry.name)
        names = await fs.listdir("/store")
        sizes = await asyncio.gather(*(fs.getsize(f"/store/{n}") for n in names))
        async with fs.open("/store/f.root") as fh:
            head, tail = await fh.readv([(0, 4096), (1 << 20, 4096)])

asyncio.run(main())

Authentication. gsi, ztn, sss, unix and host out of the box, tried in that order against whatever the server offers, with every rejected mechanism and its reason named in the final error. That means X.509 proxies from $X509_USER_PROXY (RFC 3820 and legacy Globus, with the lifetime checked before the round trip, so an expired proxy is a sentence and not a timeout an hour into a job) and WLCG / SciTokens / macaroons. TLS on roots://, xroots:// and davs:// — all three present the same proxy as the client certificate, so mutual TLS costs no argument.

krb5 needs nothing but kinit: it reads your credential cache, fetches the service ticket from the KDC itself when the cache holds only your TGT, and will tell you when your ticket expired. It is pure Python too, and tested against a real MIT KDC and a real xrootd.

At a terminal, a login with no proxy and no token asks for one — naming what is missing, where it looked, and the command that produces it — instead of failing; in a batch job it stays silent and puts the same explanation in the error. Config(prompt=False), --no-prompt and $XRD_PROMPT=0 settle it either way, and Config(prompter=...) moves the question into a GUI or a notebook.

Credentials are redacted from logs, reprs and tracebacks — that is enforced by a test, not a convention.

Guard rails. Safer than the stock tools where a beginner meets them: read() on a file bigger than config.max_read_size raises with the sentence that streams it instead of filling memory; xrd-cp refuses to overwrite without -f; xrd-fs rm -r asks at a terminal, with a count of what is about to go, and refuses a path less than two components deep until --yes; and root://host/store/f means the same file as root://host//store/f rather than a confusing miss. Each has one flag that says "yes, I mean it".

When something does go wrong, xrd-fs doctor (or xrdclient.diagnose()) asks every question a transfer would ask - settings, each authentication mechanism and what would fix it, DNS, the port, the login, how far down the path exists - and prints one line each, so the first !! is the cause rather than the last symptom. See Safety.

Command line

$ xrd-fs ls -l root://eos.example.org//store/user/me
$ xrd-fs stat --json davs://dav.example.org/store/f.root
$ xrd-fs checksum -a adler32 root://host//store/f.root
$ xrd-fs tail -f root://host//store/running.log
$ xrd-fs du root://host//store/run7
$ xrd-fs doctor root://eos.example.org//store/user/me   # why will this not work?
$ xrd-cp -r /tmp/results davs://dav.example.org/store/results
$ xrd-cp -r --sync size --delete /tmp/results root://host//store/results/
$ xrd-cp --tpc root://a//store/f.root root://b//store/f.root

Every subcommand takes whole URLs and understands --json, so a shell script never has to parse columns. Exit codes are the usual three: 0 success, 1 a runtime failure, 2 a usage error. Settings that never change can live in ~/.config/xrd/config.ini and be selected with --alias.

fsspec

Wherever fsspec is installed - pandas, dask and pyarrow bring it - root://, roots://, xroot://, dav://, davs:// and webdav:// are registered URL schemes, with nothing to import:

import pandas as pd
df = pd.read_parquet("root://eos.example.org//store/t.parquet")

Testing against it

xrdclient.testing ships the servers this library's own suite runs against — no storage element required:

from xrdclient.testing import FakeServer

with FakeServer(files={"/data/a.root": b"hello"}) as server:
    fs = xrdclient.FileSystem(server.url)
    assert fs.read_bytes("/data/a.root") == b"hello"

FakeDAVServer is the same idea for HTTP and WebDAV, and FakeS3Server a bucket — signatures checked against the AWS specification rather than trusted.

Status

The wire protocol, session state machine, the whole authentication ladder, file and namespace APIs, pathlib bindings, the async facade, HTTP/WebDAV, S3, the copy engine, the bulk data plane, the CLI and the fsspec bindings are implemented and tested — 2,815 tests, of which the great majority need no network, no KDC and no openssl. The remainder are the interoperability suite, which runs against a real xrootd daemon and reads back what xrdcp and xrdfs write, and the parity suite, which runs every operation through this client and the official XRootD bindings and compares the answers field by field. Coverage is 100% of statements and branches across the package, and the wire protocol, the cryptography and the client surface are gated there; ruff and mypy --strict pass clean and are hard gates too. An absolute maintainability gate reports CCN, Cognitive Complexity, NPath, Halstead Volume and maximum nesting per function and file; the same limits apply to all existing and new code, without baseline allowances. The package ships py.typed, and xrdclient.open is overloaded the way the builtin is, so a literal mode tells your type checker whether you get bytes or text.

Staging from tape works in both dialects from the same three method names: prepare/query_prepare/cancel_prepare send kXR_prepare and kXR_QPrep to a root:// endpoint and drive the WLCG Tape REST API - the one FTS and Rucio use - at an http(s)/dav(s) one, and archive_info answers "on disk or still on tape" over either.

Third-party copy works in both dialects from one call: xrdclient.third_party sends the XrdOucTPC rendezvous to a root:// pair and the WLCG COPY dialect to an http(s)/dav(s) one, so the bytes move server to server either way.

Connections are pooled across instances: a FileSystem that closes hands its authenticated connection to the next one opened on the same server by the same person, so a script that constructs one per file logs in once rather than a thousand times. Reuse is keyed on the credentials as well as the endpoint, and a connection that failed is discarded rather than passed on.

A file being streamed can put its bytes on a connection of their own: bind_data_path() binds a second socket to the same session with kXR_bind, and from then on reads and writes travel there while requests keep the control link to themselves. The second connection inherits the session's identity rather than logging in again.

GSI speaks the signed Diffie-Hellman exchange to servers that offer it and can delegate the X.509 proxy - Config(gsi_delegate=True) or XrdSecGSIDELEGPROXY=1, off by default as in the stock client - signing the server's proxy request only once its certificate checks out against the CA directory and the host name. See Authentication.

Not yet: HTTP/2.

Full documentation is in docs/ (mkdocs serve to read it), with SECURITY.md for the threat model, benchmarks/bench.py for the measurements, docs/superpowers/plans/ for the roadmap and docs/superpowers/specs/ for the design.

Licence

LGPL-3.0-or-later: LICENSE is the Lesser terms, which apply on top of the GPL text in COPYING.

Metadata

Release files for xrdclient 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xrdclient 0.1.0
File Size Uploaded
xrdclient-0.1.0.tar.gz 995.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for xrdclient 0.1.0
File Interpreter ABI Platform
xrdclient-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.5 MB

Release files / xrdclient-0.1.0.tar.gz

Download URL xrdclient-0.1.0.tar.gz
Size 995.2 kB
Tags Source
SHA-256 checksum
How to use checksums
111c7acdb101756192b77480138dd76048137ff058950baf5dd650a85ffbeba8
BLAKE2b-256 checksum
How to use checksums
40a0c068491859dd93b4dabe1f0536e044196cde5bd67c47298391dbc6bf2f0f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / xrdclient-0.1.0-py3-none-any.whl

Download URL xrdclient-0.1.0-py3-none-any.whl
Size 490.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d61c5df9c4daf2cd6d8bbc5feaa299211b0a08d4fec91edcc51bbcafff49246a
BLAKE2b-256 checksum
How to use checksums
bb180f539691091ade821f46d4fc5438a1be62f82870dd65c8c713ce68ea1a4e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page