Keys and matching

A case key is any value you want to compare the switch value against. This page covers how that comparison works, what makes a valid key, and the three ways to map several keys to one action: lists, ranges and closed_range.

Equality, not identity

A case matches when key == value. That is the same rule a dict lookup uses, with the same consequences: 1, 1.0 and True are all the same key.

from switchlang import switch

with switch(1.0) as s:
    s.case(1, lambda: 'matched the integer 1')
    s.default(lambda: 'no match')

print(s.result)
matched the integer 1

Registering two of them is a duplicate, and switchlang says so:

try:
    with switch(1) as s:
        s.case(1, lambda: 'one')
        s.case(True, lambda: 'true')
except ValueError as e:
    print(e)
Duplicate case: True

Keys must be hashable

Keys are kept in a set so duplicates can be detected, so a key has to be hashable. Numbers, strings, None, enum members, tuples and most objects qualify. A dict or a list does not; a list is instead treated as a collection of keys (see below).

try:
    with switch('x') as s:
        s.case({'not': 'hashable'}, lambda: None)
except TypeError as e:
    print(e)
cannot use 'dict' as a set element (unhashable type: 'dict')

Any hashable value works

Enum members and None are good keys. A tuple is a single key, which makes it a natural way to switch on a pair of values:

from enum import Enum


class Light(Enum):
    RED = 1
    GREEN = 2


with switch(Light.GREEN) as s:
    s.case(Light.RED, lambda: 'stop')
    s.case(Light.GREEN, lambda: 'go')
    s.case(None, lambda: 'light is off')

print(s.result)

with switch(('admin', 'delete')) as s:
    s.case(('admin', 'delete'), lambda: 'allowed')
    s.case(('user', 'delete'), lambda: 'denied')
    s.default(lambda: 'unknown action')

print(s.result)
go
allowed

Patterns builds a small state machine on tuple keys.

Lists: several keys, one action

A list key registers one case per item, all pointing at the same function:

def kind(ch):
    with switch(ch) as s:
        s.case(list('aeiou'), lambda: 'vowel')
        s.case(list('bcdfghjklmnpqrstvwxyz'), lambda: 'consonant')
        s.default(lambda: 'not a lowercase letter')
    return s.result


print(kind('e'), kind('k'), kind('7'))
vowel consonant not a lowercase letter

Each item is a real case, so an item that already appears in another case is a duplicate error, and an empty list is refused because it could never match. The list itself is never a key; if you want to match a sequence as one value, use a tuple.

Ranges

A range works the same way: every number in it becomes a case. Remember that range is half-open, so range(1, 6) is 1 through 5:

with switch(5) as s:
    s.case(range(1, 6), lambda: 'range(1, 6) covers 1 to 5')
    s.case(range(6, 10), lambda: 'range(6, 10) covers 6 to 9')
    s.default(lambda: 'out of range')

print(s.result)
range(1, 6) covers 1 to 5

That 6 at the end of one case and the start of the next is correct, but you have to know the half-open rule to read it. For “levels one through five” it is easier to say what you mean.

closed_range: inclusive on both ends

closed_range(start, stop) includes stop. With a step, it never goes past stop, and stop itself is included when the step lands on it:

from switchlang import closed_range

print(list(closed_range(1, 5)))
print(list(closed_range(1, 6, 2)))
print(list(closed_range(1, 7, 2)))
[1, 2, 3, 4, 5]
[1, 3, 5]
[1, 3, 5, 7]

So the levels example reads the way you’d say it:

with switch(5) as s:
    s.case(closed_range(1, 5), lambda: 'one to five')
    s.case(closed_range(6, 9), lambda: 'six to nine')
    s.default(lambda: 'out of range')

print(s.result)
one to five

One thing to watch: adjacent closed ranges share their boundary. closed_range(1, 5) and closed_range(5, 9) both contain 5, which is a duplicate:

try:
    with switch(5) as s:
        s.case(closed_range(1, 5), lambda: 'low')
        s.case(closed_range(5, 9), lambda: 'high')
except ValueError as e:
    print(e)
Duplicate case: 5

Start the neighbor at 6. closed_range also insists that start < stop and step >= 1; Errors and validation lists those checks.

A note on large ranges

Because every number in a range becomes its own case, s.case(range(1, 1_000_000), f) registers a million keys. That works, but it is a million set insertions on every run of the block. For wide numeric bands, a few comparisons before the switch, or a default() that does the arithmetic, is a better fit. switchlang is for dispatching on a readable set of values, not for interval arithmetic.

Next