Pattern Matching Lists and Dictionaries in Python

Explore Python's Structural Pattern Matching: Master Lists, Tuples, and Dictionaries for Efficient Coding. Unveil the power of Python's advanced features for streamlined data processing and code readability. Join us at The Turing Taco Tales for in-depth insights and expert coding tips.

Pattern Matching Lists and Dictionaries in Python
Navigate the seas of code with Python's structural pattern matching.

Overview

In previous articles, we've introduced Python's pattern matching. In "Python's Power Play: Structural Pattern Matching," we discussed the fundamentals of using pattern matching in Python. We followed up with "Putting the Structural in Structural Pattern Matching," where we discussed how to match not only literal values but also the structure of complex types.

And in "Python's Path to Embracing Algebraic Data Types", we examined how adding pattern matching to Python 3.10 made using Algebraic Data Types practical and almost conversational.

Building on these foundations, we now focus on how pattern matching can be applied to two of Python's most commonly used data structures: lists and dictionaries.


Understanding List Pattern Matching in Python

The complete code from which the following snippet was extracted is available on GitHub:

Python 3.12

def process_command(args):
    match args:
        case ["count", (FileType.SOURCE_CODE.value | FileType.HTML.value | FileType.IMAGES.value | FileType.DOCUMENTS.value | FileType.OTHERS.value) as file_type]:
            return count_files(FileType(file_type))
        
        case ["count", _, *_]:
            return "Error: 'count' command requires exactly one file type argument"

        case ["sha1", "verify"] | ["sha1", "verify", _, _, _, *_]:
            return "Error: 'verify-sha1' command requires exactly two arguments (file path and expected hash)"
        
        case ["sha1", "verify", file_path, expected_hash]:
            return verify_sha1(file_path, expected_hash)

        case ["sha1", *file_paths] if file_paths:
            return [calculate_sha1(path) for path in file_paths]        
        
        case ["sha1"]:
            return "Error: 'sha1' command requires at least one file path"

        case ["free-space"]:
            return show_free_space()
        
        case ["free-space", *_]:
            return "Error: 'free-space' command does not require any additional arguments"

        case _:
            return "Unknown command"
        

As demonstrated in the process_command function above, We can pattern-match not only on the type but also the elements within a list. Since the list is an ordered sequence, the position of the pattern corresponds to that in the input list. For instance, in the pattern on line 3, the string "count" must be the first element in the input list. If the first element in the input list is not "count", it won't result in a match even if the input list has the string on a different location.

When we need to match against variable numbers of elements, we can use the star (*) wildcards. In our example, this feature is convenient for commands with multiple arguments. The pattern on line 15, ["sha1", *file_paths], is an excellent example, allowing the elements to be captured after the initial 'sha1' in the variable filepaths.

In our previous article, "Python’s Power Play: A Structural Pattern Matching First Look" we used the underscore as a pattern for the default case. But it is not simply a default; we can use an underscore anywhere to write any pattern or sub-pattern that matches any value in that location. For example, on line 9, we used it to ensure a minimal list size by writing three underscores after the two string literals, providing the pattern won't match any list with less than five elements.

In that same line, we finished with *_ to match any elements, including zero. In line 27, we used the underscore as the last match since it is the only thing in the pattern that will match any value, making it the default case.

In a previous article, we used OR patterns; in this example, in line 3, we show that we can use OR in subpatterns; not only that, but we can capture the value of a subpattern utilizing an alias, we assign aliases using the as keyword.

Aliases increase the power of pattern matching by enabling us to name specific parts of a match, thereby making the subsequent code more readable and functional: (FileType.SOURCE_CODE.value | FileType.HTML.value | FileType.IMAGES.value | FileType.DOCUMENTS.value | FileType.OTHERS.value) as file_type.

Collectively, these features contribute to making the pattern matching in Python more concise and highly expressive, which proves particularly beneficial in scenarios like CLI command parsing, where input formats can be diverse and complex.


Pattern Matching with Tuples in Python

Pattern matching with tuples in Python operates under the same principles as lists, but we'll use parenthesis instead of square brackets.

Besides that, the process has a crucial distinction of the underlying type. When defining a pattern for a tuple, the matching process strictly expects a tuple in the input. This means that a pattern designed for a tuple won't match a list, even if the elements within them are identical in value and order.

Using wildcards in tuple pattern matching captures the matched elements as a smaller tuple. For example, in a pattern like case (a, *b), a matches the first element of the input tuple, while b captures the rest as another tuple. This feature allows for flexible matching of variable elements within a tuple, similar to how it works with lists.

This type-specific nature of pattern matching ensures clear and precise handling of different data structures, making Python's pattern matching both robust and versatile.


Pattern Matching With Dictionaries in Python

Python 3.12

def process_user_data(user_data):
    match user_data:
        case {"name": name, "age": age} as person:
            return f"Processing data for {name}, age {age}"
        case {"username": username, "posts": posts}:
            return f"Processing {len(posts)} posts for user {username}"
        case {"email": email}:
            return f"Sending email to {email}"
        case _:
            return "Unknown data format"

# Example usage
data1 = {"name": "Alice", "age": 30}
data2 = {"username": "user123", "posts": ["Post 1", "Post 2"]}
data3 = {"email": "user@example.com"}

print(process_user_data(data1))
print(process_user_data(data2))
print(process_user_data(data3))
        

As shown in our previous example, dictionary pattern matching makes Python stand out in handling JSON data, making it extremely useful for writing REST APIs. This feature aligns well with the nature of JSON, as it often directly maps to a dictionary format in Python.

Unlike lists or tuples, dictionaries in Python, the sequence in which keys are specified in the pattern is immaterial. This is a consequence of the unordered nature of dictionaries, which simplifies the matching process, allowing us to write the matches, taking only readability and maintainability into consideration.

However, the nature of the underlying structure also brings limitations; specifically, the keys used in the pattern must be literals. Python does not allow variables or expressions as keys in the pattern. This limitation ensures predictability and consistency in the matching process but somewhat restricts flexibility.

Also, unlike lists, unmatched keys don't result in a match failure, nor can we use wildcards to match the rest of the keys. However, we can use an alias to capture the matched section of the dictionary, as shown in line 4.

Accessing the unmatched data requires additional code outside the pattern-matching construct; since we know which keys have been matching, this issue only raises an inconvenience.


Conclusion

In this series, we travel deeper into the versatile world of structural pattern matching in Python, using it to handle lists, tuples, and dictionaries precisely and efficiently. Pattern matching is a big step in Python's evolution into a more expressive language.

This feature streamlines complex data processing, especially with JSON in REST APIs, and opens doors to more intuitive coding practices. Embracing these powerful features will undoubtedly help us write elegant and concise solutions.

If you've found this exploration of Python's pattern matching insightful and wish to stay updated on more such topics, we would like to invite you to subscribe. By subscribing, you'll gain regular access to knowledge that can enrich your programming skills and inform you about the latest advancements in Python and other technologies. Please take advantage of this opportunity to enhance your coding journey with us!


Addendum: A Special Note for Our Readers

I decided to delay the introduction of subscriptions, you can read the full story here.

In the meantime, I decided to accept donations.

If you can afford it, please consider donating:

Every donation helps me offset the running costs of the site and an unexpected tax bill. Any amount is greatly appreciated.

Also, if you are looking to buy some Swag, please visit I invite you to visit the TuringTacoTales Store on Redbubble.

Take a look, maybe you can find something you like: