Named Handlers Guide
Test Source: Examples in this guide are from test_complete.py
Named handlers allow you to retrieve and invoke specific handlers by name, without relying on automatic rule-based dispatch. This is useful for explicit invocation, method routing, and plugin systems.
Basic Named Handler Access
Every handler registered with a Switcher is accessible by its function name:
From test: test_get_handler_by_name
from smartswitch import Switcher
sw = Switcher()
@sw
def compute(x):
return x * 2
# Retrieve handler by name
handler = sw('compute')
assert handler(x=5) == 10
Key Points:
Handlers are registered by their function name automatically
Use
sw('name')to retrieve a handlerThe returned handler is a normal Python function
One-Liner Invocation
For quick invocations, you can retrieve and call in one line:
From test: test_get_handler_by_name_one_liner
sw = Switcher()
@sw
def compute(x):
return x * 2
# One-liner: retrieve + invoke
assert sw('compute')(x=5) == 10
This is especially useful in routing scenarios where the handler name comes from external input.
Custom Aliases
You can register handlers with custom names different from their function name:
From test: test_register_with_alias
sw = Switcher()
@sw('reset')
def destroyall():
return "destroyed"
# Access by alias, not function name
assert sw('reset')() == "destroyed"
Use Cases:
User-facing command names
API endpoint mapping
Backward compatibility aliases
Explicit Named Dispatch
sw = Switcher()
@sw
def process_payment(amount):
return f"Processing ${amount}"
@sw
def refund_payment(amount):
return f"Refunding ${amount}"
# Explicit: you choose the handler
action = 'process_payment' # From user input, config, etc.
result = sw(action)(amount=100)
print(result) # → "Processing $100"
When to Use Each:
Automatic: When dispatch logic is complex or based on types/values
Explicit: When handler selection comes from external sources (user input, config, database)
Prefix-Based Auto-Naming
When handlers follow a naming convention, you can use prefix-based auto-naming to simplify handler retrieval:
From test: test_prefix_basic_stripping
sw = Switcher(prefix='handle_')
@sw
def handle_foo(x):
return f"foo: {x}"
@sw
def handle_bar(x):
return f"bar: {x}"
# Handlers registered with stripped names
assert 'foo' in sw._handlers
assert 'bar' in sw._handlers
# Access by shortened names (prefix stripped)
assert sw('foo')(x=5) == "foo: 5"
assert sw('bar')(x=10) == "bar: 10"
How It Works:
The
prefixparameter is specified when creating the SwitcherFunctions whose names start with the prefix are registered with shortened names
The prefix is stripped from the handler name
Functions not matching the prefix keep their full name
Real-World Example: Protocol Handlers
protocol = Switcher(prefix='protocol_')
@protocol
def protocol_s3_aws(bucket, key):
return f"S3: s3://{bucket}/{key}"
@protocol
def protocol_gcs(bucket, key):
return f"GCS: gs://{bucket}/{key}"
@protocol
def protocol_local(path):
return f"Local: {path}"
# Access by shortened protocol names
print(protocol('s3_aws')(bucket='data', key='file.csv'))
# → "S3: s3://data/file.csv"
print(protocol('gcs')(bucket='data', key='file.csv'))
# → "GCS: gs://data/file.csv"
print(protocol('local')(path='/tmp/file.csv'))
# → "Local: /tmp/file.csv"
Use Cases:
Plugin systems (
plugin_*)Command handlers (
cmd_*)Protocol dispatchers (
protocol_*)HTTP handlers (
handle_*)
Prefix with Description
Combine prefix with description for self-documenting code:
From test: test_prefix_with_description
sw = Switcher(
name='protocol',
description='Storage protocol dispatcher',
prefix='protocol_'
)
@sw
def protocol_s3_aws(x):
return "s3_aws"
@sw
def protocol_gcs(x):
return "gcs"
assert sw.description == 'Storage protocol dispatcher'
assert 's3_aws' in sw._handlers
assert 'gcs' in sw._handlers
assert sw('s3_aws')(x=1) == "s3_aws"
assert sw('gcs')(x=1) == "gcs"
When Prefix Doesn’t Match
Functions not matching the prefix keep their full name:
From test: test_prefix_no_match_uses_full_name
sw = Switcher(prefix='handle_')
@sw
def other_name(x):
return f"other: {x}"
# Registered with full name (no prefix match)
assert 'other_name' in sw._handlers
assert sw('other_name')(x=5) == "other: 5"
Best Practice: Be consistent with your naming convention to avoid confusion.
Explicit Names Override Prefix
You can always override the prefix with an explicit alias:
From test: test_prefix_explicit_name_override
sw = Switcher(prefix='handle_')
@sw('custom_name')
def handle_foo(x):
return x
# Registered as 'custom_name', not 'foo'
assert 'custom_name' in sw._handlers
assert 'foo' not in sw._handlers
assert sw('custom_name')(x=5) == 5
Handler Registry
All registered handlers live in the _handlers dictionary:
sw = Switcher()
@sw
def foo(x):
return x
@sw
def bar(x):
return x * 2
# Check what's registered
print('foo' in sw._handlers) # → True
print('bar' in sw._handlers) # → True
print(len(sw._handlers)) # → 2
Use Cases:
Introspection
Dynamic handler discovery
Plugin listing
Duplicate Name Detection
SmartSwitch prevents duplicate handler names:
sw = Switcher()
@sw
def my_function():
return "first"
# This will raise ValueError
@sw
def my_function():
return "second"
Error Message: ValueError: Handler 'my_function' already taken by function 'my_function'
Duplicate Alias Detection
sw = Switcher()
@sw('action')
def first():
return "first"
# This will raise ValueError
@sw('action')
def second():
return "second"
Error Message: ValueError: Alias 'action' is already registered
Best Practice: Use unique names or aliases for all handlers.
Handler Not Found
When you request a handler that doesn’t exist, SmartSwitch returns a decorator (for potential future registration):
sw = Switcher()
@sw
def existing(x):
return x
# 'nonexistent' is not registered
result = sw('nonexistent')
# Returns a decorator (not an error)
# You can use it to register a handler with that name
@result
def new_handler(x):
return "new"
# Now it's available
print(sw('nonexistent')(x=1)) # → "new"
This allows dynamic registration, which can be useful for plugin systems or lazy loading.
Common Patterns
Command Dispatcher
commands = Switcher(prefix='cmd_')
@commands
def cmd_start(service):
return f"Starting {service}"
@commands
def cmd_stop(service):
return f"Stopping {service}"
@commands
def cmd_restart(service):
return f"Restarting {service}"
# From user input
user_command = 'start' # e.g., from CLI argument
service_name = 'nginx'
result = commands(user_command)(service=service_name)
print(result) # → "Starting nginx"
Plugin Registry
plugins = Switcher(
name='plugins',
description='Available plugins',
prefix='plugin_'
)
@plugins
def plugin_logger(event):
print(f"LOG: {event}")
@plugins
def plugin_metrics(event):
print(f"METRIC: {event}")
@plugins
def plugin_alerts(event):
print(f"ALERT: {event}")
# Dynamically invoke all plugins
enabled_plugins = ['logger', 'metrics']
for plugin_name in enabled_plugins:
plugins(plugin_name)(event="User logged in")
# Output:
# LOG: User logged in
# METRIC: User logged in
Method Routing
class APIHandler:
routes = Switcher(prefix='handle_')
@routes
def handle_get_users(self):
return "List of users"
@routes
def handle_post_users(self):
return "Create user"
def dispatch(self, method, path):
# Build handler name from HTTP method + path
handler_name = f"{method.lower()}_{path.strip('/').replace('/', '_')}"
return self.routes(handler_name)()
api = APIHandler()
print(api.dispatch('GET', '/users')) # → "List of users"
print(api.dispatch('POST', '/users')) # → "Create user"
Configuration-Based Dispatch
import os
storage = Switcher(prefix='backend_')
@storage
def backend_s3(data):
return f"S3: {data}"
@storage
def backend_local(data):
return f"Local: {data}"
@storage
def backend_gcs(data):
return f"GCS: {data}"
# From environment variable
backend_type = os.getenv('STORAGE_BACKEND', 'local')
# Dispatch based on config
handler = storage(backend_type)
result = handler(data="test.csv")
print(result)
Combining Named and Automatic Dispatch
You can use both modes together:
sw = Switcher()
# Named handlers
@sw
def explicit_action(x):
return f"Explicit: {x}"
@sw
def another_action(x):
return f"Another: {x}"
# Call by name
print(sw('explicit_action')(x=10)) # → "Explicit: 10"
print(sw('another_action')(x=20)) # → "Another: 20"
Use Case: Some handlers are invoked explicitly (admin commands), while others are dispatched automatically (request routing).
Descriptor Protocol for Class Usage
When using Switcher as a class attribute, it automatically binds self to handlers:
sw = Switcher()
@sw
def method(self, x):
return f"{self.name}: {x}"
class MyClass:
dispatch = sw
def __init__(self, name):
self.name = name
obj = MyClass("test")
# Get bound switcher from instance
bound = obj.dispatch
# Handlers automatically receive 'self'
result = bound('method')(x=5)
print(result) # → "test: 5"
How It Works:
Accessing
obj.dispatchreturns aBoundSwitcherBoundSwitcherautomatically bindsselfto retrieved handlersNo need to pass
selfmanually
Use Cases:
Method dispatch in classes
State machines with instance state
Plugin systems in object-oriented code
Performance Characteristics
Named handler access is extremely fast:
Dictionary lookup: O(1)
No rule evaluation
No type checking
Overhead: ~50-100 nanoseconds (dictionary lookup)
This makes named handlers ideal for:
✅ High-frequency dispatch (millions of calls)
✅ Performance-critical paths
✅ Simple routing where rules are overkill
Best Practices
Use descriptive names: Handler names should be clear and self-documenting
Prefix conventions: Use consistent prefixes for related handlers
Documentation: Document available handler names for users
Error handling: Handle missing handlers gracefully
Testing: Test handler registration and retrieval
# Good: clear names with prefix
api = Switcher(prefix='handle_')
@api
def handle_get_users(): ...
@api
def handle_create_user(): ...
# Also good: explicit aliases
api = Switcher()
@api('GET /users')
def list_users(): ...
@api('POST /users')
def create_user(): ...
Next Steps
Explore Plugin System for extending functionality
See Best Practices for production usage patterns
Check Real-World Examples for practical use cases
Read the API Reference for detailed documentation