Python Match Case Mapping Pattern: Dictionary Matching
python match case mapping pattern: Learn how to use Python's mapping pattern in match-case to match dictionaries by structure, extract values, and dispatch on complex...
Python's match statement, introduced in 3.10, includes a mapping pattern that lets you match dictionaries by their structure and extract values in a single step. This pattern is a direct alternative to manual key checks and dictionary dispatch, but it has its own semantics around missing keys, extra keys, and capture behavior. Understanding the python match case mapping pattern helps you write more declarative dispatch logic when your input is a dictionary with a known shape.
The Mapping Pattern Syntax
A mapping pattern looks like a dictionary literal with keys and patterns for values. The match statement evaluates the subject against each case in order, and the first case whose pattern matches is executed.
def handle_command(command): match command: case {"action": "start", "target": target}: print(f"Starting {target}") case {"action": "stop", "target": target}: print(f"Stopping {target}") case _: print("Unknown command")
Here, command is a dictionary. The pattern {"action": "start", "target": target} requires that the dictionary has both keys "action" and "target", and that the value for "action" equals the string "start". If those conditions hold, the value for "target" is bound to the variable target. Extra keys in the dictionary do not prevent a match, which is a key difference from literal equality.
Matching Dictionaries with Literal Keys
The simplest mapping pattern uses only literal values. This is useful when you want to check for the presence of a key and a specific value without extracting anything.
match config: case {"debug": True}: print("Debug mode is on") case {"debug": False}: print("Debug mode is off") case _: print("No debug key")
The pattern {"debug": True} matches any dictionary that has a "debug" key with the value True, regardless of other keys. If the dictionary lacks the "debug" key, the pattern fails and the next case is tried. This behavior is different from a simple if config.get("debug") is True because the pattern also enforces that the key exists.
Capturing Values and the Whole Dictionary
To extract a value from a matched dictionary, bind it to a variable as shown earlier. You can also capture the entire dictionary with as if you need to reference it later.
match payload: case {"type": "login", "user": user} as full_payload: print(f"Login for {user}, full payload: {full_payload}")
If you want to capture any remaining keys that were not explicitly listed, use **rest. This behaves like **kwargs in function definitions.
match event: case {"type": "click", **rest}: print(f"Click event with extra data: {rest}")
The rest variable will be a dictionary containing all keys that were not matched by the explicit patterns. This is useful when you need to forward unknown fields to another function.
Using Mapping Patterns for Dispatch
A common use case for mapping patterns is replacing long if-elif chains that inspect dictionary keys. Consider a message handler that processes different message types.
def process_message(msg): match msg: case {"type": "text", "content": content}: return f"Text: {content}" case {"type": "image", "url": url, "alt": alt}: return f"Image: {alt} ({url})" case {"type": "file", "filename": name, "size": size}: return f"File: {name} ({size} bytes)" case _: return "Unknown message type"
This is more readable than a series of if "type" in msg and msg["type"] == "text": checks. It also naturally handles missing keys: if a message has "type": "text" but no "content" key, the first case fails and the fallback runs.
Combining Mapping Patterns with Guards
Guards add an extra condition to a pattern. They are evaluated after the pattern matches, and the case is only selected if the guard is true.
match request: case {"method": "GET", "path": path} if path.startswith("/api/"): print(f"API GET request for {path}") case {"method": "GET", "path": path}: print(f"Non-API GET request for {path}")
Guards are useful when the pattern alone cannot express the constraint, such as comparing a captured value to a threshold or checking a property of the bound variable.
Performance and Maintainability Considerations
Mapping patterns are evaluated top-down. For each case, Python checks the keys and values in the order they appear in the pattern. This means that if you have many cases, the time to find a match grows linearly with the number of cases. In contrast, a dictionary dispatch table uses a hash lookup and is O(1) on average.
| Approach | Time complexity | Readability | Flexibility |
|---|---|---|---|
| Mapping pattern | O(n) cases | High | Guards, nesting, captures |
| Dictionary lookup | O(1) | Medium | Requires precomputed functions |
If you are dispatching on a single key like "type" and the values are simple strings, a dictionary mapping from type to handler is often faster and simpler. Use mapping patterns when the structure of the dictionary varies beyond a single discriminator, or when you need to extract multiple values in one step.
Another consideration is that mapping patterns create new pattern objects at runtime. For a small number of cases this overhead is negligible, but in a hot loop with thousands of dispatches per second, the cost of sequential pattern matching may become measurable. Profile your code if dispatch performance is critical.
Common Pitfalls and Edge Cases
Mapping patterns require that all specified keys exist in the subject dictionary. If a key is missing, the pattern fails. To match a dictionary that may or may not have a key, you need to use a nested pattern with | (or) or a guard.
match data: case {"name": name, "age": age} if "age" in data: print(f"{name} is {age} years old") case {"name": name}: print(f"{name} has no age")
The first case uses a guard to ensure "age" exists, because the pattern itself would fail if the key were absent. Alternatively, you can use a pattern like {"name": name, "age": age} | {"name": name} but that would bind age only in the first alternative.
Extra keys are allowed by default. If you need to enforce an exact set of keys, you can use **rest and check that rest is empty, but that adds complexity. In most real-world scenarios, allowing extra keys is desirable because it makes the pattern robust to future additions.
Finally, mapping patterns work with any hashable keys, not just strings. You can match on integer keys, tuples, or even None. The keys in the pattern must be literals, not variables, unless you use a guard to compare against a variable.