PythonDec 15, 202515 minute read

Using Registry Patterns for Cleaner Conditional Logic

CH
Conor Hollern
Using Registry Patterns for Cleaner Conditional Logic

Replace Complex Conditional Logic with a Registry Class

Complex conditional logic has a way of starting small and quietly becoming unmaintainable. A few if statements turn into nested branches, duplicated logic, and fragile code paths. Python’s registry pattern offers a simple, scalable alternative that keeps your logic explicit, testable, and easy to extend—without touching existing code.

1. The Problem with Growing Conditional Logic

Most conditional complexity starts in one of two ways:

Method 1: With a long if / elif / else statement that dispatches logic based on function input:

1
def process_event(event_type, payload):
2
if event_type == 'user_created':
3
handle_user_created(payload)
4
elif event_type == 'user_deleted':
5
handle_user_deleted(payload)
6
elif event_type == 'user_updated':
7
handle_user_updated(payload)
8
else:
9
raise ValueError('Unknown event type')

Method 2: Or with a dictionary that stores the function to run:

1
# Dictionary definition
2
event_handler = {
3
'user_created': handle_user_created,
4
'user_deleted': handle_user_deleted,
5
'user_updated': handle_user_updated
6
}
7
8
# Run the selected function dynamically
9
func_to_run = event_handler['user_created']
10
response = func_to_run()

This works completely fine-- until it doesn't.

Common failure modes:

  • Adding a new case requires modifying existing code
  • Logic becomes scattered
  • Testing individual branches gets harder
  • Merge conflict become frequent

What you really want is open-for-extension, closed-for-modification behavior.

2. Introducing the Registry Pattern

At its core, a registry is just a dictionary that is managed via a class or function.

Setup:

1
class Registry:
2
def __init__(self):
3
self._registry = {}
4
5
def register(self, name, func):
6
self._registry[name] = func
7
8
def get(self, name):
9
return self._registry[name]
10
11
def run(self, name: str, **kwargs):
12
func = self.get(name)
13
return func(**kwargs)

Usage:

1
from my_project.other_module import important_function
2
3
my_registry = Registry()
4
my_registry.register('important_function', important_function)
5
6
# Can be used in any other module where you import "my_registry"
7
my_registry.run('important_function')

This registry class has 4 critical components:

  • registry dictionary: This is defined in __init__ and is used to reference the "registered" functions.
  • register: This function simple adds a python function to the registry dictionary with the given name.
  • get: This function retreives the function by the name defined in the register function.
  • run: This function runs the function with the given keyword arguments.

You can see that the setup of the registry pattern is extremely simple and adds immense power with minimal code.

3. Enhancing Registries to Promote Scalability

Example 1: Add name checking and error handling to ensure the function you attempting to call exists. And also raise an error when you attempt to register a function that already is registered.

1
def register(self, name, func):
2
# This if statement makes sure we can't overwrite an existing function
3
if name in self._registry:
4
raise KeyError(f'{name} already exists in the registry.')
5
self._registry[name] = func
6
7
def get(self, name):
8
# This if statements makes sure we don't run a function that doesn't exist
9
if name not in self._registry:
10
raise ValueError(f'{name} does not exist in the registry.')
11
return self._registry[name]

Example 2: Adding other business logic before a function is run. Let's say you want to ensure the same logic is executed every time one of these registered functions runs, a good example of this may be logging. This is extremely simple with a registry pattern.

1
def run(self, name: str, **kwargs):
2
my_logger.log_info(f'Function with name {name} is about to run...')
3
func = self.get(name)
4
return func(**kwargs)

This can, naturally, be extended to many other use cases as your system scales and needs additional functionality.

4. Adding Decorators to Registries

Importing every function that needs to be registered into the registry module can quickly become unwieldy. Using a decorator solves this by allowing each function to register itself at definition time within its own module.

Setup:

1
def register(self, name):
2
def decorator(func):
3
self._registry[name] = func
4
return func
5
return decorator

Usage:

1
from registry_definition_module import my_registry
2
3
my_registry.register('important_function')
4
def important_function():
5
return 2 + 2

This register function replaces the earlier implementation. It automatically adds the decorated function to the class-level registry, allowing it to be invoked anywhere the registry is imported.

5. Why Registries Age Better Than Conditionals

Registries give you:

  • Linear growth instead of exponential branching
  • Clear ownership of logic
  • Easy test isolation
  • Extensability

Final Thought

Registry patterns are deceptively simple--but they unlock a level of composability that scales from small scripts to production systems.