Intelligent rule-based function dispatch for Python
Project description
SmartSwitch
Intelligent rule-based function dispatch for Python
Replace messy if-elif chains and duplicated logic with clean, maintainable function registries.
Installation
pip install smartswitch
The Problem-Solution Approach
SmartSwitch helps you organize code that needs to handle different cases. Let's see how, step by step.
1. Function Registry Pattern
Problem: You have several operations and want to call them by name.
Traditional approach - Dictionary of functions:
# Hard to maintain, easy to make mistakes
operations = {
'save': save_data,
'load': load_data,
'delete': delete_data
}
# Calling
op = operations.get(action)
if op:
op(data)
With SmartSwitch - Clean registration:
from smartswitch import Switcher
ops = Switcher()
@ops
def save_data(data):
# Save logic
pass
@ops
def load_data(data):
# Load logic
pass
@ops
def delete_data(data):
# Delete logic
pass
# Call by name
ops('save_data')(data)
2. Custom Action Names
Problem: You want friendly names different from function names.
Traditional approach - Manual mapping:
actions = {
'reset': destroy_all_data,
'clear': remove_cache,
'wipe': erase_history
}
action = actions[command]
action()
With SmartSwitch - Alias registration:
ops = Switcher()
@ops('reset')
def destroy_all_data():
pass
@ops('clear')
def remove_cache():
pass
# Call with alias
ops('reset')()
Or use prefix-based auto-naming for convention-driven naming:
# Set a prefix to auto-derive handler names
protocols = Switcher(prefix='protocol_')
@protocols # Auto-registers as 's3_aws'
def protocol_s3_aws():
return {"type": "s3", "region": "us-east-1"}
@protocols # Auto-registers as 'gcs'
def protocol_gcs():
return {"type": "gcs", "bucket": "data"}
# Call by auto-derived names
protocols('s3_aws')()
protocols('gcs')()
3. Value-Based Dispatch
Problem: Choose handler based on actual data values.
Traditional approach - Long if-elif chains:
def process_user(user_type, reason):
if user_type == 'to_delete' and reason == 'no_payment':
# Remove user
pass
elif reason == 'no_payment':
# Send reminder
pass
elif user_type == 'to_delete':
# Archive
pass
else:
# Default
pass
With SmartSwitch - Declarative rules:
users = Switcher()
@users(valrule=lambda user_type, reason:
user_type == 'to_delete' and reason == 'no_payment')
def remove_user(user_type, reason):
# Remove user
pass
@users(valrule=lambda reason: reason == 'no_payment')
def send_payment_reminder(user_type, reason):
# Send reminder
pass
@users(valrule=lambda user_type: user_type == 'to_delete')
def archive_user(user_type, reason):
# Archive
pass
@users
def handle_default(user_type, reason):
# Default
pass
# Automatic dispatch
users()(user_type='to_delete', reason='no_payment')
Tip: For multi-parameter conditions, you can use compact dict-style lambda:
@users(valrule=lambda kw: kw['user_type'] == 'to_delete' and kw['reason'] == 'no_payment')
def remove_user(user_type, reason):
pass
4. Type-Based Dispatch
Problem: Handle different data types differently.
Traditional approach - Multiple isinstance checks:
def process(data):
if isinstance(data, str):
return data.upper()
elif isinstance(data, int):
return data * 2
elif isinstance(data, list):
return len(data)
else:
return None
With SmartSwitch - Type rules:
processor = Switcher()
@processor(typerule={'data': str})
def process_string(data):
return data.upper()
@processor(typerule={'data': int})
def process_number(data):
return data * 2
@processor(typerule={'data': list})
def process_list(data):
return len(data)
@processor
def process_other(data):
return None
# Automatic dispatch based on type
processor()(data="hello") # → HELLO
processor()(data=42) # → 84
Real-World Examples
API Routing
api = Switcher()
@api(valrule=lambda method, path: method == 'GET' and path == '/users')
def get_users(method, path, data=None):
return list_all_users()
@api(valrule=lambda method, path: method == 'POST' and path == '/users')
def create_user(method, path, data=None):
return create_new_user(data)
@api
def not_found(method, path, data=None):
return {"error": "Not Found", "status": 404}
# Dispatch
response = api()('GET', '/users')
Payment Processing
payments = Switcher()
@payments(typerule={'amount': int | float},
valrule=lambda method, amount: method == 'crypto' and amount > 1000)
def process_large_crypto(method, amount, details):
return {"processor": "crypto_large", "fee": amount * 0.01}
@payments(valrule=lambda method, **kw: method == 'credit_card')
def process_card(method, amount, details):
return {"processor": "credit_card", "fee": amount * 0.03}
@payments
def process_generic(method, amount, details):
return {"error": "Unsupported payment method"}
When to Use
✅ Good for:
- API handlers and request routers
- Business logic with multiple branches
- Plugin systems and extensible architectures
- State machines and workflow engines
- When you need type + value checks together
⚠️ Consider alternatives for:
- Simple 2-3 case switches → use
if/elif - Pure type dispatch → use
functools.singledispatch - Very high-performance code (< 10μs functions called millions of times)
Key Features
Core Dispatch Mechanisms
- 🔢 Type-based dispatch: Route by argument types (
int,str, custom classes, unions) - 🎯 Value-based dispatch: Match on runtime values with lambda rules
- 🔗 Combined rules: Use type AND value rules together for precise routing
Handler Management
- 📦 Named handler access: Retrieve and call handlers by name
- 🏷️ Custom aliases: Register handlers with user-friendly names
- 🔤 Prefix-based auto-naming: Convention-driven handler registration (NEW in v0.1.0)
Developer Experience
- 🧩 Modular & testable: Each handler is an independent function
- ✨ Clean API: Pythonic decorators with zero boilerplate
- 🚀 Efficient: Optimized with caching (~1-2μs overhead)
- 🛡️ Type-safe: Full type annotation support
API Discovery and Introspection
- 🔍 Handler introspection: List all registered handlers with
entries()method (v0.3.0) - 🌳 Hierarchical structures: Organize multiple Switchers with
add()method (v0.3.1) - 🔗 Dot notation access: Navigate hierarchies with dot notation like
mainswitch('users.list')(NEW in v0.3.1)
Logging and Observability
- 📊 Call history tracking: Record handler executions with args, results, timing (NEW in v0.4.0)
- 🔍 Performance analysis: Find slowest calls, analyze execution patterns
- 🐛 Error tracking: Filter and analyze failed executions
- 🤫 Silent mode: Zero-overhead history tracking for production
Organizing multiple Switchers in a class:
from smartswitch import Switcher
class MyAPI:
# Main switcher
mainswitch = Switcher(name="main")
# Add child switchers
users = mainswitch.add(Switcher(name="users", prefix="user_"))
products = mainswitch.add(Switcher(name="products", prefix="product_"))
@users
def user_list(self):
return ["alice", "bob"]
@products
def product_list(self):
return ["laptop", "phone"]
# Use directly
api = MyAPI()
api.users('list')() # Direct access
# Or via mainswitch with dot notation
api.mainswitch('users.list')() # Hierarchical access
api.mainswitch('products.list')()
Discovering child Switchers:
# Iterate all children
for child in api.mainswitch.children:
print(f"{child.name}: {child.entries()}")
# Output:
# users: ['list']
# products: ['list']
See the API Discovery Guide for details, or smpub for production usage.
Documentation
📚 Full documentation: smartswitch.readthedocs.io
Guides:
- Type Rules - Dispatch based on types
- Value Rules - Dispatch based on runtime values
- Named Handlers - Direct handler access
- API Discovery - Introspection and hierarchies
- Logging - History tracking and performance analysis (NEW in v0.4.0)
- Best Practices - Production patterns
- API Reference - Complete API docs
All examples in documentation are tested - They come directly from our test suite with 95% coverage.
Performance
SmartSwitch adds ~1-2 microseconds per dispatch. For real-world functions (API calls, DB queries, business logic), this overhead is negligible:
Function time: 50ms (API call)
Dispatch overhead: 0.002ms
Impact: 0.004% ✅
See Performance Best Practices for more details.
Thread Safety
SmartSwitch is designed for typical Python usage patterns:
- ✅ Handler dispatch (calling
sw()(args)) is fully thread-safe - uses read-only operations - ⚠️ Decorator registration should be done at module import time (single-threaded)
Recommended usage:
# Module level - executed once at import (safe)
switch = Switcher()
@switch(typerule={'x': int})
def handle_int(x):
return x * 2
# Runtime - called many times (thread-safe)
result = switch()(x=42)
For advanced scenarios requiring runtime registration in multi-threaded applications, external synchronization is needed.
License
MIT
Contributing
Contributions welcome! Please feel free to submit a Pull Request.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file smartswitch-0.4.0.tar.gz.
File metadata
- Download URL: smartswitch-0.4.0.tar.gz
- Upload date:
- Size: 30.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92a7e74570db0039632e1e2afb1705965e655517165ae55c2c963eaaaedfc057
|
|
| MD5 |
e6ab829c07d3c32be0c4288384d0d123
|
|
| BLAKE2b-256 |
522e86044ca0a3a83f4b9b1cb3d7fffa2b7c5a39499894783ec0228072bbf5dc
|
Provenance
The following attestation bundles were made for smartswitch-0.4.0.tar.gz:
Publisher:
publish.yml on genropy/smartswitch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
smartswitch-0.4.0.tar.gz -
Subject digest:
92a7e74570db0039632e1e2afb1705965e655517165ae55c2c963eaaaedfc057 - Sigstore transparency entry: 685815445
- Sigstore integration time:
-
Permalink:
genropy/smartswitch@76b7f1844f37150cba817972baf12023c0bb82b4 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@76b7f1844f37150cba817972baf12023c0bb82b4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file smartswitch-0.4.0-py3-none-any.whl.
File metadata
- Download URL: smartswitch-0.4.0-py3-none-any.whl
- Upload date:
- Size: 14.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6deac2e9f8525bd4404b175bb9a12e99217b0c4e1428616ff5ddb434d1467d1
|
|
| MD5 |
0bd9831f5c6d04043856df30a904c1b0
|
|
| BLAKE2b-256 |
8cc0c2659d174ce0b51e47bdb47d6a5df974f7e0e465be40b10768f798457cf2
|
Provenance
The following attestation bundles were made for smartswitch-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on genropy/smartswitch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
smartswitch-0.4.0-py3-none-any.whl -
Subject digest:
e6deac2e9f8525bd4404b175bb9a12e99217b0c4e1428616ff5ddb434d1467d1 - Sigstore transparency entry: 685815447
- Sigstore integration time:
-
Permalink:
genropy/smartswitch@76b7f1844f37150cba817972baf12023c0bb82b4 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@76b7f1844f37150cba817972baf12023c0bb82b4 -
Trigger Event:
push
-
Statement type: