How to Connect Python to AWS DynamoDB: Complete Guide
Amazon DynamoDB is a fully managed NoSQL database service that provides fast and predictable performance with seamless scalability. When combined with Python, DynamoDB enables developers to build highly scalable, serverless applications with minimal operational overhead. This comprehensive guide covers everything you need to know about connecting Python to DynamoDB.
Why Use DynamoDB with Python?
DynamoDB with Python offers powerful capabilities:
- Fully managed with zero server administration
- Consistent single-digit millisecond latency at any scale
- Built-in security, backup, and restore
- Automatic scaling based on workload
- Global tables for multi-region replication
- On-demand and provisioned capacity modes
- Perfect for serverless architectures
- Integrates seamlessly with AWS Lambda
Prerequisites
Before connecting Python to DynamoDB, ensure you have:
- Python 3.6 or higher installed
- AWS account with DynamoDB access
- AWS CLI configured with credentials
- Basic understanding of NoSQL and key-value stores
- boto3 library (AWS SDK for Python)
Installing boto3
Install the AWS SDK for Python:
bash
pip install boto3
Setting Up AWS Credentials
Configure AWS credentials using AWS CLI:
bash
aws configure
Or set environment variables:
bash
export AWS_ACCESS_KEY_ID='your_access_key'
export AWS_SECRET_ACCESS_KEY='your_secret_key'
export AWS_DEFAULT_REGION='us-east-1'
Basic Connection to DynamoDB
Let's start with a simple connection:
python
import boto3
# Create DynamoDB resource
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
# Create DynamoDB client (lower-level API)
client = boto3.client('dynamodb', region_name='us-east-1')
print("Connected to DynamoDB successfully!")
# List all tables
tables = list(dynamodb.tables.all())
print(f"Existing tables: {[table.name for table in tables]}")
Connection with Error Handling
python
import boto3
from botocore.exceptions import ClientError, NoCredentialsError
def create_dynamodb_connection():
try:
dynamodb = boto3.resource(
'dynamodb',
region_name='us-east-1',
endpoint_url=None # Use for local DynamoDB
)
# Test connection by listing tables
tables = list(dynamodb.tables.all())
print(f"Successfully connected to DynamoDB")
print(f"Available tables: {len(tables)}")
return dynamodb
except NoCredentialsError:
print("AWS credentials not found")
return None
except ClientError as e:
print(f"AWS error: {e}")
return None
except Exception as e:
print(f"Error: {e}")
return None
# Usage
dynamodb = create_dynamodb_connection()
Creating a Table
python
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
# Create table
try:
table = dynamodb.create_table(
TableName='Employees',
KeySchema=[
{
'AttributeName': 'employee_id',
'KeyType': 'HASH' # Partition key
},
{
'AttributeName': 'email',
'KeyType': 'RANGE' # Sort key
}
],
AttributeDefinitions=[
{
'AttributeName': 'employee_id',
'AttributeType': 'S' # String
},
{
'AttributeName': 'email',
'AttributeType': 'S'
}
],
BillingMode='PAY_PER_REQUEST' # On-demand billing
)
# Wait for table to be created
table.wait_until_exists()
print(f"Table {table.table_name} created successfully!")
print(f"Table status: {table.table_status}")
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceInUseException':
print("Table already exists")
else:
print(f"Error creating table: {e}")
Creating Table with Provisioned Capacity
python
import boto3
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.create_table(
TableName='Products',
KeySchema=[
{'AttributeName': 'product_id', 'KeyType': 'HASH'}
],
AttributeDefinitions=[
{'AttributeName': 'product_id', 'AttributeType': 'S'},
{'AttributeName': 'category', 'AttributeType': 'S'}
],
GlobalSecondaryIndexes=[
{
'IndexName': 'CategoryIndex',
'KeySchema': [
{'AttributeName': 'category', 'KeyType': 'HASH'}
],
'Projection': {'ProjectionType': 'ALL'},
'ProvisionedThroughput': {
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
}
],
ProvisionedThroughput={
'ReadCapacityUnits': 10,
'WriteCapacityUnits': 10
}
)
table.wait_until_exists()
print("Table with GSI created!")
Inserting Items
Insert Single Item
python
import boto3
from datetime import datetime
from decimal import Decimal
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Insert single item
response = table.put_item(
Item={
'employee_id': 'EMP001',
'email': 'john.doe@company.com',
'first_name': 'John',
'last_name': 'Doe',
'department': 'Engineering',
'salary': Decimal('75000.00'), # Use Decimal for numbers
'hire_date': str(datetime.now().date()),
'skills': ['Python', 'AWS', 'DynamoDB']
}
)
print(f"Item inserted successfully!")
print(f"Response: {response['ResponseMetadata']['HTTPStatusCode']}")
Note: DynamoDB requires Decimal type for numbers, not float.
Batch Write Items
python
import boto3
from decimal import Decimal
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Batch write items
with table.batch_writer() as batch:
batch.put_item(
Item={
'employee_id': 'EMP002',
'email': 'jane@company.com',
'first_name': 'Jane',
'last_name': 'Smith',
'department': 'Marketing',
'salary': Decimal('68000')
}
)
batch.put_item(
Item={
'employee_id': 'EMP003',
'email': 'bob@company.com',
'first_name': 'Bob',
'last_name': 'Johnson',
'department': 'Sales',
'salary': Decimal('72000')
}
)
batch.put_item(
Item={
'employee_id': 'EMP004',
'email': 'alice@company.com',
'first_name': 'Alice',
'last_name': 'Williams',
'department': 'Engineering',
'salary': Decimal('80000')
}
)
print("Batch write completed!")
Retrieving Items
Get Single Item
python
import boto3
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Get item by key
response = table.get_item(
Key={
'employee_id': 'EMP001',
'email': 'john.doe@company.com'
}
)
if 'Item' in response:
item = response['Item']
print(f"Employee: {item['first_name']} {item['last_name']}")
print(f"Department: {item['department']}")
print(f"Salary: ${item['salary']}")
else:
print("Item not found")
Batch Get Items
python
import boto3
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
# Batch get items
response = dynamodb.batch_get_item(
RequestItems={
'Employees': {
'Keys': [
{'employee_id': 'EMP001', 'email': 'john.doe@company.com'},
{'employee_id': 'EMP002', 'email': 'jane@company.com'},
{'employee_id': 'EMP003', 'email': 'bob@company.com'}
]
}
}
)
items = response['Responses']['Employees']
print(f"Retrieved {len(items)} items")
for item in items:
print(f"{item['first_name']} {item['last_name']} - {item['department']}")
Query Operation
Query is efficient for retrieving items with same partition key:
python
import boto3
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Query by partition key
response = table.query(
KeyConditionExpression=Key('employee_id').eq('EMP001')
)
items = response['Items']
print(f"Found {len(items)} items")
for item in items:
print(f"{item['first_name']} {item['last_name']}")
# Query with sort key condition
response = table.query(
KeyConditionExpression=Key('employee_id').eq('EMP001') &
Key('email').begins_with('john')
)
Scan Operation
Scan reads all items in table (expensive operation):
python
import boto3
from boto3.dynamodb.conditions import Attr
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Scan all items
response = table.scan()
items = response['Items']
print(f"Total items: {len(items)}")
for item in items:
print(f"{item['first_name']} {item['last_name']}")
# Scan with filter
response = table.scan(
FilterExpression=Attr('department').eq('Engineering')
)
engineers = response['Items']
print(f"\nEngineers: {len(engineers)}")
Updating Items
python
import boto3
from decimal import Decimal
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Update item
response = table.update_item(
Key={
'employee_id': 'EMP001',
'email': 'john.doe@company.com'
},
UpdateExpression='SET salary = :new_salary, department = :new_dept',
ExpressionAttributeValues={
':new_salary': Decimal('85000'),
':new_dept': 'Senior Engineering'
},
ReturnValues='UPDATED_NEW'
)
print(f"Updated attributes: {response['Attributes']}")
# Atomic counter increment
response = table.update_item(
Key={
'employee_id': 'EMP001',
'email': 'john.doe@company.com'
},
UpdateExpression='SET salary = salary + :increment',
ExpressionAttributeValues={
':increment': Decimal('5000')
},
ReturnValues='UPDATED_NEW'
)
print(f"New salary: {response['Attributes']['salary']}")
# Add to list
response = table.update_item(
Key={
'employee_id': 'EMP001',
'email': 'john.doe@company.com'
},
UpdateExpression='SET skills = list_append(skills, :new_skill)',
ExpressionAttributeValues={
':new_skill': ['Docker']
},
ReturnValues='UPDATED_NEW'
)
Conditional Updates
python
import boto3
from boto3.dynamodb.conditions import Attr
from botocore.exceptions import ClientError
from decimal import Decimal
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Update only if condition is met
try:
response = table.update_item(
Key={
'employee_id': 'EMP001',
'email': 'john.doe@company.com'
},
UpdateExpression='SET salary = :new_salary',
ConditionExpression=Attr('salary').lt(Decimal('80000')),
ExpressionAttributeValues={
':new_salary': Decimal('85000')
},
ReturnValues='UPDATED_NEW'
)
print("Update successful!")
except ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
print("Condition not met - update skipped")
else:
print(f"Error: {e}")
Deleting Items
python
import boto3
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Delete single item
response = table.delete_item(
Key={
'employee_id': 'EMP005',
'email': 'old@company.com'
}
)
print(f"Item deleted: {response['ResponseMetadata']['HTTPStatusCode']}")
# Conditional delete
from boto3.dynamodb.conditions import Attr
response = table.delete_item(
Key={
'employee_id': 'EMP006',
'email': 'temp@company.com'
},
ConditionExpression=Attr('department').eq('Temporary')
)
Working with Global Secondary Indexes
python
import boto3
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Products')
# Query using GSI
response = table.query(
IndexName='CategoryIndex',
KeyConditionExpression=Key('category').eq('Electronics')
)
items = response['Items']
print(f"Electronics products: {len(items)}")
for item in items:
print(f"{item['product_id']}: {item.get('name', 'N/A')}")
Pagination
python
import boto3
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Employees')
# Paginate through scan results
response = table.scan(Limit=10)
items = response['Items']
print(f"Page 1: {len(items)} items")
# Continue pagination
while 'LastEvaluatedKey' in response:
response = table.scan(
Limit=10,
ExclusiveStartKey=response['LastEvaluatedKey']
)
items.extend(response['Items'])
print(f"Retrieved {len(response['Items'])} more items")
print(f"Total items retrieved: {len(items)}")
Transactions
DynamoDB supports ACID transactions:
python
import boto3
from decimal import Decimal
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
client = boto3.client('dynamodb', region_name='us-east-1')
# Transactional write
try:
response = client.transact_write_items(
TransactItems=[
{
'Update': {
'TableName': 'Employees',
'Key': {
'employee_id': {'S': 'EMP001'},
'email': {'S': 'john@company.com'}
},
'UpdateExpression': 'SET salary = salary - :amount',
'ExpressionAttributeValues': {
':amount': {'N': '5000'}
}
}
},
{
'Update': {
'TableName': 'Employees',
'Key': {
'employee_id': {'S': 'EMP002'},
'email': {'S': 'jane@company.com'}
},
'UpdateExpression': 'SET salary = salary + :amount',
'ExpressionAttributeValues': {
':amount': {'N': '5000'}
}
}
}
]
)
print("Transaction successful!")
except Exception as e:
print(f"Transaction failed: {e}")
Complete Application Example
python
import boto3
from boto3.dynamodb.conditions import Key, Attr
from botocore.exceptions import ClientError
from decimal import Decimal
from datetime import datetime
class EmployeeDatabase:
def __init__(self, region='us-east-1'):
self.dynamodb = boto3.resource('dynamodb', region_name=region)
self.table_name = 'Employees'
self.table = self.dynamodb.Table(self.table_name)
def create_table(self):
try:
table = self.dynamodb.create_table(
TableName=self.table_name,
KeySchema=[
{'AttributeName': 'employee_id', 'KeyType': 'HASH'},
{'AttributeName': 'email', 'KeyType': 'RANGE'}
],
AttributeDefinitions=[
{'AttributeName': 'employee_id', 'AttributeType': 'S'},
{'AttributeName': 'email', 'AttributeType': 'S'},
{'AttributeName': 'department', 'AttributeType': 'S'}
],
GlobalSecondaryIndexes=[
{
'IndexName': 'DepartmentIndex',
'KeySchema': [
{'AttributeName': 'department', 'KeyType': 'HASH'}
],
'Projection': {'ProjectionType': 'ALL'},
'ProvisionedThroughput': {
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
}
],
BillingMode='PAY_PER_REQUEST'
)
table.wait_until_exists()
print(f"Table {self.table_name} created")
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceInUseException':
print("Table already exists")
def add_employee(self, employee_id, email, first_name, last_name,
department, salary):
try:
self.table.put_item(
Item={
'employee_id': employee_id,
'email': email,
'first_name': first_name,
'last_name': last_name,
'department': department,
'salary': Decimal(str(salary)),
'created_at': str(datetime.now())
}
)
return True
except ClientError as e:
print(f"Error adding employee: {e}")
return False
def get_employee(self, employee_id, email):
try:
response = self.table.get_item(
Key={'employee_id': employee_id, 'email': email}
)
return response.get('Item')
except ClientError as e:
print(f"Error getting employee: {e}")
return None
def get_employees_by_department(self, department):
try:
response = self.table.query(
IndexName='DepartmentIndex',
KeyConditionExpression=Key('department').eq(department)
)
return response['Items']
except ClientError as e:
print(f"Error querying department: {e}")
return []
def update_salary(self, employee_id, email, new_salary):
try:
response = self.table.update_item(
Key={'employee_id': employee_id, 'email': email},
UpdateExpression='SET salary = :salary',
ExpressionAttributeValues={':salary': Decimal(str(new_salary))},
ReturnValues='UPDATED_NEW'
)
return response['Attributes']
except ClientError as e:
print(f"Error updating salary: {e}")
return None
def delete_employee(self, employee_id, email):
try:
self.table.delete_item(
Key={'employee_id': employee_id, 'email': email}
)
return True
except ClientError as e:
print(f"Error deleting employee: {e}")
return False
# Usage
db = EmployeeDatabase()
# Add employee
db.add_employee('EMP001', 'sarah@company.com', 'Sarah', 'Connor',
'Security', 78000)
# Get employee
employee = db.get_employee('EMP001', 'sarah@company.com')
print(f"Employee: {employee['first_name']} {employee['last_name']}")
# Get by department
engineers = db.get_employees_by_department('Engineering')
print(f"Engineers: {len(engineers)}")
# Update salary
db.update_salary('EMP001', 'sarah@company.com', 82000)
Best Practices
Follow these best practices when using DynamoDB with Python:
- Design tables based on access patterns
- Use Decimal type for numeric values
- Implement exponential backoff for retries
- Use batch operations for multiple items
- Choose appropriate partition keys to avoid hot partitions
- Use GSIs for alternative query patterns
- Enable point-in-time recovery for critical tables
- Monitor capacity metrics in CloudWatch
- Use on-demand billing for unpredictable workloads
- Implement proper error handling for all operations
- Use conditional writes to prevent race conditions
- Avoid scans on large tables - use Query instead
- Use projection expressions to fetch only needed attributes
- Enable encryption at rest for sensitive data
- Cache frequently accessed data using DAX
Conclusion
Connecting Python to AWS DynamoDB provides a powerful foundation for building serverless, highly scalable applications. The boto3 library offers comprehensive features and seamless integration with DynamoDB's capabilities. By following the examples and best practices in this guide, you'll be able to build robust applications that leverage DynamoDB's speed, scalability, and fully managed nature. Whether you're building web applications, mobile backends, or IoT platforms, mastering Python-DynamoDB connectivity is essential for modern cloud-native development on AWS.