Upgrade from 2.x

3.0.0 is a breaking release: it removes the old ways instead of keeping them next to the new ones. This page lists what to change in code written for 2.x, by topic. The release history has every change with the reason for it.

A good way to upgrade:

  1. Require Python 3.11 or newer, and s3pathlib>=3.0.0,<4.0.0.

  2. Change the imports (section 1) and the clients (section 2); most code then runs.

  3. Run your tests with -W error::DeprecationWarning, to find the code that 4.0.0 will break (section 3).

  4. Read the other sections for the behavior that changed.

If you use Claude Code, copy the s3pathlib-upgrade-to-3 folder into the .claude/skills/ folder of your project and run /s3pathlib-upgrade-to-3: it scans your code and notebooks for 2.x usage, rewrites the renames, and walks you through the behavior changes.

1. Imports

Import every public name from s3pathlib.api. The old top level import raises an ImportError that says what to write instead:

[1]:
try:
    from s3pathlib import S3Path
except ImportError as e:
    print(e)
'from s3pathlib import S3Path' was removed in 3.0.0; use 'from s3pathlib.api import S3Path'
[2]:
from s3pathlib.api import S3Path

2.x

3.x

from s3pathlib import S3Path (and context, and_, or_, not_, …)

from s3pathlib.api import S3Path

import s3pathlib, then s3pathlib.utils

from s3pathlib import utils (import s3pathlib no longer imports the submodules)

s3pathlib.api.is_bucket_exists, upload_dir, paginate_list_objects_v2, …

s3pathlib.api.better_client.is_bucket_exists, …

from s3pathlib import context

from s3pathlib.api import s3_context

from s3pathlib.aws import Context

from s3pathlib.aws import S3Context

from s3pathlib.compat import ...

removed

Python 3.9 / 3.10

Python 3.11 or newer

boto-session-manager, func-args, pathlib-mate installed with s3pathlib

no longer dependencies: add them to your own if you use them

2. Clients

The bsm argument is now s3_client, and it takes a boto3 S3 client only. The global context is now s3_context, and it only holds the default S3 client.

[3]:
p = S3Path("my-bucket", "file.txt")
try:
    p.read_text(bsm=None)
except TypeError as e:
    print(e)
ReadAndWriteAPIMixin.read_text() got an unexpected keyword argument 'bsm'

2.x

3.x

p.read_text(bsm=bsm) (a BotoSesManager)

p.read_text(s3_client=bsm.s3_client)

p.read_text(bsm=s3_client)

p.read_text(s3_client=s3_client)

context.attach_boto_session(boto_ses)

s3_context.attach_s3_client(boto_ses.client("s3"))

context.aws_account_id

boto_ses.client("sts").get_caller_identity()["Account"]

context.aws_region

s3_context.s3_client.meta.region_name

context.boto_ses, context.sts_client, attach_sts_client

removed: keep your own session and clients

sync(...) used AWS_PROFILE or the default credentials

sync uses the identity of the S3 client, like every other method

See Clients and Errors.

3. Paths

Join onto a folder. A path without a trailing / is a file, and a file has no sub path. Joining onto a file, and relative_to with a file as the base, still work in 3.x but emit a DeprecationWarning, and raise S3PathIsNotFolderError in 4.0.0. The most common case is a chain, where the middle part has no /:

[4]:
import warnings

with warnings.catch_warnings():
    warnings.simplefilter("error", DeprecationWarning)
    try:
        S3Path("my-bucket") / "folder" / "file.txt"
    except DeprecationWarning:
        print("deprecated: joins onto the file 'folder'")
deprecated: joins onto the file 'folder'

Any of these is right:

[5]:
S3Path("my-bucket") / "folder/" / "file.txt"
[5]:
S3Path('s3://my-bucket/folder/file.txt')
[6]:
S3Path("my-bucket").joinpath("folder", "file.txt")
[6]:
S3Path('s3://my-bucket/folder/file.txt')

2.x

3.x

S3Path("b") / "folder" / "file.txt"

S3Path("b") / "folder/" / "file.txt", or S3Path("b").joinpath("folder", "file.txt")

S3Path("b", "folder").joinpath("x")

S3Path("b", "folder/").joinpath("x"), or .to_dir().joinpath("x")

p.relative_to(S3Path("b", "folder"))

p.relative_to(S3Path("b", "folder/"))

S3Path("b", "a/").relative_to(S3Path("b", "a")) gave void

raises ValueError

p.change(new_ext="png") and other invalid arguments gave an odd path

raises ValueError: new_ext starts with ., new_dirname needs a parent folder (use new_dirpath)

p.my_attr = 1

raises AttributeError (S3Path has __slots__); keep your data in a dict keyed by the path

sorted(paths) put a folder after names such as a-b

a folder comes right before its content, like pathlib; use sorted(paths, key=...) with p.key for the S3 order

p.get_regional_console_url(region)

p.get_console_url(region=region)

p.us_gov_cloud_console_url

p.get_console_url(partition="aws-us-gov")

p.s3_select_console_url and the other S3 Select URLs

removed (S3 Select is closed to new customers)

S3Path("https://...") was accepted

raises S3ValidationError

See Path Manipulation.

4. Metadata

A path’s metadata is a snapshot of one object version, taken by the call that produced the path; see Metadata and Tags.

2.x

3.x

p.exists() or p.read_bytes() refreshed the loaded metadata

they only fill an empty snapshot; call p.head_object() or p.clear_cache()

changing the dict of p.metadata / p.response changed the cache

they return a copy

a listed old version loaded its missing metadata from the latest version

it loads them from its own version

p.update_metadata(...) (always raised NotImplementedError)

removed: copy the object onto itself with metadata=

except S3FileNotExist around the metadata of a delete marker

it returns None

5. Read, write and open

2.x

3.x

p.open(..., ignore_ext=True)

p.open(..., compression="disable")

p.open("r", range=...), part_number=... (never had an effect)

p.read_bytes(range=...), or f.seek(offset) then f.read(n)

p.open("w", if_match=...) was ignored

needs multipart_upload=False (ValueError otherwise)

p.open(...) on a key that contains ?versionId=

raises ValueError; use read_bytes / write_bytes

p.upload_file(...) returned None

returns the new S3Path

p.exists(version_id=...) on a folder or a bucket

raises ValueError

6. List

2.x

3.x

S3Path("b", "file.txt").iter_objects() listed every key starting with file.txt

raises S3PathIsNotFolderError; to list by a string prefix, use better_client.paginate_list_objects_v2(s3_client, bucket, prefix=...)

iterdir() gave the folders first, then the files

gives them in S3 key order, mixed

filter_by_ext("csv") matched nothing

raises ValueError; write filter_by_ext(".csv")

a key such as a//b.txt was listed as the path of a/b.txt

raises S3ValidationError; list such keys with better_client

list_object_versions() repeated the entries of earlier pages

each entry once, in S3 order

paginate_list_objects_v2(...).common_prefixs()

.common_prefixes()

7. Copy, move and delete

2.x

3.x

res = p.copy_file(dst), then res["CopyObjectResult"]

returns the new S3Path: new = p.copy_file(dst), then new.etag

except ClientError around a copy of a missing source

except exc.S3FileNotExist

p.copy_dir(p / "sub/"), or a move into an overlapping folder

raises ValueError

copy_dir skipped the folder markers

copies every key, markers included

p.delete() returned p and cleared its cache

returns a new S3Path (the delete marker with versioning); p does not change

p.delete(version_id=v, is_hard_delete=True) deleted every version

raises ValueError: give one of them

p.delete(is_hard_delete=True) on data/file.txt also deleted data/file.txt.bak

deletes this exact key only

p.delete_if_exists()

p.delete() (deleting what does not exist is not an error)

delete(check_sum_algorithm=...)

delete(checksum_algorithm=...)

See Copy, Move and Delete and Versioning.

8. Sync

2.x

3.x

except SystemError around sync

except exc.S3SyncError (it has returncode, stderr and command)

sync(..., quite=True)

sync(..., quiet=True) (quite is deprecated)

S3Path("b", "file.txt").sync_to(...)

raises S3PathIsNotFolderError: sync works on folders

--include came before --exclude

every exclude comes first, then every include

See Sync.

9. Errors

2.x

3.x

except ClientError around the 403 of S3Path("bucket").exists()

except exc.S3PermissionDenied (or PermissionError)

except ValueError around fname / ext of the void path

except exc.S3PathIsNotFileError (or TypeError)

except KeyError around S3Path.from_dict

except exc.S3ValidationError (or ValueError)

except validate.S3BucketValidationError

except exc.S3ValidationError (or ValueError)

exc._S3PathTypeError

exc.S3PathTypeError

Every exception of s3pathlib now inherits from exc.S3PathlibError. See Clients and Errors.

10. Helpers

2.x

3.x

utils.md5_binary(b), sha256_binary, hash_binary, hash_file

hashlib.md5(b).hexdigest(), …

from s3pathlib.marker import deprecate_v1 (and deprecate_v2, warn_deprecate, warn_beta)

from s3pathlib.marker import deprecated

better_client.delete_object_versions(prefix="data/file.txt")

better_client.delete_object_all_versions(key="data/file.txt")

exc.ensure_one_and_only_one_not_none, exc.ensure_all_none

removed

tests that subclass s3pathlib.tests.mock.BaseTest and use cls.bsm

s3pathlib.tests.mock_aws.BaseTest, with self.s3_client, cls.aws_account_id, cls.aws_region