Introduction to Dictionaries
Welcome to our exploration of Python dictionaries! If you're building web applications, working with APIs, or handling data in virtually any form, dictionaries will become your constant companion. Today, we'll discover why dictionaries are one of Python's most powerful and versatile data structures.
Dictionaries provide a way to store data as key-value pairs, enabling fast lookup, flexible data modeling, and intuitive data representation. Think of a dictionary as a real-world address book where names (keys) help you quickly find phone numbers (values). This direct mapping makes dictionaries ideal for scenarios where you need to retrieve, update, or organize data efficiently.
Real-World Analogy: Library Catalog
Consider a library catalog where each book has a unique ISBN number. When you want to find information about a specific book, you look it up by its ISBN rather than searching through every book on the shelves. Python dictionaries work similarly – they let you retrieve values instantly using their associated keys, rather than searching through the entire collection sequentially.
Dictionary Fundamentals
Creating Dictionaries
Python offers several ways to create dictionaries:
# Empty dictionary
empty_dict = {}
another_empty = dict()
# Dictionary with initial key-value pairs
student = {
"name": "John Smith",
"age": 20,
"major": "Computer Science",
"gpa": 3.7
}
# Using dict() constructor with keyword arguments
config = dict(
host="localhost",
port=8080,
debug=True
)
# Using dict() with a list of tuples
colors = dict([
("red", "#FF0000"),
("green", "#00FF00"),
("blue", "#0000FF")
])
# Dictionary comprehension (covered in detail later)
squares = {x: x*x for x in range(6)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
Each approach has its use cases. Curly braces are most common for literal dictionaries, while dict() constructor can be handy when creating dictionaries programmatically or from other data structures.
Keys and Values
In Python dictionaries:
- Keys must be immutable (strings, numbers, tuples of immutable elements)
- Values can be any Python object (including other dictionaries)
- Keys must be unique within a dictionary
# String keys (most common)
user = {
"username": "jsmith",
"email": "john@example.com",
"is_active": True
}
# Number keys
employee_ids = {
101: "Alice Johnson",
102: "Bob Smith",
103: "Charlie Davis"
}
# Tuple keys (must contain only immutable elements)
coordinates = {
(0, 0): "origin",
(0, 1): "north",
(1, 0): "east"
}
# Invalid - lists cannot be keys because they're mutable
# error_dict = {[1, 2]: "value"} # This raises TypeError
The restriction on immutable keys ensures that dictionary lookups remain efficient and reliable. If keys could change their values, the internal hash table would become corrupted.
Accessing Dictionary Values
There are multiple ways to access values in a dictionary:
student = {
"name": "John Smith",
"age": 20,
"major": "Computer Science",
"gpa": 3.7
}
# Using square bracket notation
name = student["name"]
print(name) # "John Smith"
# Using get() method - safer, returns None if key doesn't exist
age = student.get("age")
print(age) # 20
# Providing a default value with get()
graduation_year = student.get("graduation_year", 2025)
print(graduation_year) # 2025 (default value since key doesn't exist)
# This raises KeyError because the key doesn't exist
# missing = student["graduation_year"]
# Checking if a key exists
if "gpa" in student:
print(f"GPA: {student['gpa']}")
if "address" not in student:
print("No address information available")
The get() method is typically preferred in production code because it won't raise an exception for missing keys. This is especially important when dealing with user inputs or external data sources where you can't guarantee the presence of all keys.
Real-World Usage: User Profiles
When handling user data from forms or APIs, dictionaries provide a natural way to store and access profile information:
# User profile from a web form or API
user_profile = {
"user_id": "u12345",
"name": "Sarah Connor",
"email": "sarah@example.com",
"preferences": {
"theme": "dark",
"notifications": True,
"language": "en-US"
},
"subscription": {
"plan": "premium",
"renewal_date": "2023-12-31"
}
}
# Safe way to access nested properties
theme = user_profile.get("preferences", {}).get("theme", "default")
print(f"User theme: {theme}") # "dark"
# Checking for subscription status
if user_profile.get("subscription", {}).get("plan") == "premium":
print("Access granted to premium features")
# Handling missing data gracefully
address = user_profile.get("address", {}).get("city", "Unknown")
print(f"City: {address}") # "Unknown" (because address key doesn't exist)
Common Dictionary Operations
Adding and Updating Elements
Dictionaries are mutable, so you can easily add or modify entries:
product = {
"id": "P001",
"name": "Smartphone",
"price": 699.99
}
# Adding new key-value pairs
product["brand"] = "TechCo"
product["in_stock"] = True
# Updating existing values
product["price"] = 649.99
# Adding/updating multiple items at once with update()
product.update({
"color": "Black",
"storage": "128GB",
"price": 599.99 # This overwrites the previous price
})
print(product)
# {'id': 'P001', 'name': 'Smartphone', 'price': 599.99, 'brand': 'TechCo',
# 'in_stock': True, 'color': 'Black', 'storage': '128GB'}
The update() method is particularly useful when you need to merge data from multiple sources, such as combining default settings with user preferences.
Removing Elements
Python provides several ways to remove items from dictionaries:
server_config = {
"host": "192.168.1.10",
"port": 8080,
"user": "admin",
"password": "secure123",
"debug": True,
"temp_setting": "delete_me"
}
# Remove specific key-value pair and return the value
password = server_config.pop("password")
print(f"Removed password: {password}")
# Remove and return the last inserted item (Python 3.7+ where dicts maintain insertion order)
last_item = server_config.popitem()
print(f"Last item: {last_item}") # ('temp_setting', 'delete_me')
# Delete a specific key
del server_config["debug"]
# Clear all items
server_config.clear()
print(server_config) # {}
Each removal method has its specific use case. Use pop() when you need the value you're removing, del for straightforward removal, and clear() when you need to empty a dictionary but keep the variable.
Error Handling with Dictionary Removal
config = {"host": "localhost", "port": 8000, "debug": True}
# Safe removal with pop() - provides a default if key doesn't exist
timeout = config.pop("timeout", 30) # Returns 30 since "timeout" doesn't exist
print(f"Timeout set to: {timeout}")
# This raises KeyError if the key doesn't exist
try:
protocol = config.pop("protocol") # KeyError: 'protocol'
except KeyError as e:
print(f"Error: {e}")
protocol = "http"
print(f"Using protocol: {protocol}")
Dictionary Methods
Dictionaries come with several built-in methods for common operations:
| Method | Description | Example |
|---|---|---|
keys() |
Returns a view object of all keys | dict.keys() |
values() |
Returns a view object of all values | dict.values() |
items() |
Returns a view object of all key-value pairs as tuples | dict.items() |
get(key, default) |
Returns value for key, or default if key doesn't exist | dict.get('key', 'default') |
pop(key, default) |
Removes key and returns its value | dict.pop('key', 'default') |
popitem() |
Removes and returns the last inserted key-value pair | dict.popitem() |
update(other_dict) |
Updates dictionary with key-value pairs from another dictionary | dict.update({'key': 'value'}) |
clear() |
Removes all items | dict.clear() |
copy() |
Returns a shallow copy of the dictionary | dict.copy() |
setdefault(key, default) |
Returns value for key, or sets key to default and returns default if key doesn't exist | dict.setdefault('key', 'default') |
user_data = {
"username": "jdoe",
"email": "john.doe@example.com",
"active": True
}
# Getting all keys, values, and items
keys = user_data.keys()
print(f"Keys: {list(keys)}") # Convert view to list for display
values = user_data.values()
print(f"Values: {list(values)}")
items = user_data.items()
print(f"Items: {list(items)}")
# View objects are dynamic - they update when the dictionary changes
user_data["last_login"] = "2023-05-15"
print(f"Updated keys: {list(keys)}") # Now includes 'last_login'
# setdefault - get value if exists, otherwise set default
role = user_data.setdefault("role", "user") # Adds 'role': 'user' if not present
print(f"Role: {role}")
print(user_data) # Now contains the 'role' key with 'user' value
# Creating a copy
user_data_copy = user_data.copy()
user_data_copy["username"] = "johndoe" # Modifying the copy doesn't affect original
print(f"Original: {user_data['username']}") # Still "jdoe"
print(f"Copy: {user_data_copy['username']}") # "johndoe"
The view objects returned by keys(), values(), and items() are particularly useful because they dynamically reflect changes to the dictionary without needing to be regenerated.
Dictionary Comprehensions
Similar to list comprehensions, dictionary comprehensions provide a concise way to create dictionaries:
# Basic dictionary comprehension: {key_expr: value_expr for item in iterable}
squares = {x: x**2 for x in range(1, 6)}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# With conditional filtering
even_squares = {x: x**2 for x in range(1, 11) if x % 2 == 0}
print(even_squares) # {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}
# Converting between data formats
names = ["Alice", "Bob", "Charlie", "David"]
name_lengths = {name: len(name) for name in names}
print(name_lengths) # {'Alice': 5, 'Bob': 3, 'Charlie': 7, 'David': 5}
# Transforming an existing dictionary
prices = {"apple": 0.5, "banana": 0.25, "orange": 0.75, "pear": 0.60}
discounted_prices = {item: price * 0.8 for item, price in prices.items()}
print(discounted_prices) # {'apple': 0.4, 'banana': 0.2, 'orange': 0.6, 'pear': 0.48}
# Multiple conditions
filtered_prices = {item: price for item, price in prices.items()
if price > 0.3 if len(item) > 4}
print(filtered_prices) # {'orange': 0.75}
Dictionary comprehensions shine when transforming data or creating mappings from other collections. They combine the conciseness of comprehensions with the power of dictionaries.
Real-World Example: API Response Processing
When processing API responses, dictionary comprehensions can help transform and filter data:
# Simulated API response (list of products)
api_response = [
{"id": "P001", "name": "Smartphone", "price": 699.99, "in_stock": True},
{"id": "P002", "name": "Laptop", "price": 1299.99, "in_stock": True},
{"id": "P003", "name": "Headphones", "price": 199.99, "in_stock": False},
{"id": "P004", "name": "Tablet", "price": 499.99, "in_stock": True},
{"id": "P005", "name": "Smartwatch", "price": 249.99, "in_stock": False}
]
# Create a lookup dictionary by product ID
products_by_id = {item["id"]: item for item in api_response}
print(f"Product P003: {products_by_id['P003']['name']}")
# Create price map of in-stock products
in_stock_prices = {item["name"]: item["price"] for item in api_response if item["in_stock"]}
print(f"In-stock product prices: {in_stock_prices}")
# Extract specific fields for display
product_display = {
item["name"]: f"${item['price']} - {'In Stock' if item['in_stock'] else 'Out of Stock'}"
for item in api_response
}
print(product_display)
# Group products by availability status
status_groups = {"In Stock": [], "Out of Stock": []}
for product in api_response:
key = "In Stock" if product["in_stock"] else "Out of Stock"
status_groups[key].append(product["name"])
print(f"Product availability: {status_groups}")
Nested Dictionaries
Dictionaries can contain other dictionaries, enabling representation of complex hierarchical data:
# Nested dictionary representing a simple e-commerce system
store = {
"name": "Online Gadget Store",
"products": {
"P001": {
"name": "Smartphone",
"price": 699.99,
"specs": {
"screen": "6.1 inch",
"processor": "A15",
"storage": "128GB"
},
"inventory": {
"in_stock": 42,
"warehouse_locations": ["East", "West"]
}
},
"P002": {
"name": "Laptop",
"price": 1299.99,
"specs": {
"screen": "15.6 inch",
"processor": "i7",
"storage": "512GB SSD"
},
"inventory": {
"in_stock": 15,
"warehouse_locations": ["East"]
}
}
},
"locations": {
"East": {
"address": "123 East St",
"manager": "Alice Johnson"
},
"West": {
"address": "456 West Blvd",
"manager": "Bob Smith"
}
}
}
Accessing Nested Dictionary Values
You can access nested values using chained square brackets or the get() method for safer access:
# Direct access (may raise KeyError if keys don't exist)
try:
laptop_price = store["products"]["P002"]["price"]
print(f"Laptop price: ${laptop_price}")
# Accessing deeply nested properties
smartphone_storage = store["products"]["P001"]["specs"]["storage"]
print(f"Smartphone storage: {smartphone_storage}")
except KeyError as e:
print(f"Error accessing key: {e}")
# Safe access with get() method
smartphone_screen = store.get("products", {}).get("P001", {}).get("specs", {}).get("screen", "Unknown")
print(f"Smartphone screen: {smartphone_screen}")
# This won't raise an error, just returns the default value
nonexistent_product = store.get("products", {}).get("P999", {}).get("name", "Product not found")
print(nonexistent_product) # "Product not found"
Modifying Nested Dictionaries
You can modify values at any level of nesting:
# Update a nested value
store["products"]["P001"]["price"] = 649.99
# Add a new nested property
store["products"]["P001"]["discount"] = 0.1
# Modify deeply nested properties
store["products"]["P002"]["specs"]["storage"] = "1TB SSD"
# Add a completely new nested structure
store["products"]["P003"] = {
"name": "Tablet",
"price": 499.99,
"specs": {
"screen": "10.9 inch",
"processor": "A14",
"storage": "64GB"
},
"inventory": {
"in_stock": 28,
"warehouse_locations": ["East", "West"]
}
}
# Update multiple nested properties at once
store["products"]["P001"].update({
"price": 599.99,
"discount": 0.15,
"color": "Black"
})
Practical Application: Configuration Management
Nested dictionaries are perfect for configuration settings in applications:
# Application configuration with nested settings
config = {
"app": {
"name": "MyWebApp",
"version": "1.2.0",
"debug": True
},
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp_db",
"user": "admin",
"password": "secure_password",
"pool_size": 10
},
"api": {
"base_url": "https://api.example.com/v1",
"timeout": 30,
"retry": {
"max_attempts": 3,
"backoff_factor": 2
}
},
"logging": {
"level": "INFO",
"file": "/var/log/myapp.log",
"rotate": {
"when": "midnight",
"backup_count": 7
}
}
}
# Function to get configuration with dot notation path
def get_config(path, default=None):
"""Access config values with dot notation path."""
keys = path.split('.')
value = config
for key in keys:
if isinstance(value, dict) and key in value:
value = value[key]
else:
return default
return value
# Example usage
db_host = get_config("database.host")
print(f"Database host: {db_host}")
log_level = get_config("logging.level")
print(f"Logging level: {log_level}")
max_retries = get_config("api.retry.max_attempts")
print(f"API max retry attempts: {max_retries}")
# Loading environment-specific overrides
def load_environment_config(env):
"""Load environment-specific configuration overrides."""
# In a real app, these might come from files or environment variables
environments = {
"development": {
"app.debug": True,
"database.host": "localhost"
},
"staging": {
"app.debug": False,
"database.host": "staging-db.example.com"
},
"production": {
"app.debug": False,
"database.host": "prod-db.example.com",
"logging.level": "WARNING"
}
}
# Apply overrides for the specified environment
if env in environments:
for path, value in environments[env].items():
# Set config value (simplified implementation)
keys = path.split('.')
target = config
for key in keys[:-1]:
target = target[key]
target[keys[-1]] = value
print(f"Loaded configuration for {env} environment")
else:
print(f"Unknown environment: {env}")
# Load production configuration
load_environment_config("production")
print(f"Updated database host: {config['database']['host']}")
print(f"Updated debug mode: {config['app']['debug']}")
Merging Dictionaries
Python offers several ways to combine dictionaries:
# Two separate dictionaries
user_info = {
"name": "John Doe",
"email": "john@example.com"
}
user_settings = {
"theme": "dark",
"notifications": True,
"language": "en-US"
}
# Method 1: Using update() (modifies the first dictionary)
user_data = user_info.copy() # Create a copy to avoid modifying the original
user_data.update(user_settings)
print(user_data)
# Method 2: Dictionary unpacking (Python 3.5+)
merged = {**user_info, **user_settings}
print(merged)
# Method 3: Using dict() constructor
merged_alt = dict(user_info, **user_settings) # Less common
print(merged_alt)
# Method 4: Using the | operator (Python 3.9+)
# merged_new = user_info | user_settings
# print(merged_new)
# Handling key conflicts
defaults = {
"theme": "light",
"notifications": False,
"language": "en-US",
"auto_save": True
}
# Later keys override earlier ones with the same name
user_preferences = {**defaults, **user_settings}
print(user_preferences) # Uses settings from user_settings where available, defaults otherwise
# Merging multiple dictionaries
profile = {
"bio": "Python developer",
"location": "San Francisco"
}
complete_user = {**user_info, **user_settings, **profile}
print(complete_user)
Dictionary unpacking with ** is particularly elegant and clearly shows the intent to merge dictionaries. In Python 3.9+, the pipe operator | and the update operator |= provide even more intuitive syntax for merging.
Deep Merging
The standard methods perform shallow merges. For nested dictionaries, you might need a deep merge function:
def deep_merge(dict1, dict2):
"""
Recursively merge dict2 into dict1.
If keys exist in both, and both values are dictionaries, merge those dictionaries.
Otherwise, values from dict2 overwrite those in dict1.
"""
result = dict1.copy()
for key, value in dict2.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
# Recursively merge nested dictionaries
result[key] = deep_merge(result[key], value)
else:
# Overwrite or add key-value pair
result[key] = value
return result
# Default configuration
default_config = {
"app": {
"name": "MyApp",
"debug": False,
"components": ["core", "auth"]
},
"database": {
"host": "localhost",
"port": 5432,
"settings": {
"pool_size": 10,
"timeout": 30
}
}
}
# User configuration overrides
user_config = {
"app": {
"debug": True,
"components": ["core", "auth", "api"]
},
"database": {
"host": "db.example.com",
"settings": {
"pool_size": 20
}
}
}
# Deep merge configurations
merged_config = deep_merge(default_config, user_config)
print(merged_config)
# Result preserves nested structure while applying overrides
# {
# 'app': {
# 'name': 'MyApp', # Kept from default
# 'debug': True, # Overridden by user config
# 'components': ['core', 'auth', 'api'] # Overridden by user config
# },
# 'database': {
# 'host': 'db.example.com', # Overridden by user config
# 'port': 5432, # Kept from default
# 'settings': {
# 'pool_size': 20, # Overridden by user config
# 'timeout': 30 # Kept from default
# }
# }
# }
Practical Application: API Response Handling
Deep merging is useful when working with API responses that need to be combined with cached data:
# Cached product data (complete but possibly outdated)
cached_product = {
"id": "P001",
"name": "Smartphone",
"price": 699.99,
"description": "Latest model with advanced features.",
"specs": {
"screen": "6.1 inch",
"processor": "A15",
"storage": "128GB",
"camera": "12MP",
"battery": "3000mAh"
},
"reviews": [
{"user": "user1", "rating": 5, "comment": "Great phone!"},
{"user": "user2", "rating": 4, "comment": "Good value."}
],
"last_updated": "2023-04-15"
}
# New partial data from API (only changed fields)
api_update = {
"id": "P001",
"price": 649.99, # Price dropped
"specs": {
"processor": "A16", # New processor
"battery": "3200mAh" # Better battery
},
"last_updated": "2023-05-20"
}
# Merge updates into cached data
updated_product = deep_merge(cached_product, api_update)
print(f"Updated product: {updated_product['name']}")
print(f"New price: ${updated_product['price']}")
print(f"Processor: {updated_product['specs']['processor']}")
print(f"Last updated: {updated_product['last_updated']}")
# Original reviews are preserved
print(f"Review count: {len(updated_product['reviews'])}")
Dictionaries in Web Development
Dictionaries are foundational to Python web development, appearing in virtually every aspect of web applications:
Working with JSON
Python dictionaries map directly to JSON objects, making them perfect for API interactions:
import json
# Creating an API response
user_response = {
"id": 123,
"username": "jsmith",
"email": "john@example.com",
"profile": {
"full_name": "John Smith",
"bio": "Software developer",
"location": "San Francisco"
},
"preferences": {
"theme": "dark",
"notifications": True
},
"posts": [
{"id": 1, "title": "Hello World", "likes": 42},
{"id": 2, "title": "Python Tips", "likes": 28}
]
}
# Converting dictionary to JSON string
json_string = json.dumps(user_response, indent=2)
print(f"JSON response:\n{json_string}")
# Converting JSON back to dictionary
parsed_data = json.loads(json_string)
print(f"Username: {parsed_data['username']}")
print(f"Post count: {len(parsed_data['posts'])}")
# Writing JSON to a file
with open("user_data.json", "w") as f:
json.dump(user_response, f, indent=2)
# Reading JSON from a file
with open("user_data.json", "r") as f:
loaded_data = json.load(f)
print(f"Loaded user: {loaded_data['profile']['full_name']}")
HTTP Request and Response Handling
Web frameworks like Flask and Django heavily utilize dictionaries:
# Simplified Flask-like route handling
def handle_login():
# Request data as dictionary
request_data = {
"method": "POST",
"form": {
"username": "jsmith",
"password": "secret123",
"remember_me": "true"
},
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0",
"Accept-Language": "en-US,en;q=0.9"
}
}
# Extract form data
username = request_data["form"].get("username")
password = request_data["form"].get("password")
remember = request_data["form"].get("remember_me") == "true"
# Authentication logic (simplified)
if username == "jsmith" and password == "secret123":
# Response as dictionary
response = {
"status_code": 200,
"body": {
"message": "Login successful",
"user_id": 123,
"token": "abc123xyz456"
},
"headers": {
"Content-Type": "application/json",
"Set-Cookie": f"session=abc123xyz456; {'Max-Age=2592000;' if remember else ''} Path=/"
}
}
else:
response = {
"status_code": 401,
"body": {
"message": "Invalid credentials"
},
"headers": {
"Content-Type": "application/json"
}
}
# In real framework, this would be converted to an HTTP response
print(f"Status: {response['status_code']}")
print(f"Body: {response['body']}")
print(f"Headers: {response['headers']}")
return response
# Simulate route call
handle_login()
Template Context
Web templates receive variables through dictionary-like context objects:
def render_profile_page(user_id):
# In a real app, this would fetch data from a database
user_data = {
"id": user_id,
"username": "jsmith",
"email": "john@example.com",
"joined": "2023-01-15",
"posts": [
{"title": "Hello World", "date": "2023-01-16"},
{"title": "Python Tips", "date": "2023-02-05"},
{"title": "Web Development", "date": "2023-03-20"}
]
}
# Create template context
context = {
"user": user_data,
"page_title": f"Profile: {user_data['username']}",
"is_admin": False,
"current_year": 2023,
"site_name": "My Web App"
}
# In a real app, this would render a template
print("Template rendering with context:")
print(f"Title: {context['page_title']}")
print(f"User: {context['user']['username']}")
print(f"Post count: {len(context['user']['posts'])}")
# The template might access these variables like:
# <h1>{{ page_title }}</h1>
# <p>Username: {{ user.username }}</p>
# <p>Email: {{ user.email }}</p>
# {% for post in user.posts %}
# <div>{{ post.title }} - {{ post.date }}</div>
# {% endfor %}
return context
# Render a user profile page
render_profile_page(123)
Real-World Example: Flask Application
This example shows how dictionaries are used throughout a typical Flask web application:
# This is simplified Flask-like code (not meant to run directly)
# Route decorator uses dictionaries for configuration
@app.route("/api/products", methods=["GET"])
def get_products():
# Request object often represented as dictionary-like structure
query_params = request.args
# Database query params
filter_params = {
"category": query_params.get("category"),
"min_price": query_params.get("min_price", type=float),
"max_price": query_params.get("max_price", type=float),
"in_stock": query_params.get("in_stock", "").lower() == "true"
}
# Clean up None values
clean_params = {k: v for k, v in filter_params.items() if v is not None}
# Simulated database query
products = [
{"id": 1, "name": "Product A", "price": 19.99, "category": "Electronics"},
{"id": 2, "name": "Product B", "price": 29.99, "category": "Home"},
{"id": 3, "name": "Product C", "price": 9.99, "category": "Electronics"}
]
# Filter products based on parameters
filtered_products = products
if "category" in clean_params:
filtered_products = [p for p in filtered_products
if p["category"] == clean_params["category"]]
if "min_price" in clean_params:
filtered_products = [p for p in filtered_products
if p["price"] >= clean_params["min_price"]]
if "max_price" in clean_params:
filtered_products = [p for p in filtered_products
if p["price"] <= clean_params["max_price"]]
# Response as dictionary
response = {
"total": len(filtered_products),
"products": filtered_products
}
# Convert to JSON and return
return jsonify(response)
# Another example with form data
@app.route("/api/products", methods=["POST"])
def create_product():
# Request JSON body parsed as dictionary
product_data = request.json
# Validate required fields
required_fields = ["name", "price", "category"]
missing_fields = [field for field in required_fields if field not in product_data]
if missing_fields:
# Error response
error_response = {
"error": "Missing required fields",
"missing_fields": missing_fields
}
return jsonify(error_response), 400
# Process the new product (simplified)
new_product = {
"id": 4, # In real app, this would be generated
"name": product_data["name"],
"price": product_data["price"],
"category": product_data["category"]
}
# Success response
return jsonify({
"message": "Product created successfully",
"product": new_product
}), 201
Common Dictionary Patterns and Idioms
Defaultdict: Handling Missing Keys
The defaultdict from the collections module provides a convenient way to handle missing keys automatically:
from collections import defaultdict
# Regular dict requires manual initialization for counters
word_counts = {}
text = "the quick brown fox jumps over the lazy dog"
words = text.split()
for word in words:
if word not in word_counts:
word_counts[word] = 0
word_counts[word] += 1
print(f"Regular dict counts: {word_counts}")
# Using defaultdict simplifies this common pattern
word_counts_default = defaultdict(int) # Default value is 0 for int()
for word in words:
word_counts_default[word] += 1
print(f"defaultdict counts: {dict(word_counts_default)}")
# Other useful defaultdict applications
# Group items by some property
animals = [
("dog", "mammal"),
("cat", "mammal"),
("snake", "reptile"),
("lizard", "reptile"),
("eagle", "bird"),
("sparrow", "bird")
]
# Without defaultdict
animal_groups = {}
for animal, group in animals:
if group not in animal_groups:
animal_groups[group] = []
animal_groups[group].append(animal)
print(f"Regular dict grouping: {animal_groups}")
# With defaultdict
animal_groups_default = defaultdict(list)
for animal, group in animals:
animal_groups_default[group].append(animal)
print(f"defaultdict grouping: {dict(animal_groups_default)}")
# Nested defaultdicts for hierarchical data
nested_data = defaultdict(lambda: defaultdict(list))
entries = [
("2023-05-01", "user1", "login"),
("2023-05-01", "user2", "login"),
("2023-05-01", "user1", "purchase"),
("2023-05-02", "user1", "login"),
("2023-05-02", "user3", "login")
]
for date, user, action in entries:
nested_data[date][user].append(action)
print("Activity log:")
for date, users in nested_data.items():
print(f"Date: {date}")
for user, actions in users.items():
print(f" {user}: {actions}")
Counter: Frequency Analysis
The Counter class makes counting elements even easier:
from collections import Counter
# Count word frequencies
text = "the quick brown fox jumps over the lazy dog the fox"
word_counter = Counter(text.split())
print(f"Word frequencies: {word_counter}")
# Most common words
print(f"Most common words: {word_counter.most_common(2)}")
# Count character frequencies
char_counter = Counter(text.replace(" ", ""))
print(f"Character frequencies: {char_counter}")
# Updating counters
more_text = "the fox and the hound"
word_counter.update(more_text.split())
print(f"Updated word frequencies: {word_counter}")
# Arithmetic with counters
inventory1 = Counter(apples=5, oranges=3, bananas=2)
inventory2 = Counter(apples=2, oranges=4, pears=1)
# Combined inventory
combined = inventory1 + inventory2
print(f"Combined inventory: {combined}")
# Subtraction (only positive counts remain)
remaining = inventory1 - inventory2 # Only keeps positive counts
print(f"Remaining after subtraction: {remaining}") # No oranges because 3-4 < 0
Using dictionaries for memoization
Dictionaries can cache function results to improve performance:
# Recursive Fibonacci without memoization (slow)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# With memoization using a dictionary
def fibonacci_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
result = n
else:
result = fibonacci_memo(n-1, memo) + fibonacci_memo(n-2, memo)
memo[n] = result
return result
# Compare performance for larger values of n
import time
n = 30 # Without memoization, this would be very slow
start = time.time()
result1 = fibonacci_memo(n)
end = time.time()
memo_time = end - start
print(f"Fibonacci({n}) = {result1}")
print(f"Time with memoization: {memo_time:.6f} seconds")
# Try without memoization for a smaller value
n_small = 20
start = time.time()
result2 = fibonacci(n_small)
end = time.time()
regular_time = end - start
print(f"Fibonacci({n_small}) = {result2}")
print(f"Time without memoization: {regular_time:.6f} seconds")
Practical Application: Caching API Responses
Dictionaries make excellent caches for API or database calls:
import time
# Simulated expensive API call
def fetch_user_data(user_id):
print(f"Fetching data for user {user_id} from API...")
time.sleep(1) # Simulate network delay
return {
"id": user_id,
"name": f"User {user_id}",
"email": f"user{user_id}@example.com"
}
# Cache implementation using dictionary
class APICache:
def __init__(self, expiry_seconds=60):
self.cache = {}
self.expiry = expiry_seconds
def get(self, key):
if key in self.cache:
timestamp, value = self.cache[key]
# Check if cache entry has expired
if time.time() - timestamp < self.expiry:
print(f"Cache hit for {key}")
return value
else:
print(f"Cache expired for {key}")
del self.cache[key]
print(f"Cache miss for {key}")
return None
def set(self, key, value):
self.cache[key] = (time.time(), value)
print(f"Cached value for {key}")
def clear(self):
self.cache.clear()
print("Cache cleared")
# Using the cache with API calls
cache = APICache(expiry_seconds=5)
def get_user(user_id):
# Try to get from cache first
cached_data = cache.get(f"user:{user_id}")
if cached_data:
return cached_data
# Fetch from API if not in cache
data = fetch_user_data(user_id)
cache.set(f"user:{user_id}", data)
return data
# First call - should fetch from API
user1 = get_user(101)
print(f"Got user: {user1['name']}")
# Second call - should use cache
user1_again = get_user(101)
print(f"Got user again: {user1_again['name']}")
# Different user - should fetch from API
user2 = get_user(102)
print(f"Got user: {user2['name']}")
print("\nWaiting for cache to expire...")
time.sleep(6) # Wait for cache to expire
# After expiry - should fetch from API again
user1_expired = get_user(101)
print(f"Got user after expiry: {user1_expired['name']}")
Performance Considerations
Dictionaries in Python are implemented as hash tables, providing excellent performance characteristics:
- Lookup time: O(1) average case - constant time regardless of dictionary size
- Insertion time: O(1) average case
- Deletion time: O(1) average case
- Space complexity: O(n) - proportional to the number of items
import time
import random
# Generate test data: dictionaries of various sizes
sizes = [1000, 10000, 100000, 1000000]
test_dicts = {}
for size in sizes:
# Create dictionary with numeric keys
test_dicts[size] = {i: f"value_{i}" for i in range(size)}
# Test lookup performance
print("Lookup performance test:")
for size, dictionary in test_dicts.items():
# Generate random keys to look up (existing keys)
keys_to_lookup = [random.randint(0, size-1) for _ in range(1000)]
start = time.time()
for key in keys_to_lookup:
value = dictionary[key]
end = time.time()
print(f"Dictionary size {size}: {end - start:.6f} seconds for 1000 lookups")
# Comparing to list lookup (which is O(n))
print("\nComparison with list lookup:")
size = 10000
test_dict = {i: f"value_{i}" for i in range(size)}
test_list = [(i, f"value_{i}") for i in range(size)]
# Random key to look up
key = random.randint(0, size-1)
# Dictionary lookup
start = time.time()
value = test_dict[key]
dict_time = time.time() - start
# List lookup (linear search)
start = time.time()
for k, v in test_list:
if k == key:
value = v
break
list_time = time.time() - start
print(f"Dictionary lookup: {dict_time:.9f} seconds")
print(f"List lookup: {list_time:.9f} seconds")
print(f"Dictionary is {list_time / dict_time:.1f}x faster")
Performance Optimization Tips
- Use dictionary comprehensions instead of loops for creating dictionaries when possible
- Prefer
inoperator overkeys()for membership checking (if key in dictvsif key in dict.keys()) - Use
get()with default instead of explicit key checking when retrieving values - Consider
defaultdictfor collections that require default values - Use
collections.ChainMapfor multiple dictionary lookups without merging - For very large dictionaries with numeric keys, consider NumPy arrays or specialized data structures
Practice Exercises
Exercise 1: Dictionary Manipulation
Create functions that perform common dictionary operations:
- Write a function that filters a dictionary, keeping only key-value pairs where the value meets a given condition.
- Write a function that flattens a nested dictionary into a single-level dictionary with concatenated keys.
Solution
# Exercise 1: Dictionary filtering
def filter_dict(input_dict, condition_func):
"""
Filter a dictionary based on a condition function applied to values.
Args:
input_dict: The dictionary to filter
condition_func: A function that takes a value and returns True/False
Returns:
A new dictionary with only the entries where condition_func returns True
"""
return {key: value for key, value in input_dict.items() if condition_func(value)}
# Test filtering
data = {
"a": 10,
"b": 25,
"c": 13,
"d": 42,
"e": 8
}
# Filter for values > 15
result1 = filter_dict(data, lambda x: x > 15)
print(f"Values > 15: {result1}")
# Filter for even values
result2 = filter_dict(data, lambda x: x % 2 == 0)
print(f"Even values: {result2}")
# Filter a dict of dicts
products = {
"P001": {"name": "Laptop", "price": 1299.99, "in_stock": True},
"P002": {"name": "Phone", "price": 799.99, "in_stock": True},
"P003": {"name": "Tablet", "price": 349.99, "in_stock": False},
"P004": {"name": "Headphones", "price": 149.99, "in_stock": True}
}
in_stock_products = filter_dict(products, lambda p: p["in_stock"])
print(f"In-stock products: {in_stock_products.keys()}")
expensive_products = filter_dict(products, lambda p: p["price"] > 500)
print(f"Expensive products: {expensive_products.keys()}")
# Exercise 2: Flattening nested dictionaries
def flatten_dict(nested_dict, prefix="", separator="."):
"""
Flatten a nested dictionary by concatenating the keys at each level.
Args:
nested_dict: The nested dictionary to flatten
prefix: Prefix for keys at the current level
separator: String to join key parts
Returns:
A flat dictionary with concatenated keys
"""
flat_dict = {}
for key, value in nested_dict.items():
# Create the new key with prefix if needed
new_key = f"{prefix}{separator}{key}" if prefix else key
# If value is a dictionary, recursively flatten it
if isinstance(value, dict):
# Merge the flattened sub-dictionary
flat_dict.update(flatten_dict(value, new_key, separator))
else:
# Add the key-value pair to the flat dictionary
flat_dict[new_key] = value
return flat_dict
# Test flattening
config = {
"app": {
"name": "MyApp",
"version": "1.0.0",
"settings": {
"debug": True,
"log_level": "INFO"
}
},
"database": {
"host": "localhost",
"port": 5432,
"credentials": {
"username": "admin",
"password": "secret"
}
}
}
flat_config = flatten_dict(config)
print("\nFlattened config:")
for key, value in flat_config.items():
print(f"{key}: {value}")
# Test with different separator
flat_config_custom = flatten_dict(config, separator="_")
print("\nFlattened config with custom separator:")
for key, value in flat_config_custom.items():
print(f"{key}: {value}")
Exercise 2: Word Frequency Counter
Create a function that counts word frequencies in a text and provides analysis:
- Split the text into words and count the frequency of each word
- Ignore case and strip punctuation
- Filter out common "stop words" like "the", "and", "of"
- Return the top N most frequent words and their counts
Solution
def word_frequency_analysis(text, num_top_words=5, stop_words=None):
"""
Analyze word frequencies in a text.
Args:
text: The input text to analyze
num_top_words: Number of top frequent words to return
stop_words: Set of words to exclude (common words like 'the', 'and', etc.)
Returns:
Dictionary with analysis results
"""
if stop_words is None:
stop_words = {'the', 'and', 'of', 'to', 'in', 'a', 'is', 'that', 'it', 'with',
'for', 'as', 'was', 'on', 'be', 'at', 'by', 'this', 'an', 'are'}
# Normalize text: lowercase and replace punctuation with spaces
import string
for char in string.punctuation:
text = text.replace(char, ' ')
text = text.lower()
# Split into words and count frequencies
words = text.split()
word_counts = {}
for word in words:
if word and word not in stop_words: # Skip empty strings and stop words
word_counts[word] = word_counts.get(word, 0) + 1
# Sort words by frequency
sorted_words = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)
top_words = sorted_words[:num_top_words]
# Calculate statistics
total_words = len(words)
unique_words = len(word_counts)
# Build result dictionary
result = {
'total_words': total_words,
'unique_words': unique_words,
'top_words': dict(top_words),
'stop_words_removed': len([w for w in words if w in stop_words])
}
return result
# Test with example text
sample_text = """
Python is a programming language that lets you work quickly and integrate systems more effectively.
Python is powerful, and fast; plays well with others; runs everywhere; is friendly and easy to learn;
is open source; has a supportive community. These qualities make Python incredibly useful and popular.
"""
analysis = word_frequency_analysis(sample_text)
print(f"Total words: {analysis['total_words']}")
print(f"Unique words (excluding stop words): {analysis['unique_words']}")
print(f"Stop words remove