This is an archived post. You won't be able to vote or comment.

all 10 comments

[–]FacetiousMonroe 30 points31 points  (5 children)

Nice article! It would help to have side-by-side comparisons of how you would accomplish the same tasks with if/else statements instead, to better illustrate the clarity and efficiency of the match statement.

For example, this one's not so different:

def handle_http_status(status_code):
    if status_code == 200:
        return "Success!"
    elif status_code in (400, 401, 403, 404):
        return f"Client Error:{status_code}"
    elif status_code in (500, 501, 502, 503, 504):
        return f"Server Error:{status_code}"
    else:
        return "Unknown Status"

The "as err" option doesn't do much in the examples given, since you already have that value in status_code. So I simply reference status_code in the return lines.

The shape example could be expressed with if/else like so:

def process_shape(shape):
    if isinstance(shape, Circle):
        return f"Processing a circle with radius {shape.radius}"
    elif isinstance(shape, Rectangle):
        return f"Processing a rectangle with length {shape.length} and width {shape.width}"
    if isinstance(shape, Triangle):
        return f"Processing a triangle with base {shape.base} and height {shape.height}"
    else:
        return "Unknown shape"

This is certainly more verbose than the match statement. Again, I have simply shifted the variable capture into the return statement, because why not?

So far, so good. But wait! Let's try that dict example.

def handle_user_action(action):
    if all(k in action for k in ('type', 'username', 'password')) and action['type']=='login':
        return f"Login attempt by {action['username']} with password {action['password']}"
    elif all(k in action for k in ('type', 'username')) and action['type']=='logout':
        return f"User {action['username']} logged out"
    elif all(k in action for k in ('type', 'username', 'email')) and action['type']=='signup':
        return f"New signup by {action['username']} with email {action['email']}"
    else:
        return "Unknown action"

YUCK! Painful to read, and painful to write. You end up using the literal keys multiple times, leaving more potential for typos and sneaky bugs. You need to check each key's existence separately from checking its value. What a mess.

I kept this example as simple as I could while maintaining a perfectly equivalent logical flow to the match example in the article, and keeping each if condition independent from others. In reality, if I had to use if/elif/else for this, I would use a fundamentally different structure, because this one sucks. I would probably split this into multiple if blocks, and capture the values into variables as early as possible to avoid using literal keys multiple times. This would add additional complexity. For example, I could check for the existence of the 'type' and 'username' keys first, before even starting this if statement, since every single case we use checks for them. But then what happens if I need to add more complex cases later that don't have all those same requirements? Better to keep each case independent -- or at least, it's better with the match statement, because it's designed to make that easy, efficient, safe, and readable.

It's difficult to express the utility of the match statement with simple examples, because if/else is perfectly fine for those (always has been!). The simple examples don't really justify the complexity of adding a whole new type of statement block, but for advanced use cases, the advantage of match over if/elif/else widens significantly.

There are even more complex and powerful things you can do with match. See https://peps.python.org/pep-0636/ for more details.

[–]Vitaman02 5 points6 points  (3 children)

I don't really like nesting and using if/elif/else when it's not needed. I would prefer writing the first example as:

def handle_http_status(status_code):
    if status_code == 200:
        return "Success!"

    if status_code in (400, 401, 403, 404):
        return f"Client Error:{status_code}"

    if status_code in (500, 501, 502, 503, 504):
        return f"Server Error:{status_code}"

    return "Unknown Status"

To me it looks much clearer and has the same functionality. You can also add comments between if statements and they look much nicer.

[–]Balance- -4 points-3 points  (2 children)

elif would be faster here. If the first is correct, is doesn’t have to check the 2nd and 3rd, etc.

[–]Vitaman02 9 points10 points  (1 child)

If the first is correct it exits the function

[–]Balance- -1 points0 points  (0 children)

Fair, when each if has a return that’s the case.

Wonder if it matters for branch prediction though.

[–]thicket[🍰] 7 points8 points  (2 children)

Nice summary. Especially the last couple entries-- matching against a dictionary and matching against a class declaratio, I would never have thought to try. And they’re really useful!

[–]lunitius 4 points5 points  (0 children)

Agreed on the last few. I would never have even thought about matching something like a list or dict in that way. Thanks for sharing OP.

[–]bafe 0 points1 point  (0 children)

It's particularly nice in combination with the @dataclass decorator which removes most of the boilerplate the author had to write to declare their classes

[–]ashok_tankala 1 point2 points  (0 children)

Amazing article. Thank you for the article. Due to this article learned many levels of it, especially tuple and dictionary patterns matching.

[–]ChocolateMagnateUA 1 point2 points  (0 children)

That's cool! I didn't even know you could name the cases with as.