switch, dict, if/elif or match?

Python has three ordinary ways to dispatch on a value, and each one is right for something. This page sets them next to switchlang so you can pick on purpose.

The short version

You want to… Reach for
Take data apart by shape (tuples, dataclasses, nested structures) match
Look up one of a fixed set of values, nothing else a dict
Branch on arbitrary conditions, not just equality if / elif / else
Dispatch on values with ranges, shared actions, a captured result, duplicate checking and optional fall-through switch

A dict of functions

handlers = {1: method_on_one, 2: method_on_two, 3: method_three}
result = handlers.get(value, default_method)()

For a handful of simple keys this is hard to beat. It gets awkward when several keys share an action (each one is written out), when a key is a range (unrolled by hand), and when a key is accidentally repeated: a dict literal silently keeps the last value, so the earlier handler disappears with no error. switchlang raises on the duplicate the first time the block runs, and lists and ranges are one line each.

A dict also has no notion of “nothing matched.” .get() with a default hides that; [] on a missing key raises KeyError with no sense of which switch failed. switchlang requires a default() or raises with the value in the message.

if / elif / else

if action == 'c' or action == 'a':
    result = create_account()
elif action == 'l':
    result = log_into_account()
elif action == 'a':
    result = update_availability()
else:
    unknown_command()

This is the most flexible option, because each branch can test anything, not just equality. It is also where two quiet bugs live. The 'a' in the third branch is dead code, because the first branch already claims it, and nothing in Python will say so. And the else forgot to set result, which only matters on the branch nobody tests. switchlang turns the first into a ValueError at registration and makes the second impossible, because every case including the default contributes a result.

Use if / elif when the tests aren’t equality tests. Use a switch when they are.

match

Python 3.10’s structural pattern matching is excellent at what it was built for: destructuring data by shape.

match event:
    case {'type': 'click', 'x': x, 'y': y}:
        handle_click(x, y)
    case {'type': 'key', 'code': code}:
        handle_key(code)

Compared with a value switch, it covers different ground:

  • No fall-through. match arms are exclusive. switchlang’s fall-through is opt-in per case.
  • Arms are syntax, not data. You can’t build match cases from a list at runtime. s.case(some_list, action) is a method call, so you can (see Patterns).
  • The capture trap. In a case pattern, a bare name such as case other: doesn’t compare against a variable named other; it binds a new one and matches everything. Keys in switchlang are values, so this can’t happen.
  • No result capture. match arms are blocks of statements. switchlang dispatches to callables and hands back the chosen one’s return value as s.result.
  • Python version. match needs 3.10 or newer. switchlang runs on 3.9 and newer.

If you are matching on structure, use match. If you are dispatching on values and want ranges, shared actions, a result and the validation described in Errors and validation, use switchlang.

What switchlang is not

It is not faster than any of the above; a switch block does a little more work than a dict lookup, because it validates as it goes. It is not a language change, and it won’t be recognized by tools that understand match. It is a small, readable library for the common case of “here is a value, here is what to do for each of its possibilities,” written so that the code looks like that sentence.