awscli
awscli on PyPI is version 1 of the AWS command line: the aws binary that turns every AWS API into aws <service> <operation> commands, built on botocore, the same API model layer under boto3. You get named profiles, JMESPath filtering with --query, json, text, and table output, high-level s3 commands like sync and cp, and automatic pagination across a couple hundred services. The catch you must know before installing: this package is CLI v1, which AWS moved into maintenance mode on July 15, 2026 ahead of an announced end of support. AWS CLI v2, the recommended line, is not distributed on PyPI; it ships as platform installers and containers.
The aws command itself remains essential; this specific package is the legacy way to get it. Reach for it when pip is your only practical installer or when pinned v1 scripts demand it, and install AWS CLI v2 from AWS's installers for everything else, because v1 is in maintenance mode with an end of support already announced.
Use it if
- You need the aws command inside a pip-managed environment (a Python Docker image, a virtualenv, a requirements.txt-driven CI job) where pulling in the v2 bundled installer is more moving parts than you want
- You maintain existing scripts written against v1 behavior (v1 output defaults, v1 s3 semantics) and need reproducible pinned installs
- You want quick one-off API calls with --query and --output table instead of writing a boto3 script for every lookup
- You need the exact same botocore version behavior as your Python code, since v1 shares its dependency line with boto3
- You are starting anything new: v1 entered maintenance mode in July 2026 per the project README, and new features land in AWS CLI v2, which you install from AWS's own installers rather than pip
- You rely on v2-only features: the SSO login flow (aws configure sso, aws sso login), aws logs tail, auto-prompt, wizards, and yaml output do not exist in v1
- You are installing it into an application's virtualenv: v1 pins dependencies hard (docutils<=0.19, PyYAML<6.1, colorama<0.4.7, rsa<4.8), and those pins regularly collide with other packages during pip resolution
- Your shell script around aws is growing conditionals and json parsing: past a few dozen lines, boto3 in Python is easier to test and debug than bash plus --query
Setup reality
pip install awscli works on Python 3.10+, but do it in a dedicated virtualenv or with pipx, because the pinned docutils, PyYAML, colorama, and rsa ranges are famous for fighting whatever else lives in the environment. Then run aws configure to write ~/.aws/credentials and ~/.aws/config, and expect the first real commands to fail on missing region or wrong profile until those files settle. Remember which major you are on: tutorials found via search are mostly written for v2, so commands like aws sso login or aws logs tail will simply not exist for you. Upgrading pip-installed v1 never turns it into v2; that migration is a separate reinstall from AWS's installers.
Patterns
Set up credentials and named profilesconfigure-profiles
# interactive default profile
aws configure
# separate profile for a second account
aws configure --profile staging
# use it per command
aws s3 ls --profile stagingCredentials land in ~/.aws/credentials and settings in ~/.aws/config. In config files, non-default profiles are prefixed with 'profile ', as in [profile staging]; forgetting that prefix is a classic silent failure.
Filter output with JMESPath and pick a formatquery-and-format-output
# just the instance IDs and states, as a table
aws ec2 describe-instances \
--query 'Reservations[].Instances[].[InstanceId,State.Name]' \
--output table
# plain text for shell scripting
aws ec2 describe-instances \
--query 'Reservations[0].Instances[0].PublicIpAddress' \
--output text--query runs client-side JMESPath after the API call. v1 defaults to json output unless configured; --output text prints None for missing fields, so guard scripts accordingly.
Sync a directory to S3s3-sync-directories
# upload changed files only
aws s3 sync ./build s3://my-bucket/site \
--exclude '*.map' \
--delete
# preview without touching anything
aws s3 sync ./build s3://my-bucket/site --delete --dryrun--delete removes remote files missing locally, which is what you want for site deploys and a disaster on shared buckets. Always run --dryrun first when --delete is involved.
Share a private S3 object temporarilys3-presigned-url
# valid for 1 hour by default
aws s3 presign s3://my-bucket/report.pdf
# valid for 15 minutes
aws s3 presign s3://my-bucket/report.pdf --expires-in 900The URL is signed with your current credentials; if those are temporary (assumed role), the link dies when the session expires, regardless of --expires-in.
Assume a role automatically via configassume-role-profile
# ~/.aws/config
[profile prod-admin]
role_arn = arn:aws:iam::123456789012:role/Admin
source_profile = default
mfa_serial = arn:aws:iam::111111111111:mfa/me
region = us-east-1
# then just:
# aws s3 ls --profile prod-adminThe CLI handles the sts:AssumeRole call, MFA prompt, and temporary credential caching for you; no manual export of AWS_SESSION_TOKEN needed.
Page through large result setscontrol-pagination
# first page of 50
aws s3api list-objects-v2 --bucket my-bucket --max-items 50
# continue from the returned NextToken
aws s3api list-objects-v2 --bucket my-bucket \
--max-items 50 \
--starting-token eyJDb250aW51YXRpb2...
# smaller API pages to avoid timeouts on huge buckets
aws s3api list-objects-v2 --bucket my-bucket --page-size 100By default the CLI auto-paginates and returns everything, which can eat memory on big listings. --max-items limits what you get; --page-size only changes the per-call chunk.
Filter EC2 results on the serverfilter-server-side
aws ec2 describe-instances \
--filters 'Name=tag:Env,Values=prod' \
'Name=instance-state-name,Values=running' \
--query 'Reservations[].Instances[].InstanceId' \
--output text--filters reduces what the API returns; --query only trims it client-side afterwards. On large accounts, doing both keeps calls fast and cheap.
Verify which identity you are usingcheck-current-identity
aws sts get-caller-identity
# {
# "UserId": "AIDA...",
# "Account": "123456789012",
# "Arn": "arn:aws:iam::123456789012:user/me"
# }The first command to run when anything returns AccessDenied: it tells you which account and principal your credential chain actually resolved to.
Configure via environment variables in CIenvironment-variable-config
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION=eu-west-1
# or point at a profile instead of keys
export AWS_PROFILE=staging
aws sts get-caller-identityEnvironment variables beat the config file, and explicit keys beat AWS_PROFILE. A stale AWS_PROFILE exported in your shell is a common source of 'why is this hitting the wrong account'.
Drive complex commands from a JSON filecli-input-json
# generate a template of all parameters
aws ec2 run-instances --generate-cli-skeleton > run.json
# edit run.json, then execute it
aws ec2 run-instances --cli-input-json file://run.jsonMuch saner than escaping nested structures on the command line, and the JSON file can live in version control next to your runbook.
See what a failing command actually doesdebug-failing-calls
aws s3api get-object-acl \
--bucket my-bucket --key file.txt \
--debug 2> debug.log
# the resolved credentials, region, request, and response are in debug.log--debug prints the full credential resolution order and signed request to stderr, which answers most 'works on my machine' auth mysteries. It also logs secrets-adjacent material, so do not paste it raw into tickets.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| boto3 | PyPI | The logic around your AWS calls is real code: conditionals, retries, or data processing beyond what --query can express. |
| aws-sam-cli | PyPI | Your workflow is serverless build, local test, and deploy of Lambda and API Gateway rather than general AWS administration. |
| s3cmd | PyPI | You only ever touch S3 and want a small standalone tool instead of the full AWS surface. |