Skip to content

Migration Guide

This guide helps you migrate to newer versions of universal-pathlib.

Warning

Please open an issue if you run into issues when migrating to a newer UPath version and this guide is missing information.

Migrating to v0.3.0

Version 0.3.0 introduced a breaking change to fix a longstanding bug related to os.PathLike protocol compliance. This change affects how UPath instances work with standard library functions that expect local filesystem paths.

Background: PathLike Protocol and Local Filesystem Paths

In Python, os.PathLike objects and pathlib.Path subclasses represent local filesystem paths. The standard library functions like os.remove(), shutil.copy(), and similar expect paths that point to the local filesystem. However, UPath implementations like S3Path or MemoryPath do not represent local filesystem paths and should not be treated as such.

Prior to v0.3.0, all UPath instances incorrectly implemented os.PathLike, which could lead to runtime errors when non-local paths were passed to functions expecting local paths. Starting with v0.3.0, only local UPath implementations (PosixUPath, WindowsUPath, and FilePath) implement os.PathLike.

Migration Strategies

If your code passes UPath instances to functions expecting os.PathLike objects, you have several options:

Option 1: Explicitly Request a Local Path (Recommended)

import os
from upath import UPath

# Explicitly specify the file:// protocol to get a FilePath instance
path = UPath(__file__, protocol="file")
assert isinstance(path, os.PathLike)  # True

# Now you can safely use it with os functions
os.remove(path)

Option 2: Use UPath's Filesystem Operations

from upath import UPath

# Works for any UPath implementation, not just local paths
path = UPath("s3://bucket/file.txt")
path.unlink()  # UPath's native unlink method

Option 3: Use Type Checking with upath.types

For code that needs to work with different path types, use the type hints from upath.types to properly specify your requirements:

import os
from upath import UPath
from upath.types import (
    JoinablePathLike,
    ReadablePathLike,
    WritablePathLike,
)

def read_only_local_file(path: os.PathLike) -> str:
    """Read a file on the local filesystem."""
    with open(path) as f:
        return f.read()

def write_only_local_file(path: os.PathLike, content: str) -> None:
    """Write to a file on the local filesystem."""
    with open(path, 'w') as f:
        f.write(content)

def read_any_file(path: ReadablePathLike) -> str:
    """Read a file on any filesystem."""
    return UPath(path).read_text()

def write_any_file(path: WritablePathLike, content: str) -> None:
    """Write a file on any filesystem."""
    UPath(path).write_text(content)

Example: Incorrect Code That Would Fail

The following example shows code that would incorrectly work in v0.2.x but properly fail in v0.3.0:

import os
from upath import UPath

# This creates a MemoryPath, which is not a local filesystem path
path = UPath("memory:///file.txt")

# In v0.2.x this would incorrectly accept the path and fail at runtime
# In v0.3.0 this correctly fails at type-check time
os.remove(path)  # TypeError: expected str, bytes or os.PathLike, not MemoryPath

Working with Polars and Object Store

When using UPath with Polars, be aware that Polars uses Rust's object-store library instead of fsspec. This requires special handling to preserve storage options.

Don't Rely on Implicit String Conversion

Avoid passing UPath instances directly to functions that implicitly cast them to strings via os.fspath() or os.path.expanduser(). This loses storage options and can lead to authentication failures.

Problematic Pattern:

import polars as pl
from upath import UPath

# This loses storage_options when implicitly converted to string!
path = UPath('s3://bucket/file.parquet', anon=True)
df = pl.read_parquet(path)  # anon=True is lost!

Recommended Approaches:

Option 1: Use fsspec/s3fs as the Backend (via file handle)

import polars as pl
from upath import UPath

path = UPath("s3://bucket/file.parquet", anon=True)

# Open file handle with fsspec, preserving storage_options
df = pl.scan_parquet(path.open('rb'))

Option 2: Use Polars' Native Rust Backend (with object-store options)

import polars as pl
from upath import UPath

path = UPath("s3://bucket/file.parquet", key="ACCESS_KEY", secret="SECRET_KEY")

# Convert fsspec storage_options to object-store format
object_store_options = {
    "aws_access_key_id": path.storage_options.get("key"),
    "aws_secret_access_key": path.storage_options.get("secret"),
    # Add other options as needed
}

df = pl.scan_parquet(path.as_uri(), storage_options=object_store_options)

Storage Options Mapping

Polars uses object-store configuration keys, which differ from fsspec's naming:

fsspec/s3fs object-store
key aws_access_key_id
secret aws_secret_access_key
endpoint_url aws_endpoint
region_name aws_region

See also: pola-rs/polars#24921

Extending UPath via _protocol_dispatch=False

If you previously used _protocol_dispatch=False to enable extension of the UPath API, we now recommend subclassing upath.extensions.ProxyUPath. See the advanced usage documentation for examples.

Migrating to v0.2.0

_FSSpecAccessor Subclasses with Custom Filesystem Access Methods

If you implemented a custom accessor subclass, override the corresponding UPath methods in your subclass directly:

# OLD: v0.1.x
from upath.core import UPath, _FSSpecAccessor

class MyAccessor(_FSSpecAccessor):
    def exists(self, path, **kwargs):
        # custom logic
        pass

class MyPath(UPath):
    _default_accessor = MyAccessor


# NEW: v0.2.0+
from upath import UPath

class MyPath(UPath):
    def exists(self, *, follow_symlinks=True):
        # custom logic
        pass

_FSSpecAccessor Subclasses with Custom __init__ Method

If you implemented a custom __init__ method for your accessor subclass to customize fsspec filesystem instantiation, use the new _fs_factory or _parse_storage_options classmethods:

# OLD: v0.1.x
import fsspec
from upath.core import UPath, _FSSpecAccessor

class MyAccessor(_FSSpecAccessor):
    def __init__(self, parsed_url, **kwargs):
        # custom filesystem setup
        super().__init__(parsed_url, **kwargs)

class MyPath(UPath):
    _default_accessor = MyAccessor


# NEW: v0.2.0+
from upath import UPath

class MyPath(UPath):
    @classmethod
    def _fs_factory(cls, protocol, storage_options):
        # custom filesystem setup
        return super()._fs_factory(protocol, storage_options)

Access to ._accessor

The _accessor attribute and the _FSSpecAccessor class are deprecated. Use UPath().fs to access the underlying filesystem:

# OLD: v0.1.x
from upath import UPath

class MyPath(UPath):
    def mkdir(self, mode=0o777, parents=False, exist_ok=False):
        self._accessor.mkdir(self.path, **kwargs)


# NEW: v0.2.0+
from upath import UPath

class MyPath(UPath):
    def mkdir(self, mode=0o777, parents=False, exist_ok=False):
        self.fs.mkdir(self.path, **kwargs)

Private Attributes to Public API

Move from deprecated private attributes to public API:

Deprecated v0.2.0+
UPath()._path UPath().path
UPath()._kwargs UPath().storage_options
UPath()._drv UPath().drive
UPath()._root UPath().root
UPath()._parts UPath().parts

Access to ._url

The ._url attribute will likely be deprecated once UPath() has support for URI fragments and query parameters through a public API. If you need this functionality, please open an issue.

Custom Path Flavours

The _URIFlavour class was removed. The internal FSSpecFlavour in upath._flavour is experimental. If you need custom path flavour functionality, please open an issue to discuss maintainable solutions.