list_objects_v2 → 1000 keys
A raw call silently truncates and sets IsTruncated. Always get_paginator('list_objects_v2'). This bug passes every dev test and loses data in production.
resp.get('Contents', [])
When nothing matches, the key is absent, not empty. resp['Contents'] raises KeyError. Same for CommonPrefixes and Errors.
prefix ≠ folder
Keys are flat strings. There is no recursive delete, no rename, no mkdir. Deleting a "folder" means listing and deleting every key beneath it.
Body is a one-shot stream
StreamingBody can be read once and must be closed, or you leak a pooled connection. Use it as a context manager.
head 404 vs get NoSuchKey
get_object raises the named exception; head_object can only raise ClientError with code '404' — HEAD has no body to carry the real code. And a 403 may mean "exists, denied".
delete_objects 200 ≠ success
Max 1000 keys per call, and per-key failures come back in Errors. Check it, or you will report deletions that never happened.
boto3 ≥ 1.36 sends CRC32
Default integrity protections replaced Content-MD5 on some calls. Breaks MinIO / GCS / older emulators → set request_checksum_calculation and response_checksum_validation to when_required.
clients yes, resources no
Client objects are thread-safe and meant to be shared. Sessions and resources are not — build one per thread.
us-east-1 is the odd one
Creating a bucket there must omit CreateBucketConfiguration; every other region must include it. And get_bucket_location returns None for it.
ETag is not a checksum
It equals the MD5 only for single-part uploads. Anything multipart ends in -N. Use get_object_attributes(ObjectAttributes=['Checksum']) instead.
presign: first expiry wins
SigV4 caps at 7 days, but the URL also dies with the signing credentials. Signing with the wrong region produces a URL that 400s on use.
MetadataDirective='REPLACE'
Without it, copy_object inherits the source metadata and silently ignores yours. Copy-onto-itself is the only way to edit metadata in place.
aborted parts bill forever
Incomplete multipart uploads are invisible to listing and never expire on their own. Put AbortIncompleteMultipartUpload: 7 days on every bucket you own.
the resource API is frozen
AWS has said it will get no new features; it stays working for boto3's lifetime. Write client code, and keep resources for bucket.objects.filter(...).delete().