S3 is the most used AWS service from Python: data lakes, uploads, backups and ML artifacts all live there.
S3 stores objects (bytes plus metadata) under keys in buckets. Keys are flat strings; slashes only look like folders.
Two pairs of methods cover most work:
put_object(Bucket=, Key=, Body=b"...", ContentType=...)uploads bytes or a file-like object in one request;get_object(Bucket=, Key=)returns a dict whoseBodyis a streaming object: call.read()once, or.iter_lines()/.iter_chunks()for big files.upload_file(filename, bucket, key)anddownload_file(bucket, key, filename)are managed transfers: they switch to parallel multipart uploads and ranged downloads for large files automatically.upload_fileobj/download_fileobjdo the same with file objects.
Pass metadata on upload with ExtraArgs={"ContentType": "text/csv", "Metadata": {...}} for managed transfers, or as parameters for put_object.
head_object returns metadata (size, ETag, content type, last modified) without downloading. delete_object and delete_objects (up to 1000 keys per call) remove objects; copy_object copies server-side.
Listing uses list_objects_v2(Bucket=, Prefix="reports/2026/"), which returns at most 1000 keys per call: use a paginator for more. Delimiter="/" groups keys into CommonPrefixes, the "folders".
Buckets are created rarely and usually by infrastructure code (Terraform, CloudFormation), not by applications. Outside us-east-1, create_bucket needs a CreateBucketConfiguration with the region.
Example
import boto3
s3 = boto3.client("s3", region_name="us-east-1")
s3.create_bucket(Bucket="demo-data-2026")
s3.put_object(Bucket="demo-data-2026", Key="reports/2026/q1.csv",
Body=b"region,total\neu,120\n", ContentType="text/csv")
with open("local.txt", "w") as f:
f.write("hello from disk")
s3.upload_file("local.txt", "demo-data-2026", "notes/hello.txt")
obj = s3.get_object(Bucket="demo-data-2026", Key="reports/2026/q1.csv")
print(obj["ContentType"], obj["Body"].read().decode()) # text/csv region,total ...
head = s3.head_object(Bucket="demo-data-2026", Key="notes/hello.txt")
print(head["ContentLength"]) # 15
resp = s3.list_objects_v2(Bucket="demo-data-2026", Delimiter="/")
print([p["Prefix"] for p in resp["CommonPrefixes"]]) # ['notes/', 'reports/']
s3.download_file("demo-data-2026", "notes/hello.txt", "copy.txt")
print(open("copy.txt").read()) # hello from diskRemember
- put_object/get_object for bytes; upload_file/download_file for files
- get_object's Body is a stream: read it once or iterate
- head_object reads metadata without downloading
- list_objects_v2 returns up to 1000 keys; Delimiter gives 'folders'
Watch out for
- Reading obj['Body'] twice and getting empty bytes the second time.