A few shapes that come up again and again once a switch block is available. Each example runs as shown.
Wrap the block in a function
Most switch blocks exist to produce a value, and a function that returns s.result is the cleanest way to use one. It gives the switch a name, keeps s out of the enclosing scope, and makes the result available where the caller wants it:
from switchlang import switch, closed_range
def shipping_cost(weight_kg):
with switch(weight_kg) as s:
s.case(closed_range(0, 1), lambda: 5.00)
s.case(closed_range(2, 5), lambda: 9.50)
s.case(closed_range(6, 20), lambda: 19.00)
s.default(lambda: 'quote required')
return s.result
print(shipping_cost(1), shipping_cost(4), shipping_cost(12), shipping_cost(80))
5.0 9.5 19.0 quote required
A command loop
The example that motivated the library: a loop that reads a command and dispatches on it. The input is scripted here so the page can run, but the shape is the same with input():
inputs = iter(['a', '3', 'v', 'q'])
def get_action():
return next(inputs)
state = {'level': None, 'running': True}
def set_level(n):
state['level'] = n
return f'level set to {n}'
def quit_app():
state['running'] = False
return 'bye'
while state['running']:
action = get_action()
with switch(action) as s:
s.case(['a', 'c'], lambda: 'create account')
s.case(['v', 'b'], lambda: 'view bookings')
s.case([str(n) for n in range(1, 6)], lambda: set_level(int(action)))
s.case('q', quit_app)
s.case('', lambda: None)
s.default(lambda: f'unknown command {action!r}')
print(f'{action!r} -> {s.result}')
'a' -> create account
'3' -> level set to 3
'v' -> view bookings
'q' -> bye
Two things to notice. The lambdas close over action, so the digit case can read the value that matched. And the digit keys are built with a list comprehension: a case key is an ordinary expression, so it can come from anywhere.
Cases built from data
Because case() is a method call, you can register cases in a loop. That makes a switch block a good front end for a table of handlers that lives in configuration or is assembled at startup:
parsers = {
'json': lambda: 'parse as JSON',
'yaml': lambda: 'parse as YAML',
'toml': lambda: 'parse as TOML',
}
extension = 'yaml'
with switch(extension) as s:
for key, handler in parsers.items():
s.case(key, handler)
s.default(lambda: f'no parser for .{extension}')
print(s.result)
You still get duplicate detection and the “nothing matched” check, which a bare dict lookup doesn’t give you. If the table is the whole story and never changes, a dict is fine on its own; the switch earns its keep when some cases are data and others are code.
A state machine on tuple keys
Tuples are hashable, so a (state, event) pair is a valid key. That turns a transition table into a switch block, with default() as “stay where you are”:
from enum import Enum, auto
class Job(Enum):
QUEUED = auto()
RUNNING = auto()
DONE = auto()
FAILED = auto()
def next_state(state, event):
with switch((state, event)) as s:
s.case((Job.QUEUED, 'start'), lambda: Job.RUNNING)
s.case((Job.RUNNING, 'finish'), lambda: Job.DONE)
s.case((Job.RUNNING, 'error'), lambda: Job.FAILED)
s.case((Job.FAILED, 'retry'), lambda: Job.QUEUED)
s.default(lambda: state)
return s.result
job = Job.QUEUED
for event in ['start', 'error', 'retry', 'start', 'finish', 'start']:
job = next_state(job, event)
print(f'{event:7} -> {job.name}')
start -> RUNNING
error -> FAILED
retry -> QUEUED
start -> RUNNING
finish -> DONE
start -> DONE
The last 'start' arrives in DONE, where it means nothing, and the default keeps the job where it is.
An explicit “do nothing” case
Sometimes a value is valid and should do nothing, and you want that written down rather than left to the default. lambda: None is the idiom:
s.case('', lambda: None) # empty input: ignore it
It reads as a decision, which is the point. The default stays free to mean “this value was not expected.”
Grouped actions with fall-through
Fall-through is rare in practice, and when you want it, it is usually to share a tail of work between neighboring cases. Here the “verbose” level does everything “normal” does, plus more:
def configure_logging(level):
steps = []
with switch(level) as s:
s.case('verbose', lambda: steps.append('log every request'), fallthrough=True)
s.case('normal', lambda: steps.append('log errors and warnings'), fallthrough=True)
s.case('quiet', lambda: steps.append('log errors only'))
s.default(lambda: steps.append('unknown level, logging errors only'))
return steps
print(configure_logging('verbose'))
print(configure_logging('quiet'))
['log every request', 'log errors and warnings', 'log errors only']
['log errors only']
The order of the cases is the order of the chain, so arrange them from “most” to “least”. Fall-through explains the rules.