Skip to content

How to Check if a Dictionary is a Subset of Another Dictionary in Python

Learn how to verify if one Python dictionary is a subset of another while ignoring extra keys, with methods for flat, nested, and hashable-value dictionaries.

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)  # True

As 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())  # True

Recursive 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))  # True

Handling 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))  # True

Finding 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() with items() 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.
Navigation