In API testing and configuration validation, we often need to verify that a dictionary contains all keys and matching values from another dictionary, while ignoring extra keys. This "subset" check is essential for partial matching scenarios.
Using all() for Flat Dictionaries
The most Pythonic approach for simple dictionaries uses all() with a generator expression:
target = {"color": "red", "size": "L", "price": 100, "stock": 50}
query = {"color": "red", "size": "L"}
is_subset = all(item in target.items() for item in query.items())
print(is_subset) # TrueAs a Reusable Function
def is_subset(subset: dict, superset: dict) -> bool:
"""Check if all key-value pairs in subset exist in superset."""
return all(item in superset.items() for item in subset.items())
config = {"debug": True, "port": 8080, "host": "localhost"}
print(is_subset({"debug": True}, config)) # True
print(is_subset({"port": 8080, "host": "localhost"}, config)) # True
print(is_subset({"port": 3000}, config)) # False (wrong value)
print(is_subset({"missing": True}, config)) # False (missing key)Using Set Comparison
When dictionary values are hashable (strings, numbers, tuples), you can use set operators:
d1 = {"a": 1, "b": 2}
d2 = {"a": 1, "b": 2, "c": 3}
print(d1.items() <= d2.items()) # True
print(d2.items() >= d1.items()) # TrueRecursive Check for Nested Dictionaries
For nested structures like JSON responses, the flat approach fails because it doesn't look into nested dictionaries. Use recursion:
def is_subset_deep(subset: dict, superset: dict) -> bool:
"""Recursively check if subset is contained within superset."""
for key, value in subset.items():
if key not in superset:
return False
superset_value = superset[key]
if isinstance(value, dict) and isinstance(superset_value, dict):
if not is_subset_deep(value, superset_value):
return False
elif superset_value != value:
return False
return True
api_response = {
"user": {
"id": 1,
"profile": {
"name": "Alice",
"role": "admin",
"settings": {"theme": "dark", "notifications": True}
}
},
"timestamp": "2024-01-15"
}
query = {
"user": {
"profile": {
"role": "admin",
"settings": {"theme": "dark"}
}
}
}
print(is_subset_deep(query, api_response)) # TrueHandling Lists in Nested Structures
Extend the recursive function to handle lists:
def is_subset_deep_extended(subset, superset) -> bool:
"""Deep subset check supporting dicts and lists."""
if isinstance(subset, dict) and isinstance(superset, dict):
for key, value in subset.items():
if key not in superset:
return False
if not is_subset_deep_extended(value, superset[key]):
return False
return True
elif isinstance(subset, list) and isinstance(superset, list):
for item in subset:
found = any(
is_subset_deep_extended(item, sup_item)
if isinstance(item, (dict, list)) else item == sup_item
for sup_item in superset
)
if not found:
return False
return True
else:
return subset == superset
data = {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
],
"count": 2
}
query = {
"users": [{"name": "Alice"}]
}
print(is_subset_deep_extended(query, data)) # TrueFinding Missing or Mismatched Keys
Get details about what's missing or different:
def compare_dicts(subset: dict, superset: dict) -> dict:
"""Compare dicts and return details about differences."""
missing_keys = []
mismatched_values = []
for key, value in subset.items():
if key not in superset:
missing_keys.append(key)
elif superset[key] != value:
mismatched_values.append({
"key": key,
"expected": value,
"actual": superset[key]
})
return {
"is_subset": len(missing_keys) == 0 and len(mismatched_values) == 0,
"missing_keys": missing_keys,
"mismatched_values": mismatched_values
}
expected = {"status": "success", "code": 200, "data": "test"}
actual = {"status": "error", "code": 500}
result = compare_dicts(expected, actual)
print(result)Output:
{
'is_subset': False,
'missing_keys': ['data'],
'mismatched_values': [
{'key': 'status', 'expected': 'success', 'actual': 'error'},
{'key': 'code', 'expected': 200, 'actual': 500}
]
}Practical Example: API Response Validation
def validate_api_response(response: dict, required_fields: dict) -> tuple[bool, list]:
"""Validate that API response contains required fields and values."""
errors = []
def check(subset, superset, path=""):
for key, expected in subset.items():
current_path = f"{path}.{key}" if path else key
if key not in superset:
errors.append(f"Missing field: {current_path}")
continue
actual = superset[key]
if isinstance(expected, dict) and isinstance(actual, dict):
check(expected, actual, current_path)
elif isinstance(expected, type):
if not isinstance(actual, expected):
errors.append(
f"Type mismatch at {current_path}: "
f"expected {expected.__name__}, got {type(actual).__name__}"
)
elif actual != expected:
errors.append(
f"Value mismatch at {current_path}: "
f"expected {expected!r}, got {actual!r}"
)
check(required_fields, response)
return len(errors) == 0, errors
response = {"status": "ok", "data": {"id": 123, "name": "Test"}}
required = {"status": "ok", "data": {"id": int}} # int means "any integer"
valid, errors = validate_api_response(response, required)
print(f"Valid: {valid}, Errors: {errors}") # Output: Valid: True, Errors: []Method Comparison
| Scenario | Method | Notes |
|---|---|---|
| Flat dictionaries | all(item in target.items() ...) |
Simple and readable |
| Hashable values only | d1.items() <= d2.items() |
Fastest, but limited |
| Nested dictionaries | Recursive function | Handles arbitrary depth |
| With lists | Extended recursive | Full JSON support |
Summary
- Use
all()withitems()for simple flat dictionary subset checks. - Use set operators (
<=) when values are guaranteed hashable. - Use recursive functions for nested JSON-like structures.
- Consider returning details about mismatches for debugging and testing.