npx skills add ...
npx skills add aws/agent-toolkit-for-aws --skill aws-sdk-python-usage
npx skills add aws/agent-toolkit-for-aws --skill aws-sdk-python-usage
AWS SDK for Python (boto3/botocore) development patterns. You MUST use this skill when writing Python code that uses AWS services via boto3 or botocore. This includes creating service clients or resources, configuring sessions and credentials, handling errors with ClientError, using paginators and waiters, S3 file transfers and presigned URLs, DynamoDB table operations, and any boto3/botocore client configuration. Use this skill whenever Python code imports boto3 or botocore, or when the user asks about AWS operations in Python.
Do not use emojis in any code, comments, or output when this skill is active.
boto3 is the high-level Python SDK for AWS. It wraps botocore (the low-level SDK) and provides two distinct interfaces: clients (low-level, 1:1 API mapping) and resources (high-level, object-oriented). Understanding which to use and when is essential.
Clients map directly to AWS service APIs. Every service has a client. Responses are plain dicts.
Resources provide an object-oriented interface with attributes and actions. Only some services have resources (S3, DynamoDB, EC2, IAM, SQS, SNS, CloudFormation, CloudWatch, Glacier). Resources auto-marshal types (especially useful for DynamoDB).
Use clients when you need full API coverage or the service has no resource interface. Use resources when they exist and simplify your code (especially DynamoDB and S3).
Do not create clients inside loops - reuse a single client instance. Clients are thread safe and can be shared across threads once they're instantiated.
Parameter names match the exact casing of the AWS API, which is typically PascalCase, not snake_case.
Only catch exceptions when you have something actionable to do - return a fallback value, retry, take a different code path. Catching an exception just to print it and swallow it is wrong: it hides the real error and prevents callers from reacting. Let exceptions propagate by default.
When you do catch, prefer typed exceptions on the client over generic
ClientError with string code matching through the client.exceptions
attribute:
Use generic ClientError only as a catch-all in a top-level error handler, not
in business logic functions. It lives in botocore, not boto3:
For the full error hierarchy and botocore exceptions, see references/error-handling.md.
When asked to write a script that uses boto3 or botocore, keep if __name__ == "__main__" to a single function call. Argument parsing, error presentation,
and exit codes belong in main(), not scattered across business logic
functions:
Never call sys.exit() from a business logic function -- it makes the function
untestable and unusable as a library. Raise an exception or return an error
value instead, and let main() decide how to present it.
Never manually loop with NextToken -- use paginators. When you only need
specific fields, use .search() with a JMESPath expression to extract and
flatten across pages:
When you need the full response object per item, or need per-page control (e.g. counting pages, batching by page), iterate pages directly:
For more details on pagination, see: references/pagination.md.
Wait for a resource to reach a desired state:
For more details on waiters, see references/waiters.md.
Use botocore.config.Config for retries, timeouts, and connection pool
settings, etc.:
When creating custom configuration for a client, see references/configuration.md.
Both boto3 and botocore use the standard library logging module. You can
configure logging through the standard logging APIs, or you can use
helpers provided by boto3 and botocore for convenience:
set_stream_logger(name, level=logging.DEBUG) adds a
StreamHandler to the named logger. This is the idiomatic way to get
request/response debug output from the SDK.
Wrong: from boto3.exceptions import ClientError
Right: from botocore.exceptions import ClientError
When writing any Python code that uses the following services, you MUST load these additional reference files for best practices and custom high level APIs:
references/s3.md.references/dynamodb.md.references/configuration.mdreferences/credentials.mdreferences/error-handling.mdreferences/pagination.mdreferences/waiters.mdreferences/s3.mdreferences/dynamodb.mdimport boto3
# Default session implicitly created
client = boto3.client("s3")
resource = boto3.resource("dynamodb")
# Explicit session use when you need to customize how
# clients are created, use an explicit profile, etc.
session = boto3.Session(
profile_name="my-profile",
region_name="us-west-2",
)
client = session.client("s3")# Client - pass parameters as keyword arguments, get dicts back
response = client.get_object(Bucket="my-bucket", Key="my-key")
data = response["Body"].read()
# Resource - use object methods and attributes
obj = s3_resource.Object("my-bucket", "my-key")
response = obj.get()
data = response["Body"].read()lambda_client = boto3.client("lambda")
def get_function_config(name: str) -> dict | None:
"""Return function configuration, or None if it doesn't exist."""
try:
return lambda_client.get_function_configuration(FunctionName=name)
except lambda_client.exceptions.ResourceNotFoundException:
return None # actionable: convert missing function to None
# Everything else propagates - caller or main() handles itfrom botocore.exceptions import ClientError
def main() -> int:
try:
result = do_the_work()
print(result)
return 0
except ClientError as e:
print(f"Error: {e}", file=sys.stderr)
return 1def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("bucket")
args = parser.parse_args()
try:
do_the_work(args.bucket)
return 0
except ClientError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())paginator = iam.get_paginator("list_users")
for name in paginator.paginate().search("Users[].UserName"):
print(name)
# Filter and project
for arn in paginator.paginate().search("Users[?Path == '/admin/'][].Arn"):
print(arn)for page in paginator.paginate():
for user in page.get("Users", []):
process(user)waiter = client.get_waiter("bucket_exists")
waiter.wait(
Bucket="my-bucket",
WaiterConfig={"Delay": 5, "MaxAttempts": 20},
)from botocore.config import Config
config = Config(
retries={"total_max_attempts": 2, "mode": "adaptive"},
connect_timeout=5,
read_timeout=10,
max_pool_connections=50,
)
client = boto3.client("s3", config=config)# Quick: log all botocore wire-level details to stderr
boto3.set_stream_logger("") # root logger -- everything
boto3.set_stream_logger("botocore") # just botocore
# Botocore, log all botocore details
import logging
from botocore.session import Session
session = Session()
session.set_stream_logger('botocore', logging.DEBUG)
# OR: Configure logging to a file.
session.set_file_logger(logging.DEBUG, '/tmp/botocore.log')