Write every Boto3 script as if it will run twice
A Python script that calls AWS is small, quick to write and very powerful. One loop can tag, stop, resize or delete thousands of resources across every region. That’s exactly why the same script, run twice by accident or pointed at the wrong account, is one of the more common ways to cause an outage.
Five habits make these scripts safe by construction.
1. Idempotent: the second run changes nothing
Design every action so running it again is harmless:
- Check before you act. Only stop instances that are running. Only tag resources that don’t already have the tag.
- Use idempotency tokens where the API offers them. EC2’s
run_instancesaccepts aClientToken: retry the call with the same token and AWS returns the original result instead of launching a second batch. - Make “already done” a success, not an error. A script that crashes because the bucket already exists will be rerun with workarounds, which is worse.
2. Paginated: you see all of it
Most list and describe calls return one page of results. A script that reads only the first page silently skips everything else, and the bug only shows up in accounts big enough to matter.
paginator = ec2.get_paginator("describe_instances")
for page in paginator.paginate(Filters=[{"Name": "tag:env", "Values": ["dev"]}]):
for reservation in page["Reservations"]:
for instance in reservation["Instances"]:
...
Use paginators everywhere. Filter on the server side when the API supports it.
3. Retries that respect throttling
At scale, AWS will throttle you. Botocore’s default retry behavior is conservative. Configure it explicitly:
from botocore.config import Config
cfg = Config(retries={"max_attempts": 10, "mode": "adaptive"})
ec2 = session.client("ec2", config=cfg)
standard mode retries with exponential backoff and jitter. adaptive also slows its own request rate when it sees throttling. Either is better than a hand-written time.sleep(1) loop.
4. Dry run by default
Make the safe behavior the default and the dangerous behavior explicit:
parser.add_argument("--apply", action="store_true", help="actually make changes")
Without --apply, the script prints exactly what it would do: resource IDs, account, region, action. Many EC2 APIs also accept DryRun=True, which checks permissions without making changes. It’s a good preflight check.
5. Explicit targets
The most dangerous line in a Boto3 script is the one that isn’t there: the one that says which account it’s about to change. Default credentials point wherever the last aws sso login pointed.
- Print the account ID and region (from
sts.get_caller_identity()) before doing anything, and require it to match an expected value or an explicit argument. - Iterate regions on purpose from a list, rather than defaulting to one.
- Assume a role per account for multi-account work, with a clear session name, so CloudTrail shows exactly which script made each change.
Why this matters in 2026
Many of these scripts are now written with an AI assistant in a few minutes, and they often work on the first try against a small test account. The habits above are what separate a script that works once from one that’s safe to run in production. They’re also easy to ask for explicitly: “paginate, add adaptive retries, default to dry run, print the account first.” Make that the standard request, and check that every generated script follows it.