Condition handling is an essential part of software development that helps our programs make decisions and respond to different situations. Whether it’s processing user input, handling errors, or managing program flow, the way we structure our conditional logic can dramatically impact code readability, maintainability, and performance.

If you’ve programmed in languages like Java, C++, or JavaScript, you’ve probably used switch-case statements to handle multiple conditions in your code. They’re a great way to make your code cleaner when you need to check a value against several options. But if you are just starting with Python, you might notice something different, depending on what version you end up using. 

If you’re working with Python 3.10 or later, you have access to a powerful pattern matching system that goes beyond traditional switch-case functionality. However, if you’re using an older version of Python, don’t worry! There are several elegant alternatives that can be just as effective as the new match statement and, in some cases, even more flexible than traditional switch-case statements. 

You can think of the available options like having different tools to solve the same problem. Just as you might use a hammer, screwdriver, or wrench depending on the task, Python provides various approaches for handling multiple conditions effectively.

In this article we will explore these approaches through a practical example. Let’s imagine that you’re building a menu system for an application where users can choose different options: create a new file, open an existing one, save changes, or exit the program.


Table of Contents

The Modern Approach With Match

The Traditional if-elif Approach

The Dictionary Mapping Approach

Comparison of Different Conditional Handling Approaches

Choosing the Right Approach

Comparing Python’s Approach With Other Languages


The Modern Approach With Match

Python’s match statement, introduced in 2021 with version 3.10, brings pattern matching to the language in a way that should feel familiar to developers coming from other languages. It’s inspired by switch-case statements but offers much more powerful capabilities for handling complex data patterns.

Let’s start with a basic example that handles our menu choices:

def process_menu_choice(choice):
    match choice:
        # Case for creating a new file
        case “1”:
            print(“Creating new file…”)
            return “create”
       
        # Case for opening an existing file
        case “2”:
            print(“Opening file…”)
            return “open”
       
        # Case for saving changes to a file
        case “3”:
            print(“Saving changes…”)
            return “save”
       
        # Case for exiting the program
        case “4”:
            print(“Exiting program…”)
            return “exit”
       
        # Default case for invalid menu options
        case _:  # The underscore catches any unmatched values
            print(“Invalid choice!”)
            return “invalid”

# Prompt the user for a menu choice
user_choice = input(“Choose an option (1-4): “)  
# Process the user’s choice and store the result
result = process_menu_choice(user_choice)

While this implementation is effective, the match statement offers extended capabilities. Beyond simple value matching, it can also handle structured data and process more complex commands with additional parameters:

def process_command(command):
    # Split the command into words and match against patterns
    match command.split():        # Matches “new document.txt”
        case [“new”, filename]:
            print(f”Creating new file: {filename}”)
            return “create”
        # Matches “open report.pdf”
        case [“open”, filename]:
            print(f”Opening file: {filename}”)
            return “open”
        # Matches “save doc.txt as pdf”
        case [“save”, filename, “as”, format]:
            print(f”Saving {filename} as {format}”)
            return “save”

        # Matches either “exit” or “quit”        case [“exit” | “quit”]:
            print(“Exiting program…”)
            return “exit”
        # Default case for unrecognized commands
        case _:
            print(“Invalid command!”)
            return “invalid”

# Using the function
user_command = input(“Enter command (e.g., ‘new document.txt’): “)
result = process_command(user_command)

The match statement can also handle complex data structures like dictionaries and classes, making it perfect for processing structured data:

def handle_application_event(event):
    match event:
        # Handle the “user_joined” event type
        case {“type”: “user_joined”, “name”: name}:
            return f”Welcome, {name}!”
       
        # Handle the “error” event type, providing code and message
        case {“type”: “error”, “code”: code, “message”: msg}:
            return f”Error {code}: {msg}”
       
        # Handle a “file_upload” event with size less than 1MB
        case {“type”: “file_upload”, “name”: name, “size”: size} if size < 1000000:
            return f”Processing file: {name}”
       
        # Handle a “file_upload” event with size greater than or equal to 1MB
        case {“type”: “file_upload”, “name”: name}:
            return f”File too large: {name}”
       
        # Default case for unrecognized event types
        case _:
            return “Unknown event type.”

# Example events to simulate various scenarios
events = [    # User joining the application
    {“type”: “user_joined”, “name”: “Alice”},
    # Error event with details
    {“type”: “error”, “code”: 404, “message”: “Not Found”},
    # File upload below size limit
    {“type”: “file_upload”, “name”: “photo.jpg”, “size”: 500000},
    # File upload above size limit    {“type”: “file_upload”, “name”: “video.mp4”, “size”: 1500000},
    # An unknown event type
    {“type”: “unknown_event”, “detail”: “Some data”}
]

# Loop through each event in the list, process it, and print the result
for event in events:
    # Print the output of the handler for each event
    print(handle_application_event(event))

The Traditional if-elif Approach

Before Python 3.10, the most straightforward way to handle multiple conditions was using if-elif-else statements. This approach still has its place today, especially when working with legacy code or when you need maximum compatibility across different Python versions. This approach is like a trustworthy old tool that you know will get the job done reliably.

Let’s implement the menu system using this approach:

def process_menu_choice(choice):
    # Check if the user selected option 1
    if choice == “1”:
        print(“Creating new file…”)
        return “create” 
   
    # Check if the user selected option 2
    elif choice == “2”:
        print(“Opening file…”)
        return “open” 
   
    # Check if the user selected option 3
    elif choice == “3”:
        print(“Saving changes…”)
        return “save” 
   
    # Check if the user selected option 4
    elif choice == “4”:
        print(“Exiting program…”)
        return “exit” 
   
    # Handle any invalid menu choices
    else:
        print(“Invalid choice!”)
        return “invalid”

# Simulate different user inputs to demonstrate the function
print(process_menu_choice(“1”))  # Simulates new file creation
print(process_menu_choice(“3”))  # Simulates saving changes
print(process_menu_choice(“5”))  # Simulates an invalid choice

This method is perfectly valid and easy to understand but it can become cumbersome as your conditions grow. Imagine maintaining a menu with 20 or more different options! You’d end up with a long chain of elif statements that could be difficult to read and maintain. However, for simple cases or when the logic within each condition is unique and complex, this approach is definitely a practical choice.

The Dictionary Mapping Approach

The dictionary mapping approach is an elegant solution that could be particularly useful if you need to modify your program’s behavior dynamically. This technique makes use of Python’s dictionary data structure to create a mapping between choices and their corresponding actions.

With this approach, we could implement our menu system like this:

def create_file():
    # Simulates creating a new file
    print(“Creating new file…”)
    return “create”

def open_file():
    # Simulates opening an existing file
    print(“Opening file…”)
    return “open”

def save_file():
    # Simulates saving changes to a file
    print(“Saving changes…”)
    return “save”

def exit_program():
    # Simulates exiting the program
    print(“Exiting program…”)
    return “exit”

# Map menu options (strings) to their corresponding functions
menu_options = {
    “1”: create_file,  # Option 1 -> Create a new file
    “2”: open_file,    # Option 2 -> Open a file
    “3”: save_file,    # Option 3 -> Save changes
    “4”: exit_program  # Option 4 -> Exit the program
}

def process_menu_choice(choice):
    # Fetch the corresponding function from the menu_options dictionary
    # If the choice is invalid, return a lambda that outputs “Invalid choice!”
    action = menu_options.get(choice, lambda: print(“Invalid choice!”))
    return action()

# Prompt the user for input and process their menu choice
user_choice = input(“Choose an option (1-4): “)
result = process_menu_choice(user_choice)

The real power of this approach becomes apparent when you need to add or remove options dynamically. This is something that can’t be done with the match statement!

# Adding a new command at runtime
def save_as_pdf():
    print(“Saving as PDF…”)
    return “pdf”

menu_options[“5”] = save_as_pdf  # Dynamically add new functionalitydel menu_options[“4”]  # Remove the exit option

Comparison of Different Conditional Handling Approaches in Python

It’s important to understand that there’s rarely a “one-size-fits-all” solution in programming. Each method has its strengths and ideal use cases, similar to how different tools in a toolbox serve different purposes. The choice between all these options depends on your Python version, the complexity of your conditions, and whether you need dynamic behavior in your code.

Table 1: Comparison of Python’s Conditional Handling Approaches

ApproachPerformanceBest Use CasesLimitationsPython Version
Match StatementVery good for complex patterns• Structured data processing
• Complex pattern matching
• Data destructuring
Only available in newer Python3.10+
Dictionary MappingExcellent for lookups• Dynamic dispatch
• Plugin systems
• Runtime modifications
Memory overhead for large mappingsAll versions
if-elif-elseGood for simple conditions• Simple logic flows
• Small number of conditions
• Maximum compatibility
Can become unmanageable with many conditionsAll versions

Choosing the Right Approach

The match statement, being Python’s newest addition, combines the best of both worlds: the readability of if-elif-else and the pattern-matching power that goes beyond what traditional switch-case statements offer in other languages.

It’s particularly effective when you’re dealing with structured data or complex patterns. For instance, when processing API responses or command-line arguments, being able to destructure data makes your code very readable and powerful:

def process_users(users):
    # Simulate processing a list of users
    return f”Processing {len(users)} users”

def process_api_response(response):
    match response:
        # Case for a successful response with a non-empty list of users
        case {“status”: “success”, “data”: {“users”: users}} if len(users) > 0:
            return process_users(users)

        # Case for a successful response but no users found
        case {“status”: “success”, “data”: {“users”: []}}:
            return “No users found”

        # Case for an error response with a message
        case {“status”: “error”, “message”: msg}:
            return f”Error: {msg}”

        # Default case for invalid or unrecognized response formats
        case _:
            return “Invalid response format”

# Example API responses
responses = [
    # Success with users
    {“status”: “success”, “data”: {“users”: [“Alice”, “Bob”]}},

    # Success with no users
    {“status”: “success”, “data”: {“users”: []}},

    # Error response
    {“status”: “error”, “message”: “Invalid API key”},

    # Invalid response
    {“status”: “unknown”}
]

# Process and print the result for each example response
for response in responses:
    print(process_api_response(response))

However, match isn’t always the best choice. The dictionary approach has a unique advantage when you need dynamic behavior. Imagine you are building a plugin system that needs to allow features to be added or removed while the program runs. Dictionary mapping would be just perfect for this:

# Dictionary to store plugin commands and their corresponding functions
plugin_commands = {}

def register_plugin(command, function):
    # Add the command-function pair to the dictionary
    plugin_commands[command] = function

def unregister_plugin(command):
    # Remove the command from the dictionary if it exists
    plugin_commands.pop(command, None)

# Example plugin functions
def compress_file():
    return “Compressing file…”

def encrypt_file():
    return “Encrypting file…”

# Register plugins at runtime
register_plugin(“compress”, lambda: compress_file())
register_plugin(“encrypt”, lambda: encrypt_file())

# Example usage
print(plugin_commands[“compress”]())
print(plugin_commands[“encrypt”]())

# Unregister the ‘compress’ plugin
unregister_plugin(“compress”)

# Check if ‘compress’ is still registered
print(“compress” in plugin_commands)  # Should print False

Lastly, the traditional if-elif-else approach remains valuable in some specific scenarios. For example, if you’re writing simple scripts where clarity is more important than flexibility, this approach would be perfect. It would also be perfect when you need to support older Python versions and want to maintain compatibility without adding complexity.

# Function to check permissions based on user role
def check_permission(user_role):
    # Admins have full access
    if user_role == “admin”:
        return “full access”

    # Editors have write access
    elif user_role == “editor”:
        return “write access”

    # Viewers have read access
    elif user_role == “viewer”:
        return “read access”

    # All other roles have no access
    else:
        return “no access”

# Test the function with different roles
roles = [“admin”, “editor”, “viewer”, “guest”]

# Process and print the permissions for each role
for role in roles:
    print(f”Role: {role}, Permission: {check_permission(role)}”)

Comparing Python’s Approach With Other Languages

The benefit of having Python’s match statement arriving later in the language’s evolution is that its designers could learn from other languages’ implementations. This allowed them to create something more powerful and flexible than traditional switch-case statements. 

In Java or JavaScript, switch-case statements are fairly basic. They can only match against single values, and you need to remember to add break statements after each case to prevent fall-through behavior (where the code continues executing into the next case):

// JavaScript switch
// Simulate a user’s choice
const choice = “1”; // Change this value to test different cases

// Use a switch statement to handle different choices
switch (choice) {
    case “1”:
        // Case for creating a file
        console.log(“Creating file”);
        break; // Exit the switch after executing this case

    case “2”:
        // Case for opening a file
        console.log(“Opening file”);
        break; // Exit the switch after executing this case

    default:
        // Default case for invalid input
        console.log(“Invalid choice”);
        // No break needed here as it’s the last case
}

// Output will depend on the value of ‘choice’

In C++, just like in JavaScript, switch-case statements are limited to matching single values. You must also include break statements to manage the flow of execution and avoid fall-through behavior. Complex scenarios, such as matching multiple values or including conditions, often require verbose if-else chains embedded within a switch:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string command = “exit”; // Example command

    // Switch on the first character of the command
    switch (command[0]) {
        case ‘c’: // Commands starting with ‘c’
            cout << (command == “create” ? “Creating file” : “Unrecognized command”) << endl;
            break;

        case ‘o’: // Commands starting with ‘o’
            cout << (command == “open” ? “Opening file” : “Unrecognized command”) << endl;
            break;

        case ‘e’: // Commands starting with ‘e’
        case ‘q’: // Commands starting with ‘q’
        case ‘b’: // Commands starting with ‘b’
            if (command == “exit” || command == “quit” || command == “bye”) {
                cout << “Exiting program” << endl;
            } else {
                cout << “Unrecognized command” << endl;
            }
            break;

        default: // For all other cases
            cout << “Unrecognized command” << endl;
    }

    return 0;
}

On the other hand, Python’s match statement eliminates the need for break statements and adds powerful pattern matching capabilities that just a few other languages offer. It can match against multiple values, destructure objects and arrays, and even include guard conditions:

# Example command to test the match statement
command = [“copy”, “file1.txt”, “to”, “file2.txt”]  # Change this to test different commands

# Match statement to handle various patterns
match command:
    # Match with a condition (guard clause)
    case [“create”, filename] if filename.endswith(“.txt”):
        print(f”Creating text file: {filename}”)

    # Match with destructuring (pattern matching for structure)
    case [“copy”, source, “to”, destination]:
        print(f”Copying file from {source} to {destination}”)

    # Match with multiple patterns using |
    case [“exit” | “quit” | “bye”]:
        print(“Exiting the program…”)

    # Match with variable capture and unpacking
    case [“process”, *arguments]:
        print(f”Processing arguments: {‘, ‘.join(arguments)}”)

    # Default case for unrecognized commands
    case _:
        print(“Unrecognized command”)

The Bottom Line

If you’re using Python 3.10 or later, the match statement will likely become your favorite tool for handling multiple conditions. The fact that it combines elegant syntax with powerful pattern matching capabilities makes it perfect for handling everything from simple value checks to complex data structures. 

However, Python is extremely flexible and that means you have a few other great options at your disposal. The dictionary approach is perfect for those situations in which you need to modify behavior while your program runs. On the other hand, the trustworthy if-elif-else statements remain invaluable for simple scripts or when you need your code to work across different Python versions. 

Each approach has its sweet spot, and the best choice depends on your specific project needs, whether you’re building a complex application, a flexible plugin system, or a simple utility script.

If you’re interested in learning more about Python programming, be sure to check out our highly reviewed Introduction To Programming Nanodegree program, or our AI Programming with Python Nanodegree program

Alan Sánchez Pérez Peña
Alan Sánchez Pérez Peña
Alan is a seasoned developer and a Digital Marketing expert, with over a decade of software development experience. He has executed over 70,000+ project reviews at Udacity, and his contributions to course and project development have significantly enhanced the learning platform. Additionally, he provides strategic web consulting services, leveraging his front-end expertise with HTML, CSS, and JavaScript, alongside his Python skills to assist individuals and small businesses in optimizing their digital strategies. Connect with him on LinkedIn here: http://www.linkedin.com/in/alan247