S3: upload, download and read objects

boto3 · Essentials · 12 min

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 whose Body is a streaming object: call .read() once, or .iter_lines() / .iter_chunks() for big files.
  • upload_file(filename, bucket, key) and download_file(bucket, key, filename) are managed transfers: they switch to parallel multipart uploads and ranged downloads for large files automatically. upload_fileobj / download_fileobj do 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 disk

Remember

  • 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.

Check yourself

  1. Why prefer upload_file over put_object for a 5 GB file?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-uploading-files.html