Skip to content

Python API

gfal provides both an async-first client (AsyncGfalClient) and a synchronous wrapper (GfalClient). Both offer the same methods — choose whichever fits your application.

Quick start

Synchronous

import gfal

client = gfal.GfalClient()
phenix_file = (
    "https://eospublic.cern.ch/eos/opendata/phenix/"
    "emcal-finding-pi0s-and-photons/single_cluster_r5.C"
)
phenix_dir = (
    "https://eospublic.cern.ch/eos/opendata/phenix/emcal-finding-pi0s-and-photons/"
)
atlas_file = (
    "https://eospublic.cern.ch/eos/opendata/atlas/rucio/data16_13TeV/"
    "DAOD_PHYSLITE.37019892._000001.pool.root.1"
)

# stat a file
info = client.stat(phenix_file)
print(f"size={info.size}, is_file={info.is_file()}")

# list a directory
for entry in client.ls(phenix_dir):
    print(f"{entry.size:>10}  {entry.info['name']}")

# copy a file from EOS public to local storage
client.copy(atlas_file, "file:///tmp/atlas-phy.root")

# compute a checksum
print(client.checksum("file:///tmp/atlas-phy.root", "MD5"))

Asynchronous

import asyncio
import gfal


async def main():
    client = gfal.AsyncGfalClient()
    phenix_file = (
        "https://eospublic.cern.ch/eos/opendata/phenix/"
        "emcal-finding-pi0s-and-photons/single_cluster_r5.C"
    )
    phenix_dir = (
        "https://eospublic.cern.ch/eos/opendata/phenix/emcal-finding-pi0s-and-photons/"
    )
    atlas_file = (
        "https://eospublic.cern.ch/eos/opendata/atlas/rucio/data16_13TeV/"
        "DAOD_PHYSLITE.37019892._000001.pool.root.1"
    )

    info = await client.stat(phenix_file)
    print(f"size={info.size}")

    entries = await client.ls(phenix_dir)
    for entry in entries:
        print(entry.info["name"])

    await client.copy(atlas_file, "file:///tmp/atlas-phy.root")


asyncio.run(main())

For a medium-sized public source family, the first 37 files matching DAOD_PHYSLITE.37019892.* in https://eospublic.cern.ch/eos/opendata/atlas/rucio/data16_13TeV/ add up to about 5.0 GiB as measured on April 22, 2026.

Local paths

Both "file:///tmp/data.txt" and "/tmp/data.txt" are accepted. Bare paths are automatically converted to file:// URIs internally.


Client configuration

Both clients accept the same parameters:

client = gfal.GfalClient(
    cert="/path/to/cert.pem",  # X.509 client certificate
    key="/path/to/key.pem",  # client key (defaults to cert path)
    timeout=1800,  # global timeout in seconds
    ssl_verify=True,  # TLS certificate verification
)

Or use ClientConfig for reusable configuration:

config = gfal.ClientConfig(
    cert="/path/to/cert.pem",
    key="/path/to/key.pem",
    timeout=3600,
    ssl_verify=True,
)

sync_client = gfal.GfalClient(config=config)
async_client = gfal.AsyncGfalClient(config=config)

ClientConfig

Parameter Type Default Description
cert str \| None None Path to client certificate (PEM)
key str \| None None Path to client key (PEM)
timeout int 1800 Global timeout in seconds
ssl_verify bool True Enable TLS certificate verification
ipv4_only bool False Force IPv4 connections
ipv6_only bool False Force IPv6 connections
app str \| None auto User-agent / application name

File operations

stat(url) → StatResult

Get POSIX-style metadata for a file or directory.

info = client.stat(
    "https://eospublic.cern.ch/eos/opendata/phenix/"
    "emcal-finding-pi0s-and-photons/single_cluster_r5.C"
)
print(f"Size: {info.size} bytes")
print(f"Mode: {oct(info.mode)}")
print(f"Is directory: {info.is_dir()}")
print(f"Modification time: {info.mtime}")

exists(url) → bool

Check whether a file or directory exists.

if client.exists(
    "https://eospublic.cern.ch/eos/opendata/phenix/"
    "emcal-finding-pi0s-and-photons/single_cluster_r5.C"
):
    print("File exists")

ls(url, detail=True) → list

List directory contents.

  • detail=True (default): returns a list of StatResult objects.
  • detail=False: returns a list of file name strings.
# Full details
entries = client.ls(
    "https://eospublic.cern.ch/eos/opendata/phenix/emcal-finding-pi0s-and-photons/"
)
for entry in entries:
    kind = "d" if entry.is_dir() else "f"
    print(f"[{kind}] {entry.info['name']}  ({entry.size} bytes)")

# Names only
names = client.ls(
    "https://eospublic.cern.ch/eos/opendata/phenix/emcal-finding-pi0s-and-photons/",
    detail=False,
)
print(names[:4])

open(url, mode="rb") → file-like

Open a remote file for reading or writing. Returns a file-like object.

# Read
with client.open(
    "https://eospublic.cern.ch/eos/opendata/phenix/"
    "emcal-finding-pi0s-and-photons/single_cluster_r5.C"
) as f:
    content = f.read()

# Write
with client.open("root://server//eos/output.txt", "wb") as f:
    f.write(b"hello world\n")

checksum(url, algorithm) → str

Compute a checksum. Supported algorithms: ADLER32, CRC32, CRC32C, MD5, SHA1, SHA256, SHA512.

md5 = client.checksum(
    "https://eospublic.cern.ch/eos/opendata/phenix/"
    "emcal-finding-pi0s-and-photons/single_cluster_r5.C",
    "MD5",
)
adler = client.checksum(
    "https://eospublic.cern.ch/eos/opendata/atlas/rucio/data16_13TeV/"
    "DAOD_PHYSLITE.37019892._000001.pool.root.1",
    "ADLER32",
)

Directory operations

mkdir(url, mode=0o755, parents=False)

Create a directory.

client.mkdir("root://server//eos/user/j/jdoe/newdir")

# With parents (like mkdir -p)
client.mkdir("root://server//eos/user/j/jdoe/a/b/c", parents=True)

rmdir(url)

Remove an empty directory.

client.rmdir("root://server//eos/user/j/jdoe/emptydir")

rm(url, recursive=False)

Remove a file or directory.

# Remove a file
client.rm("file:///tmp/old.txt")

# Remove a directory tree
client.rm("root://server//eos/user/j/jdoe/olddir", recursive=True)

rename(src_url, dst_url)

Rename or move a file within the same filesystem.

client.rename(
    "root://server//eos/data/old_name.root",
    "root://server//eos/data/new_name.root",
)

chmod(url, mode)

Change file permissions.

client.chmod("root://server//eos/data/file.root", 0o644)

Copy operations

copy(src_url, dst_url, options=None)

Copy a file (or directory tree with recursive=True) between any supported endpoints.

# Simple copy from EOS public to local storage
client.copy(
    "https://eospublic.cern.ch/eos/opendata/phenix/"
    "emcal-finding-pi0s-and-photons/single_cluster_r5.C",
    "file:///tmp/single_cluster_r5.C",
)

# Copy with overwrite and checksum verification
client.copy(
    "https://eospublic.cern.ch/eos/opendata/atlas/rucio/data16_13TeV/"
    "DAOD_PHYSLITE.37019892._000001.pool.root.1",
    "file:///tmp/atlas-phy.root",
    options=gfal.CopyOptions(
        overwrite=True,
        checksum=gfal.ChecksumPolicy("ADLER32"),
    ),
)

# Recursive directory copy
client.copy(
    "https://eospublic.cern.ch/eos/opendata/phenix/emcal-finding-pi0s-and-photons/",
    "file:///tmp/phenix-demo/",
    options=gfal.CopyOptions(recursive=True, create_parents=True),
)

CopyOptions

Parameter Type Default Description
overwrite bool False Overwrite existing destination
create_parents bool False Create parent directories at destination
recursive bool False Copy directories recursively
timeout int \| None None Per-transfer timeout in seconds
checksum ChecksumPolicy \| None None Checksum verification policy
tpc str "auto" Third-party copy: "auto", "always", "never", "only"
tpc_direction str "pull" TPC direction: "pull" or "push"
abort_on_failure bool False Stop on first error during recursive copy
preserve_times bool False Preserve modification timestamps
compare str \| None None Skip if destination matches: "size", "size_mtime", "checksum", "none"
dry_run bool False Preview without executing
just_copy bool False Skip all preparation/validation steps
scitag int \| None None WLCG SciTag identifier (65–65535)

ChecksumPolicy

Parameter Type Default Description
algorithm str required Algorithm: ADLER32, MD5, SHA256, etc.
mode str "both" Verify: "both", "source", "target"
expected_value str \| None None Expected hash value for validation

Dry-run example

client.copy(
    "https://eospublic.cern.ch/eos/opendata/phenix/emcal-finding-pi0s-and-photons/",
    "file:///tmp/phenix-demo/",
    options=gfal.CopyOptions(recursive=True, dry_run=True),
)
# Nothing is actually copied — preview only

Third-party copy (TPC)

When both source and destination are remote (e.g. two XRootD servers, or HTTP endpoints supporting WebDAV COPY), gfal can instruct the servers to transfer data directly without routing through the client:

client.copy(
    "root://server-a//eos/data/file.root",
    "root://server-b//eos/data/file.root",
    options=gfal.CopyOptions(tpc="only"),
)

Background transfers

start_copy(src_url, dst_url, options=None) → TransferHandle

Start a copy in a background thread. Returns a TransferHandle for monitoring.

handle = client.start_copy(
    "https://eospublic.cern.ch/eos/opendata/atlas/rucio/data16_13TeV/"
    "DAOD_PHYSLITE.37019892._000001.pool.root.1",
    "file:///tmp/atlas-phy.root",
    options=gfal.CopyOptions(overwrite=True),
)

# Do other work while the transfer runs...
print(f"Transfer done? {handle.done()}")

# Wait for completion
handle.wait(timeout=300)

TransferHandle

Method Description
done() → bool Check if the transfer has completed
wait(timeout=None) Block until transfer completes or timeout expires
wait_async(timeout=None) Async version of wait()
cancel() Cancel the transfer

Extended attributes

getxattr(url, name) → str

value = client.getxattr("root://server//eos/data/file.root", "xroot.checksum")

setxattr(url, name, value)

client.setxattr("root://server//eos/data/file.root", "user.tag", "important")

listxattr(url) → list[str]

attrs = client.listxattr("root://server//eos/data/file.root")

xattrs(url) → dict[str, str]

Get all extended attributes as a dictionary:

all_attrs = client.xattrs("root://server//eos/data/file.root")
for name, value in all_attrs.items():
    print(f"{name} = {value}")

Progress callbacks

Both copy() and start_copy() accept optional callbacks for monitoring transfers:

def on_progress(bytes_transferred: int) -> None:
    print(f"Transferred: {bytes_transferred} bytes")


def on_start() -> None:
    print("Transfer started")


def on_mode(mode: str) -> None:
    print(f"Transfer mode: {mode}")  # "streamed", "tpc-pull", "tpc-xrootd"


client.copy(
    "https://eospublic.cern.ch/eos/opendata/atlas/rucio/data16_13TeV/"
    "DAOD_PHYSLITE.37019892._000001.pool.root.1",
    "file:///tmp/atlas-phy.root",
    progress_callback=on_progress,
    start_callback=on_start,
    transfer_mode_callback=on_mode,
)

Error handling

All operations raise typed exceptions that inherit from GfalError (which inherits from OSError):

Exception errno Meaning
GfalFileNotFoundError ENOENT File or directory not found
GfalPermissionError EACCES Access denied
GfalFileExistsError EEXIST File already exists
GfalNotADirectoryError ENOTDIR Not a directory
GfalIsADirectoryError EISDIR Is a directory
GfalTimeoutError ETIMEDOUT Operation timed out
GfalError varies Base class for all gfal errors
import gfal

client = gfal.GfalClient()

try:
    client.stat("file:///nonexistent")
except gfal.GfalFileNotFoundError:
    print("File not found")
except gfal.GfalPermissionError:
    print("Permission denied")
except gfal.GfalError as e:
    print(f"Error (errno={e.errno}): {e}")

StatResult

Returned by stat() and ls() (when detail=True).

Property Type Description
size int File size in bytes
mode int POSIX file mode
uid int Owner user ID
gid int Owner group ID
nlink int Number of hard links
mtime float Modification time (UNIX timestamp)
atime float Access time (UNIX timestamp)
ctime float Change time (UNIX timestamp)
info dict Raw fsspec info dictionary
Method Return Description
is_dir() bool True if the entry is a directory
is_file() bool True if the entry is a regular file

Supported URL schemes

Scheme Description Requirements
file:// Local filesystem Built-in
/path (bare path) Local filesystem (auto-converted to file://) Built-in
http://, https:// HTTP/WebDAV Built-in
dav://, davs:// WebDAV (converted to http:///https://) Built-in
root:// XRootD Built-in adapter; xrootd client bindings required

Complete async example

import asyncio
import gfal


async def backup_directory():
    client = gfal.AsyncGfalClient(
        cert="/tmp/x509up_u1000",
        timeout=3600,
    )

    src = "root://server//eos/data/analysis/"
    dst = "file:///tmp/backup/"

    # Check source exists
    if not await client.exists(src):
        print("Source directory not found")
        return

    # Copy with checksum verification
    await client.copy(
        src,
        dst,
        options=gfal.CopyOptions(
            recursive=True,
            create_parents=True,
            overwrite=True,
            checksum=gfal.ChecksumPolicy("ADLER32"),
        ),
    )

    # Verify
    entries = await client.ls(dst)
    print(f"Backed up {len(entries)} items")


asyncio.run(backup_directory())