Release and Version History¶
- orphan:
Backlog (TODO)¶
Features and Improvements
make
s3pathlib.aws.Contextmulti-thread safe.
Minor Improvements
Bugfixes
Miscellaneous
3.0.0 (2026-10-13)¶
Why 3.0.0
s3pathlib started in January 2022 (1.x) and grew through 2.x for three years. Each release added features on top of what was there, and the library collected technical debt that every new feature had to work around:
A flat namespace and glue code. Everything was imported from the top level
s3pathlib, with the errors of a missing dependency swallowed. Behind it sat three third party helpers (boto-session-managerfor thebsmargument,func-argsfor the “not given” value,pathlib-mate), a compatibility layer for oldsmart_openversions, home made deprecation helpers, and a globalcontextthat managed a boto session.Features the world moved on from. Amazon S3 Select was closed to new customers, the console URL had one property per partition, and ARNs and URLs only knew the
awspartition.Behavior that was never defined. Arguments were accepted and silently ignored, an
S3Pathcould be in states that mean nothing (a “bucket that is a file”), wrong arguments raised a bareTypeError, listing a file listed every key with that string prefix, and the cached metadata could describe a different object version than the one you held. Several of these lost or hid data.An old toolchain. Python 3.9 and 3.10, poetry, and type hints of the old style.
3.0.0 pays this debt off at once, instead of carrying it into the next years. It is a breaking release on purpose: it drops the old way rather than keep it next to the new one.
The library is built on boto3 alone: a plain boto3 S3 client is the only thing you pass in (
s3_client), and the errors of AWS calls are raised as boto3 raises them, unless s3pathlib has a rule or feature of its own.Every
S3Pathis in one of six valid states, and a method either does what its arguments say or raises; nothing is silently ignored.One public entry point,
s3pathlib.api, pluss3pathlib.api.better_clientfor the thin wrappers of boto3; Python 3.11 and newer, with modern type hints and standard PEP 702 deprecations.The new behavior is documented in the docstrings and tested: the whole library is covered by a large test suite (100% branch coverage) that runs against moto and, on CI, against real AWS on Linux and Windows (Python 3.11 to 3.14).
The S3Path API still looks like pathlib, so most code only needs the changed import and bsm= renamed to s3_client=. Go through Breaking Changes below for the rest; they are grouped by topic and each says what to change.
Breaking Changes
Imports. Import public names with
from s3pathlib.api import .... The top level names ofs3pathlib(S3Path,s3_context,ContentTypeEnum,validate_s3_bucket/key/uri/arn,and_,or_,not_) are removed, and the old import raises anImportErrorthat says so.import s3pathlibonly gives the version info and no longer imports the library (2.x imported everything, and silently left names out when a dependency was missing); submodules such asfrom s3pathlib import utilsstill work. The low level helpers that weres3pathlib.api.*in 2.x (is_bucket_exists,upload_dir,paginate_list_objects_v2, …) moved tos3pathlib.api.better_client.*.Python and dependencies. Drop Python 3.9 and 3.10; Python 3.11 to 3.14 are supported and tested. Raise the minimum versions to
boto3>=1.41.0andsmart-open>=7.0.0,<9.0.0. The runtime dependencies are nowboto3,smart-open,iterproxyandtyping-extensions(Python < 3.13 only);pathlib-mate,func-argsandboto-session-managerare removed, so add them to your own dependencies if your code imports them. Remove thes3pathlib.compatmodule, which only supported oldsmart_openversions.No ``__dict__``. Every
S3Pathclass declares__slots__(2.x declared it on the base class only, so it had no effect). Setting an attribute thatS3Pathdoes not define, e.g. a typo such asp.verison_id = ..., raisesAttributeError. Keep your own data next to the path, e.g. in adictkeyed by the path.Clients. Rename
s3pathlib.contexttos3_context(from s3pathlib.api import s3_context) and its classContexttoS3Context:contextis shadowed by thecontextargument of AWS Lambda handlers (def lambda_handler(event, context)).s3_contextonly holds the default S3 client: removeattach_boto_session,attach_sts_client,boto_ses,sts_client,aws_account_idandaws_region. Replacecontext.attach_boto_session(boto_ses)withs3_context.attach_s3_client(boto_ses.client("s3")); get the account id withboto_ses.client("sts").get_caller_identity()["Account"]and the region froms3_client.meta.region_name. Rename thebsmargument of everyS3Pathmethod that calls AWS tos3_client. It only accepts a boto3 S3 client; anything else, such as aBotoSesManager(accepted in 2.x), raisesTypeErrorbefore any call: replacebsm=bsmwiths3_client=bsm.s3_client.Exceptions and validation.
Validators raise
s3pathlib.exc.S3ValidationError(aValueError): removeS3BucketValidationError,S3KeyValidationErrorand the key charset constants (safe_key_charset,req_special_handling_key_charset,to_avoid_key_charset,valid_key_charset) froms3pathlib.validate.utils.ensure_s3_objectandensure_s3_dirraise it too.Remove
s3pathlib.exc.ensure_one_and_only_one_not_noneandensure_all_none, and the two placeholderss3pathlib.utils.validate_s3_bucketandvalidate_s3_keythat did nothing. Renames3pathlib.exc._S3PathTypeErrorto the publicS3PathTypeError.valid_bucket_charsetandletter_and_numberofs3pathlib.validateare now private.s3pathlibno longer checks bucket naming rules or the key length itself; S3 enforces them when you call the API.S3Path(...),from_s3_uri,from_s3_arnandfrom_bucketonly check the format (a well formed URI / ARN and a non-empty bucket). The functions ofs3pathlib.validateare optional tools the library does not call.validate_s3_bucketnow checks the rules for a new general purpose bucket as documented by AWS: it also rejects two adjacent periods, the reserved prefixessthree-andamzn-s3-demo-, and the reserved suffixes-s3alias,--ol-s3,.mrap,--x-s3and--table-s3.S3Path(...)andutils.split_s3_urireject a malformed URI such ashttps://bucket/keyors3:/bucket/keywithS3ValidationError, instead of silently parsing it into a wrong bucket and key. An empty bucket (S3Path(""),s3://) raises it too, instead ofIndexError.better_client.is_bucket_exists,head_objectandis_object_exists(and theS3Pathmethods built on them:exists(), the metadata properties, …) raiseS3PermissionDenied(aPermissionError) when S3 answers 403: the bucket belongs to another account, or you lack the permission. This holds even withignore_not_found=True, as no permission is not “not found”. TheClientErroris now the__cause__.
Path model. An
S3Pathis always in one of six valid states (file, dir, bucket, relative file, relative dir, void; sees3pathlib.core.base), checked whenever a path is built.S3Path("bucket").to_file()raisesS3PathIsNotFileError(aTypeError); it used to return a “bucket that is a file”.S3Path.from_dictraisesS3ValidationErrorfor data that is not a valid state (an empty bucket, a part containing/, a bucket root withis_dir=False,partsthat is a string), and for a missing key or a non-dict, instead of building a broken path such ass3:///aor raisingKeyError/TypeError.S3Path.changerejects withValueErrorwhat used to give a silently wrong path: anew_extthat is not empty and does not start with.(new_ext="png"turnedc.jpgintocpng),new_fnameornew_extcontaining/, an emptynew_basename, andnew_dirnameon a path without parent dir.new_fname/new_exton a dir raiseValueErrorwith a message instead of a bareTypeError.fnameandextraiseS3PathIsNotFileErroron anything that is not a file (a bareTypeErroron a dir or a bucket and a bareValueErroron the void path before).relative_to(and-) is the exact inverse of/:base / target.relative_to(base) == target. When both paths have the same parts but one is a file and the other a dir (S3Path("b", "a/").relative_to(S3Path("b", "a"))), it raisesValueError; it used to return the void path.exists(version_id=...)andensure_not_exists(version_id=...)raiseValueErroron a bucket or a folder, before calling AWS;version_idwas silently ignored (S3Path("bucket").exists(version_id="v1")wasTrue).Remove
S3Path.delete_if_exists(deprecated since 2.0.1; usedelete),S3Path.update_metadata(it only raisedNotImplementedError; usewrite_bytes(..., metadata=...)orcopy_file(..., metadata=...)), anddelete_bucketno longer clears the metadata snapshot of the path in place.
Metadata snapshot. The metadata loaded from S3 on an
S3Path(size,etag,last_modified_at,version_id,expire_at,metadata) is a snapshot of one object version at one moment, sees3pathlib.core.metadata.exists()andread_bytes()/read_text()only fill anS3Paththat has no metadata yet; they no longer replace loaded metadata. To load the latest metadata, callhead_object()(orclear_cache()).A missing field of a partial snapshot (e.g. the user metadata of a listed object) is loaded from the same object version, not the latest one. When the object was overwritten after it was listed, the new
S3ObjectChangedErroris raised instead of mixing the data of two objects.The properties of a delete marker never call AWS:
sizeis0, andetag,metadataandexpire_atareNone(metadataused to load the latest version and raiseS3FileNotExist).responseandmetadata, and the result ofhead_object(), are copies: changing them no longer changes the loaded metadata.
Listing.
iter_objects()anditerdir()on a file raiseS3PathIsNotFolderErrorbefore calling AWS, likecount_objects()andcalculate_total_size(); they used to list the keys that start with the key of the file (file.txt.bak,file.txt/...). To list by a string prefix, usebetter_client.paginate_list_objects_v2(prefix=...).iterdir()returns files and folders in the S3 order (by key, mixed), likeiter_objects(); the order used to change withbatch_size.iter_objects,iterdirandlist_object_versionsraiseS3ValidationErrorwhile iterating when S3 returns a key thatS3Pathcannot represent (consecutive/such asa//b, or a leading/).S3Pathjoins and drops those/, so the listed path was the path of another key (a/b) with the snapshot of the first one, anddelete()deleted the wrong object. List such keys withbetter_clientor boto3;copy_dir,move_toanddeleteof a folder work on the keys S3 returns and are not affected.S3PathIterProxy.filter_by_extraisesValueErrorfor an extension that is not""and does not start with.("csv"), which matched nothing.better_client.delete_object_versionsrequiresprefixto end with/(or be""for the whole bucket), likedelete_dir, and raisesS3ValidationErrorotherwise:"data/file.txt"also matcheddata/file.txt.bakand deleted all its versions. To delete all versions of one object, use the newbetter_client.delete_object_all_versions.Rename the misspelled
common_prefixs()andcontents_and_common_prefixs()of the result ofbetter_client.paginate_list_objects_v2tocommon_prefixes()andcontents_and_common_prefixes().
Open.
Remove the
ignore_ext,rangeandpart_numberarguments ofS3Path.open(passing them raisesTypeError). None had an effect:ignore_extwas removed from smart_open 6, and smart_open sets its ownRangeto stream and seek.openreads and writes whole objects; to read a part useread_bytes(range=...)/read_bytes(part_number=...), to turn off compression usecompression="disable".A key containing
?versionId=or&versionId=(page?versionId=1) raisesValueError; useread_bytes/write_bytes. smart_open 8 takes that part of the URI as a version id and removes it from the key, even whenversion_idis given, so writing failed and reading silently read another object. The key is rejected with every smart_open version, so code does not break when smart_open is upgraded. Other?in a key still work.Arguments that were silently ignored when writing now raise
ValueError:version_id;content_length,content_md5andchecksum_crc32/checksum_crc32c/checksum_sha1/checksum_sha256withmultipart_upload=True(the default), which onlyput_object(multipart_upload=False) supports; and the read argumentsif_modified_since,if_unmodified_since,response_*andchecksum_mode.if_matchandif_none_matchnow work withmultipart_upload=False(if_none_match="*"used to replace an existing object without an error) and raiseValueErrorwith the default multipart upload. A failed condition raisesPreconditionFailedas the__cause__of smart_open’sValueError.
Copy.
copy_filereturns a newS3Pathof the copy (withetag,last_modified_atandversion_idfrom the response), likewrite_bytes; it returned thecopy_objectresponsedict.Copying a missing source (
copy_file,copy_to,move_to) always raisesS3FileNotExist, also for a missingversion_id. It wasNoSuchKey, andS3FileNotExistonce a header ormetadatawas given; other errors ofcopy_objectare still raised as is.Remove the
content_md5argument ofcopy_file,copy_dir,copy_toandmove_to:copy_objecthas no such parameter, so it always raisedParamValidationError.copy_dirandmove_toraiseValueErrorwhen the source and target folders overlap (the same folder, or one inside the other, in the same bucket), as copying could overwrite source objects before they are copied;move_toalso raises it for a file moved to itself. A folder rejectscopy_source_if_match,copy_source_if_none_match,copy_source_if_modified_sinceandcopy_source_if_unmodified_since(they applied to every object, and an ETag matches one object only), andcopy_torejectsversion_idon a folder (it was ignored).copy_dircopies every key under the folder, folder markers included (the marker of the folder itself becomes the marker of the target), so empty folders are copied; the returned count includes them. The marker is skipped when the target is a bucket, which has none.
Delete.
deletealways returns a newS3Pathand no longer changes the path it is called on: with versioning, the returned path is the new delete marker, or the deleted version whenversion_idis given. It used to return the path itself with its cached metadata cleared.deleteraisesValueErrorforversion_idon a folder or together withis_hard_delete=True(it was ignored). Rename thecheck_sum_algorithmargument tochecksum_algorithminS3Path.delete,S3Path.put_bucket_versioning,better_client.delete_dir,delete_object_versionsanddelete_object_all_versions, as in every other method and in boto3; the old name raisesTypeError.Sync.
sync,sync_fromandsync_torunaws s3 syncwith the identity of the S3 client of the call, like every other method: its credentials and region, and its endpoint when it is not an AWS endpoint. Environment variables such asAWS_PROFILEno longer decide the credentials; attach a client of that profile instead. The command used the default credentials of the environment and ignored the attached client. Sees3pathlib.core.syncfor the limits (temporary credentials are not refreshed during the sync).A failed command raises
s3pathlib.exc.S3SyncError(with the exit code and stderr) instead ofSystemError. A file raisesS3PathIsNotFolderErrorand a relative path or the void pathS3PathTypeError, asaws s3 syncsyncs folders.By default (
quiet=True) the command runs with--only-show-errorsinstead of--quiet: nothing is printed either way on success, but--quietalso hid the error.All
--excludecome before all--include(the CLI lets the later filter win), soexclude="*", include="*.csv"syncs the csv files; it synced nothing, as--includecame first.
Console URLs and S3 Select. Remove
S3Path.get_regional_console_url(region),us_gov_cloud_console_urlandget_regional_us_gov_cloud_console_url(region); useS3Path.get_console_url(region=..., partition=...), e.g.p.get_console_url(partition="aws-us-gov").console_urlis kept. Remove theis_us_gov_cloudargument ofutils.make_s3_console_url(usepartition="aws-us-gov"). Remove the S3 Select console URLs (s3_select_console_url,get_regional_s3_select_console_url,s3_select_us_gov_cloud_console_url,get_regional_s3_select_us_gov_cloud_console_urlandutils.make_s3_select_console_url): AWS closed Amazon S3 Select to new customers on July 25, 2024.Other removals. Remove
deprecate_v1,deprecate_v2,warn_deprecateandwarn_betafroms3pathlib.marker(use the newdeprecated), andhash_binary,md5_binary,sha256_binary,hash_fileandDEFAULT_CHUNK_SIZEfroms3pathlib.utils(use the standard libraryhashlib).
Features and Improvements
Public API. Add the
s3pathlib.apimodule as the public entry point. It exports every public name, including theexcmodule,S3PathIterProxy(returned byiter_objects,iterdirandlist_object_versions),AwsPartitionEnumandS3PathDict(aTypedDictfor the dict ofto_dict/from_dict). A test locks the set of public names, so any change is deliberate.Exceptions. Add
s3pathlib.exc.S3PathlibError, the base class of every exception raised bys3pathlib. The hierarchy is regrouped:S3BucketNotExist,S3FolderNotExistandS3FileNotExistinherit fromS3NotExist, the*AlreadyExistones fromS3AlreadyExist; each still inherits the same builtin exception as before, so existingexceptclauses keep working. New:S3ValidationError,S3ObjectChangedError,S3SyncError, andS3DeleteObjectsError(S3 failed to delete some objects of adelete_objectscall; it has theerrorsand the number deleted so far,n_deleted).Partitions. S3 ARNs and console URLs support the
aws-cn(China),aws-us-gov(GovCloud) andaws-eusc(European Sovereign Cloud) partitions, in addition toaws. Adds3pathlib.constants.AwsPartitionEnum,S3Path.get_arn(partition),S3Path.get_console_url(region=None, partition="aws")andutils.split_s3_arn;S3Path(arn),from_s3_arnandvalidate_s3_arnaccept all of them, andutils.make_s3_console_urltakes apartition(the domains are inconstants.AWS_CONSOLE_DOMAIN).S3Path.arnstill returns theawsARN.validate_s3_uriacceptss3://bucketwithout the trailing slash.s3pathlib.constantsalso has the bucket naming rules (S3_BUCKET_MIN_LENGTH,S3_BUCKET_MAX_LENGTH,S3_BUCKET_RESERVED_PREFIXES,S3_BUCKET_RESERVED_SUFFIXES) andS3_KEY_MAX_BYTES.Relative and void paths.
S3Path.parentssupports them likepathlib.PurePath.parents(the parents of a relative path end with the void path; the void path has none), where it raisedValueError.relative_to(and-) works between two relative paths.is_parent_ofandis_prefix_ofwork between two relative paths (the void path is the parent of every relative path), and giveFalsefor an absolute and a relative path.make_relpathaccepts relative paths as parts and follows the constructor rules.S3Path() / xgivesxas a relative path, likeS3Path().joinpath(x), andS3Path() / [a, b]joins the list at once.Deprecations. Add
s3pathlib.marker.deprecated, the standard PEP 702 decorator: deprecated APIs emit aDeprecationWarningthat points at your line, and type checkers and IDEs flag the call sites. Joining onto a file and using a file as the base ofrelative_toare deprecated:S3Path("bucket", "folder") / "file.txt",.joinpath(...)andS3Path("bucket", "a/b").relative_to(S3Path("bucket", "a"))still work but warn, because a file has no sub path; they raiseS3PathIsNotFolderErrorin 4.0.0. This includes chains such asS3Path("bucket") / "folder" / "file.txt". Use a dir:S3Path("bucket") / "folder/" / "file.txt",.to_dir(), or join all parts at once withS3Path("bucket").joinpath("folder", "file.txt")orS3Path("bucket") / ["folder", "file.txt"]. The misspelledquiteargument ofsyncis deprecated for the newquiet(quitewins when both are given), and theignore_not_foundargument ofbetter_client.delete_objectnever had an effect (S3 never reports a missing key when deleting): both will be removed in 4.0.0.Client.
s3_contextis thread safe (the default client is created at most once, under a lock, andattach_s3_clientreplaces it in one step), and creates the default client when first used instead of at import, so importings3pathlibhas no side effect and environment variables such asAWS_PROFILEset after the import are honored. AnS3Pathproduced by a call with an explicit client (iter_objects(s3_client=...),head_object(s3_client=...)) remembers that client and loads its missing metadata with it; other methods uses3_clientors3_context, however the path was made. Pickle does not keep the client;copy/deepcopydo.Writes.
write_bytesandwrite_textacceptif_matchandif_none_match, the conditional writes of S3 (if_none_match="*"only writes a new object,if_match=<etag>only replaces the object if nobody changed it), and so doesopenwithmultipart_upload=False.touchmakes one conditionalput_objectcall instead ofhead_object+put_object, so an object written by someone else in between is never replaced.upload_filereturns a newS3Pathof the uploaded object, likewrite_bytes(it returnedNone). Withoverwrite=False, the copy methods check the target, then copy conditionally (IfNoneMatch="*"), so on AWS an object written in between is not replaced either.Delete and listing. Add
better_client.delete_object_all_versions(s3_client, bucket, key), which permanently deletes all versions and delete markers of exactly one key.S3Path.head_objectacceptsversion_id.S3Path.list_buckets()keeps the region of each bucket inresponse["BucketRegion"](addconstants.S3_LIST_BUCKETS_MAX_BUCKETS, 10,000).Sync.
syncsupports S3 compatible endpoints and unsigned clients (--no-sign-request), andincludeandexcludeaccept a list of patterns (all excludes, then all includes).
Bugfixes
S3Path.list_buckets()failed withS3ValidationErrorwhen the account had a legacy bucket (created in us-east-1 before March 2018, with uppercase letters or underscores). It now lists every bucket S3 returns, andfrom_s3_uri,from_s3_arnandfrom_bucketaccept legacy bucket names.utils.make_s3_console_urldid not URL encode the key, so a key with&,#,?,+,%or spaces opened the wrong object or none at all.utils.repr_data_sizeshowed1048575bytes as1024.00 KBinstead of1.00 MB, raisedKeyErrorabove 1024 YB, and accepted negative sizes. It now carries rounding over to the next unit, shows larger sizes in YB, and rejects negative sizes withValueError.utils.parse_data_sizeraised a bareValueErrorfor a number without unit ("100"); a bare number now means bytes, and invalid text raisesValueErrorwith a message.utils.smart_join_s3_keyreturned"/"for no parts withis_dir=True; it now returns""(the bucket root).utils.grouper_listwithn < 1silently returned everything as one list; it now raisesValueError.validate_s3_keyrejected valid keys with non-ASCII characters or characters such as~and%(it now accepts any UTF-8 key), and measured the 1024 limit in characters instead of UTF-8 bytes.validate_s3_bucketrejected valid names such as1.2.3as IP addresses when Python runs with-O.S3Path(...)ignores empty arguments (""or a void path) when deciding whether the path is a file or a directory.S3Path("bucket", "a", S3Path())was neither, andS3Path("bucket", "a/", "")became a file.S3Path.make_relpath("a", "/")is a dir, likeS3Path(S3Path(), "a", "/")(make_relpathignored a trailing"/").S3Path.changefailed with a bareTypeErroron a dir, a bucket or the void path, even forchange()orchange(new_bucket=...); it now keeps the last part and the type of the path.change(new_bucket=..., new_abspath=...)ignorednew_bucket, andnew_abspathon a relative path raisedTypeError; both work now.S3Path.is_parent_ofonly detected the direct parent:S3Path("bkt/a/").is_parent_of(S3Path("bkt/a/b/c"))wasFalse; it now detects every ancestor.S3Path.is_prefix_ofcompared the URIs with<=instead of checking a prefix, soS3Path("bkt/b").is_prefix_of(S3Path("bkt/c"))wasTrue; it now checks that the key starts with the other key, like the S3Prefix.The
greater_equalandless_equalfilters (S3Path.size.greater_equal(1000)) did an==comparison; they now do>=and<=.A pickled
S3Pathcarried its cached hash. The hash of astrdiffers between Python processes, so after loading it in another process (e.g. withmultiprocessing) the path was equal to the same path but had a different hash, anddict/setlookups missed it. Pickle (andcopy) now saves the path and the metadata but not the caches, and checks the state when loading; protocols 0 and 1 work too.S3Path.is_delete_markercalledhead_objectwhen no metadata was loaded. That call can never find a delete marker, and it raisedS3FileNotExistfor a missing object and for an object whose latest version is a delete marker, andTypeErroron a relative path. It now only looks at the loaded metadata and never calls AWS.The
ensure_*validators showedNonein the error message for a relative path or the void path; they now show the path.ensure_relpathandensure_not_relpathhad their error messages swapped.The parent of a single part relative path (
S3Path.make_relpath("a").parent) andS3Path(S3Path(), "/")are now the void path; they were a “void path that is a dir”, for whichis_void()wasTruebutis_relpath()wasFalse.Comparing an
S3Pathwith another type (p == "s3://bucket/key",p != None,p in ["a", p]) raisedAttributeError.==now returnsFalseand</<=/>/>=raiseTypeError, likepathlib.S3Path.partsreturned the internal list, so changing it changed the path but not its cached hash, which broke==andhash. It returns a copy.Sorting
S3Pathobjects put a dir after the part of its content whose name starts with a character smaller than/(-,!, a space, …):sorted([S3Path("b", "a/"), S3Path("b", "a/-x")])gavea/-xfirst, whilepathliband the S3 list APIs put the dir first. A dir now sorts right after the file of the same name and before all of its content. The hash of anS3Pathchanged with it, which does not matter as it is never saved.When the default boto session could not be created at import time (e.g.
AWS_PROFILEnames an unknown profile), the error was silently ignored and every later call failed withAttributeError: 'NoneType' object has no attribute 'client', even after fixing the environment. The first API call now raises the real error (e.g.ProfileNotFound), and the next call works once the environment is fixed.better_client.is_bucket_existsandhead_objectrecognized “not found” by looking forNot Foundin the error message, which is not reliable for aHEADresponse; they now check the error code.head_objectwith aversion_idthat is a delete marker raisedClientError(S3 answers 405); it now means the object does not exist, sois_object_exists(..., version_id=<delete marker>)isFalse. With moto, a missing bucket raisedNoSuchBucketinstead of “not found”; both now behave the same.better_client.paginate_list_objects_v2andpaginate_list_object_versionsreturned more thanlimitentries, becauselimitonly counted the objects / versions and not the folders (common prefixes) and delete markers:limit=3could return 8 entries.limitnow counts all of them, likeMaxKeys, which also applies toS3Path.iterdir/list_object_versions(limit=...)anddelete_object_versions(limit=...). A negativelimitis sent to S3, which rejects it (moto ignored it); it now raisesValueError, andlimit=0returns nothing without calling AWS.Data loss:
S3Path("bucket", "data/file.txt").delete(is_hard_delete=True)permanently deleted all versions of every key starting withdata/file.txt, such asdata/file.txt.bakanddata/file.txt2/.... It only deletes the versions ofdata/file.txt.delete(version_id=v, is_hard_delete=True)ignoredversion_idand deleted every version of the file (it now raisesValueError, see above).better_client.delete_diranddelete_object_versionsignored the objects S3 failed to delete (delete_objectsanswers HTTP 200 and lists them inErrors) and counted them as deleted. They now raiseS3DeleteObjectsErrorand stop.S3Path.list_object_versions()yielded the entries of the earlier pages again after each page: 8 versions listed withbatch_size=2gave 20 entries. Each entry is now yielded once, in the S3 order (by key, then newest first). On a file it also listed the versions of every key starting withfile.txt(file.txt.bak); it now lists this exact key only. Invalidlimit/batch_sizeraiseValueErrorwhen the method is called, andlimit=Nonemeans no limit, as when it is left out (TypeErrorbefore; also foriter_objects,iterdirand thebetter_clientpagination functions). On a relative path or the void path it raised a bareTypeErrorfrom botocore; it now raisesS3PathTypeError.S3Path.list_object_versions()could list a delete marker after the version it hides when both were written in the same second (S3 timestamps have a one second resolution), so the first entry was not the latest one: e.g.p.list_object_versions().one()afterp.write_bytes(...)thenp.delete(). The entry that S3 marks as the latest (IsLatest) now always comes first.S3Path.put_bucket_versioning(enable_mfa_delete=...)always failed withParamValidationError(it sentMFADeleteas a bool) and replacedStatus(enable_mfa_delete=Trueenabled versioning even withenable_versioning=False). It now sends"Enabled"/"Disabled"and keepsStatus;enable_mfa_delete=None(default) does not sendMFADelete.S3Path.create_bucket()withoutregionalways asked forus-east-1, which S3 rejects withIllegalLocationConstraintExceptionwhen the client is in another region.regionnow defaults toNone, the region of the S3 client.S3Path.list_buckets()made one ListBuckets request withoutMaxBuckets, which S3 rejects for an account allowed more than 10,000 buckets; it now fetches every page.S3Path("bucket", "folder/").exists()raisedNoSuchBucketwhen the bucket did not exist, while a file in a missing bucket returnedFalse; a folder in a missing bucket now does not exist either.exists()on a path without bucket raised a different error for each kind of path (a bareTypeErrorfor the void path, a botocoreTypeErrorfor a relative file,S3PathTypeErrorfor a relative folder); it always raisesS3PathTypeErrorbefore calling AWS. Reading a metadata property of a bucket, a relative path or the void path raised a botocore error; it raisesS3PathTypeErrortoo (a bucket fromlist_buckets()keeps itslast_modified_at).An
S3Pathreturned bylist_object_versions()(or bywrite_bytes()with versioning) loaded its missing metadata from the latest version, so an old version silently got the size, user metadata and version id of the latest one. It now loads them from its own version. Theversion_idof a path returned byiter_objects()/iterdir()was always"null"because the list API does not return it; it now loads the real one.read_bytes(range=...),read_bytes(part_number=...)andread_bytes(response_*=...)stored the response as the metadata of the object, sosizebecame the length of the range andcontent_typethe overridden header. They no longer fill the metadata.The delete marker returned by
S3Path.delete()with versioning hadis_delete_marker()False, and reading itssizeraisedS3FileNotExist; it is now a delete marker (is_delete_marker()isTrue).S3Path("bucket", "folder/").delete()ignoredcheck_sum_algorithm(it was only used withis_hard_delete=True).deleteon a relative path raised a botocoreTypeErrorand on the void path a bareValueError; both raiseS3PathTypeErrorbefore calling AWS.request_payerand the bucket owners were not sent with every call: the listing of the source folder and the check ofoverwrite=Falseof the copy methods, and the listing of the versions ofdelete(is_hard_delete=True), which a requester-pays bucket refuses. They are now sent with every call; the calls on the source bucket getexpected_source_bucket_owner, the ones on the target bucketexpected_bucket_owner.better_client.is_object_existsandpaginate_list_object_versionshave the matching arguments.S3Path.sync(..., bsm=s3_client)always failed, because it calledbsm.awscli(), which only aBotoSesManagerhas.syncraisesFileNotFoundErrorwith a message telling to install the AWS CLI when theawscommand is not found, andValueErrorwith a message when neither path is in S3.
Minor Improvements
Support
smart_open8.x.S3Path.create_bucketis no longer beta and no longer warns.Encode spaces in object tags as
%20instead of+when sending theTaggingparameter, like the official AWS SDKs, so S3 cannot read them as a literal plus sign.S3Path.put_tagsreturns a copy of the given tags.The
UserWarningfor a user metadata key with upper case letters says which lowercase key S3 stores, points at the line of your code (not a line insides3pathlib), and is now also emitted by the copy methods (write_bytes,write_text,touchandopenalready did).Clearer errors, all still the builtin exception they were:
S3Path(...),joinpath,/andmake_relpathraiseTypeErrornaming the wrong argument type;ensure_not_object/ensure_not_file/ensure_not_dir/ensure_not_relpath/abspath/dirpath/rootraiseS3PathTypeErrornaming the path;ensure_relpathraisesS3PathIsNotRelpathError;is_parent_ofraisesS3PathIsNotFolderErroron a file;relative_toraisesTypeErrorfor an argument that is not anS3Path(it wasAttributeError) and itsValueErrorsays why the path is not under the other one;get_arn,get_console_urlandmake_s3_console_urlraiseS3ValidationErrorfor an unsupported partition (listing the supported ones) or an empty region;ensure_not_existsraisesS3FileAlreadyExist,S3FolderAlreadyExistorS3BucketAlreadyExistdepending on the path, instead ofS3AlreadyExistsaying “s3 object” for every path.reprof anS3Pathwith a bucket escapes quotes in the key like a relative path does (S3Path("s3://b/it's.txt")). Deleting a filterable property (del p.key) raisesAttributeErrorwith a clear message.validate_s3_bucketlists the invalid characters in sorted order.s3_context.attach_s3_client(None)raisesTypeError(any other object is accepted, e.g. a stub in tests).S3FileNotExistfrombetter_client.head_objectnames the version and keeps theClientErroras__cause__.s3pathlib.tag.parse_tagsno longer checks its argument.Rewrite the docstrings of the whole library, which are now the reference of the behavior: the six states of a path (
s3pathlib.core.base), what each kind of path returns forbucket/key/uri/arn/console_url, how paths compare (equality ignores the object version; the order is part by part likepathlib), every filter ofFilterableProperty, the metadata snapshot (s3pathlib.core.metadata), and for each group of methods (bucket,exists, read / write,open, upload, copy, delete, tagging, listing,sync) every argument, which boto3 errors are raised as is, what is atomic and what is not. Docstring examples were fixed (wrong values, swapped operands, a non-existentpathargument ofupload_dir) and are run as tests.Document the
better_clientfunctions the same way: results of the pagination functions are lazy, can be iterated once, and raise when the iteration starts;prefixis a string prefix, not a folder; the truth table ofis_content_an_object;update_bucket_taggingandupdate_object_taggingmerge likedict.updateand are not atomic;upload_dirbuilds the keys, theprefixrules and the errors; deleting a missing key in a versioned bucket adds a delete marker.better_client.list_object_versions.CommonPrefixTypeDefIterproxyis now the same class as inbetter_client.list_objects. Fix theexpire_atdocs: it is theExpiresHTTP header, not the expiration date of a lifecycle rule.
Miscellaneous
Package and tooling: switch the build backend from poetry to setuptools, manage the environment with mise and uv, use the SPDX license expression
Apache-2.0and add the Python version classifiers, regeneraterequirements*.txtwithmise run exportand remove the stale ones, remove the old.readthedocs.yamlthat competed with.readthedocs.yml, configure ruff inpyproject.toml, adds3pathlib.pathsfor project paths, and add thetyping-extensionsdependency on Python < 3.13 for thedeprecatedbackport.Arguments passed on to boto3 default to the private
s3pathlib._arg.OPT(“not given”), replacingfunc-args.Noneis not “not given”: botocore rejects it withParamValidationError(S3 has noNonevalue), so a condition such asif_match=p.etagis never silently dropped.s3pathlib.type.PathTypeisstr | os.PathLike; the library uses the standard librarypathlib(apathlib_mate.Pathis still accepted).Tests: every test module that calls AWS uses the new
s3pathlib.tests.mock_aws.BaseTest(the olds3pathlib.tests.mockwithcls.bsmis removed) and runs against moto (TestMock) and, withS3PATHLIB_TEST_REAL_AWS=true(mise run cov-aws), against real AWS (TestRealAws);@mock_only/@real_aws_onlymark tests that only work in one mode, and each test gets its own S3 folder. Thesynctests runaws s3 syncend to end against a moto server (flaskandflask-corsare test dependencies) or AWS when the AWS CLI is installed. Shared helpers are ins3pathlib.tests.aws_helpers. Unused test data was removed.The mise tasks
setup-aws-ciandteardown-aws-ci(idempotent) create / delete the test buckets, a lifecycle rule that expires the test objects after one day, the test IAM user (its policy is built in code, replacing.github/workflows/iam-policy.json, with full access to the temporarys3pathlib-test-tmp-*buckets) and its GitHub Actions secrets; the AWS profiles are defined inmise.toml.GitHub Actions CI is rewritten with uv: on every push to
mainanddev, Python 3.11 to 3.14 on both Ubuntu and Windows, running the whole suite against moto and real AWS; each run uses its own S3 folder (GITHUB_RUN_ID), so concurrent runs do not interfere.The documentation notebooks use the standard library
pathlibinstead ofpathlib_mate.
2.3.6 (2025-08-12)¶
Miscellaneous
Fix Author and Maintainer.
2.3.5 (2025-08-10)¶
Miscellaneous
No code changes.
Housekeeping: update docs and clean unused comments.
2.3.4 (2025-07-13)¶
Bugfixes
Fix smart_open version parsing bug that could cause compatibility issues with certain version string formats.
Miscellaneous
Remove unused cached_property import fallback code for Python < 3.8 since the minimum supported Python version is now 3.9.
2.3.3 (2025-06-13)¶
Bugfixes
Relax the requirements of
func_args>=0.1.1,<1.0.0tofunc_args>=0.1.1,<2.0.0to allow the latest version offunc_argsto be used.
Miscellaneous
Use pywf_open_source code skeleton.
2.3.2 (2025-03-07)¶
Minor Improvements
Allow user to use
context.attach_s3_client(your_custom_s3_client)andcontext.attach_sts_client(your_custom_sts_client)to set a custom S3 client, so that you can work with any S3 compatible storage by customizing your own s3 client object.
Miscellaneous
Upgrade moto to 5.X.Y
Add support to use smart_open 7.X.Y
Stop testing on Python 3.7
2.3.1 (2024-08-31)¶
Features and Improvements
- Add the following public APIs:
S3Path.get_regional_console_urlS3Path.get_regional_us_gov_cloud_console_urlS3Path.get_regional_s3_select_console_urlS3Path.get_regional_s3_select_us_gov_cloud_console_url
2.2.1 (2024-08-10)¶
Features and Improvements
Add
s3pathlib.core.metadata.MetadataAPIMixin.responseproperty method to allow developer to access the underlyinghead_objectAPI response.Add support for Python 3.12.
2.1.2 (2023-09-10)¶
Minor Improvements
now most of the
S3Pathmethods can take bothboto3.client("s3")orboto_session_manager.BotoSesManager(...)object forbsmargument.
2.1.1 (2023-09-03)¶
Features and Improvements
open()method now takes full list ofs3_client.get_object,s3_client.put_object,s3_client.create_multipart_uploadarguments.
New APIs:
add
from_bucket()add
create_bucket()add
delete_bucket()add
list_buckets()add
ContentTypeEnum
Bugfixes
2.0.1 (2023-04-21)¶
Features and Improvements
Changes:
most of methods now supports
version_idargument for S3 versioning enabled bucket.s3pathlib.core.rw.ReadAndWriteAPIMixin.read_bytes()ands3pathlib.core.rw.ReadAndWriteAPIMixin.read_text()now supportversion_idand full list ofget_objectboto3 arguments.s3pathlib.core.uri.UriAPIMixin.console_url()now take the version id into consideration.add
s3pathlib.core.is_test.IsTestAPIMixin.is_delete_marker()method.s3pathlib.core.base.BaseS3Path.__new__()now can take S3 URI and ARN directly without using thefrom_s3_uriandfrom_s3_arnmethod.
New APIs:
add
s3pathlib.better_clientto the public API, it is a collection of more user friendly boto3 API, with type hint.add
list_object_versions()method. It will return all objects, all versions, all delete markers in reverse chronological order.The
is_delete_marker()method can be used to check if the version is a delete marker.delete(): delete object, directory (recursively), specific object version, object versions, all objects all versions in directory. This API will replace the olddelete_if_exists()API in 3.X.Y.
Minor Improvements
use moto for unit test.
add tons of more unit test for edge cases.
prompt to confirm when delete everything in a bucket.
Bugfixes
fix a bug that should not allow user to set value to attributes like
bucket,key.
Miscellaneous
completely rewrite the documentation in Jupyter notebook to provide interactive reading experience.
Imcompatible Change
removed
join_path().
1.4.1 (2023-02-16)¶
Features and Improvements
- now the following API supports full list of native boto3 argument:
S3Path.write_bytesS3Path.write_textS3Path.copy_fileS3Path.copy_dirS3Path.copy_toS3Path.move_to
add many optional argument to the
S3Path.sync()method.
1.3.1 (2022-12-30)¶
Features and Improvements
allow
and_,or_,not_in iterproxy filter.allow update
metadataandtaggingsincopy_file(),copy_to()andmove_to()method.
Miscellaneous
the
iterproxy.pymodule is taken out and released as a independent project.
1.2.1 (2022-11-20)¶
Features and Improvements
open()method now takesmetadataandtagsarguments.write_bytes()andwrite_text(),touch()method now takesmetadataandtagsarguments.add
sync(),sync_from(),sync_to()method that execute aws s3 sync command
Minor Improvements
raise a warning if there is upper case key used in user defined metadata.
1.1.2 (2022-11-16)¶
Minor Improvements
Refactor
core.pymodule, split the 2.5k line script into 10+ 100 line module.Made the metadata pull more intelligent.
Bugfixes
Fix a bug that the
S3Pathconstructor should not take concrete S3 object / dir path as the second / third / fourth / … argumentsFix a bug that cannot get the metadata value of the S3Path is created by
_from_content_dictmethod.
1.1.1 (2022-11-13)¶
Features and Improvements
- add
taggingmanagement feature add
get_tags()methodadd
put_tags()methodadd
update_tags()method
- add
allow update
metadataandtaggingsinwrite_text()andwrite_bytes()method.
1.0.12 (2022-09-10)¶
Features and Improvements
add
s3pathlib.core.S3Path.joinpath()method to mimickpathlib.Path.joinpathbehavioradd
s3pathlib.core.S3Path.s3_select_console_url()propertyadd
s3pathlib.core.S3Path.s3_select_us_gov_cloud_console_url()property
Bugfixes
made
s3pathlib.core.S3Path.console_url()ands3pathlib.core.S3Path.us_gov_cloud_console_url()regular property, they should not beFilterableProperty
Miscellaneous
mark
s3pathlib.core.S3Path.join_path()as deprecated
1.0.11 (2022-07-12)¶
Features and Improvements
add the
__truediv__operator override. it is as3path / part1 / part2syntax sugar.add the
__sub__operator override. it is aS3Path("bucket/folder") - S3Path("bucket")syntax sugar.
Minor Improvements
Bugfixes
Miscellaneous
1.0.10 (2022-04-30)¶
Miscellaneous
remove the boto session module, now it depends on boto_session_manager library.
add compatibility support for smart_open >= 6.0 due to the
ignore_extarg is removed.for s3 IO feature, you need
smart_open>=5.1.x
1.0.9 (2022-04-19)¶
Features and Improvements
add
s3pathlib.core.S3Path.boto_ses.BotoSesManager.get_client()method
1.0.8 (2022-04-19)¶
Bugfixes
fix import bug in
s3pathlib.awsmodule
1.0.7 (2022-04-17)¶
Bugfixes
fix import bug in
s3pathlib.boto_sesmodule
1.0.6 (2022-04-13)¶
Features and Improvements
add
s3pathlib.core.S3Path.to_file()method.add
s3pathlib.core.S3Path.to_dir()method.add
s3pathlib.core.S3Path.parents()method.add
s3pathlib.core.S3Path.iterdir()method.add
s3pathlib.core.S3Path.touch()method.add
s3pathlib.core.S3Path.mkdir()method.add
s3pathlib.core.S3Path.boto_ses.BotoSesManagerclass.
Minor Improvements
add
bsmboto session manager parameter for all method using s3 api.
Bugfixes
Miscellaneous
1.0.5 (2022-02-06)¶
Features and Improvements
add
s3pathlib.core.S3Path.us_gov_cloud_console_url()propertyadd
s3pathlib.utils.parse_data_size()method
Minor Improvements
Bugfixes
Miscellaneous
1.0.4 (2022-01-25)¶
Features and Improvements
add
s3pathlib.core.S3PathIterProxy.equal_to()add
s3pathlib.core.S3PathIterProxy.not_equal_to()add
s3pathlib.core.S3PathIterProxy.greater()add
s3pathlib.core.S3PathIterProxy.greater_equal()add
s3pathlib.core.S3PathIterProxy.less()add
s3pathlib.core.S3PathIterProxy.less_equal()add
recursive = True | Falseargument fors3pathlib.util.iter_objects(), so you can ignore files in nested foldersadd
recursive = True | Falseargument fors3pathlib.core.S3Path.iter_objects(), so you can ignore files in nested folders
Minor Improvements
Bugfixes
fix a bug that
s3pathlib.core.S3Path.fname()was a regular property and not filterable
Miscellaneous
Add “S3 Object filter” doc
Add “File Liked Object IO Object filter” doc
1.0.3 (2022-01-23)¶
Features and Improvements
make
s3pathlib.core.S3Patha file-like object that support open, read, write.add
s3pathlib.core.S3PathIterProxythat greatly simplify S3 object filtering.add
s3pathlib.core.S3Path.open()method, makesS3Patha file-like objectadd
s3pathlib.core.S3Path.write_text()add
s3pathlib.core.S3Path.read_text()add
s3pathlib.core.S3Path.write_bytes()add
s3pathlib.core.S3Path.read_bytes()
1.0.2 (2022-01-21)¶
Features and Improvements
add
s3pathlib.core.S3Path.from_s3_uri()method.add
s3pathlib.core.S3Path.from_s3_arn()method.add
s3pathlib.core.S3Path.change()method.add
s3pathlib.core.S3Path.is_parent_of()method.add
s3pathlib.core.S3Path.is_prefix_of()method.add
s3pathlib.core.S3Path.dirpath()property.add better support to handle auto-created “empty folder” object, add
include_folder=Trueparameter fors3pathlib.core.S3Path.list_objects(),s3pathlib.core.S3Path.count_objects(),s3pathlib.core.S3Path.calculate_total_size()method.
Bugfixes
fix a bug that AWS S3 will create an invisible object when creating a folder, it should not counts as a valid object for
s3pathlib.core.S3Path.count_objects()
Miscellaneous
A lot doc improvement.
1.0.1 (2022-01-19)¶
Features and Improvements
s3pathlib.S3PathAPI becomes stables3pathlib.utilsAPI becomes stables3pathlib.contextAPI becomes stable
Miscellaneous
First stable release.
0.0.1 (2022-01-17)¶
First release, a placeholder release.