
# Introduction
Each Python codebase has this downside. A perform that begins small. Two branches, perhaps three. Then somebody provides a case, another person provides one other, and a 12 months later you’ve got received 200 strains of if/elif/else that no person desires to the touch. Here is an instance:
def get_model(title):
if title == "logreg":
return LogisticRegression()
elif title == "random_forest":
return RandomForestClassifier()
elif title == "svm":
return SVC()
elif title == "xgboost":
return XGBClassifier()
# ... 15 extra branches
else:
increase ValueError(f"Unknown mannequin: {title}")
And yeah, it really works. Nevertheless it additionally breaks the Open/Closed Precept, which states that software program entities (courses, modules, and features) ought to be open for extension however closed for modification. There’s a higher technique to deal with this downside: the registry sample. This text covers what the registry sample is, the right way to construct it up from a five-line dictionary to a production-grade reusable class, and when it really earns its place in your code. So, let’s get began.
# The Drawback With If-Else Chains
An extended conditional chain fails in a number of particular methods:
- It violates the Open/Closed Precept. New case, new edit to a perform that already labored. Yesterday’s examined code will get cracked open, retested, and reviewed once more. The unit of change ought to be “add a file,” not “modify the central dispatcher.”
- It piles unrelated logic into one place. Say your cost dispatcher covers bank cards, PayPal, and crypto. Now three domains that don’t have anything to do with one another are sharing one perform. The
elifladder forces them to share a room anyway. - It scales badly. Each new department provides to the cognitive weight of the entire perform. Twenty branches is twenty issues to scroll previous each time you’re debugging department quantity three.
- It can’t be prolonged from exterior. Ship a library with a hardcoded
get_model()chain and your customers are caught. They can’t add their very own mannequin with out monkey-patching or forking. The logic is sealed shut.
The registry sample fixes all 4 by flipping the connection. As a substitute of the dispatcher understanding about each possibility, every possibility proclaims itself to the dispatcher.
What’s the registry sample?
It’s principally a central lookup desk that maps keys to things (features, courses, situations), the place every object registers itself as an alternative of being hardcoded into some conditional. In Python, that lookup desk is nearly all the time a dictionary, and “registering” is normally achieved with a decorator.
# Going From If-Else to a Dictionary
The smallest attainable win is to swap the chain for a dictionary lookup. One step, and the linear scan is gone:
MODEL_REGISTRY = {
"logreg": LogisticRegression,
"random_forest": RandomForestClassifier,
"svm": SVC,
"xgboost": XGBClassifier,
}
def get_model(title):
attempt:
return MODEL_REGISTRY[name]
besides KeyError:
increase ValueError(
f"Unknown mannequin: {title!r}. "
f"Out there: {listing(MODEL_REGISTRY)}"
) from None
That is already a registry — only a hand-maintained one. Dispatch is O(1), the choices are introspectable with listing(MODEL_REGISTRY), and the dispatcher by no means adjustments. One wart stays: each new mannequin nonetheless means modifying the dict and importing its class on the high of the file. You are able to do higher by letting every element register itself.
# Constructing a Decorator-Primarily based Registry
That is the model you will really use daily. Registration occurs in a decorator, so each perform or class declares its personal key proper the place it’s outlined:
PAYMENT_HANDLERS = {}
def register(payment_type):
def decorator(func):
PAYMENT_HANDLERS[payment_type] = func
return func
return decorator
@register("credit_card")
def charge_credit_card(quantity):
return f"Charged ${quantity} to bank card"
@register("paypal")
def charge_paypal(quantity):
return f"Charged ${quantity} through PayPal"
@register("crypto")
def charge_crypto(quantity):
return f"Charged ${quantity} in crypto"
def process_payment(payment_type, quantity):
handler = PAYMENT_HANDLERS.get(payment_type)
if handler is None:
increase ValueError(f"Unknown cost sort: {payment_type!r}")
return handler(quantity)
Have a look at what modified. The process_payment dispatcher is 4 strains, and it’ll by no means develop. Need Apple Pay? Write a brand new perform, slap @register("apple_pay") on it, put it in no matter file you want, and also you’re achieved. No central listing to edit. No merge battle. No reopening examined code. The handler sits proper subsequent to its personal key, which is strictly the place the following reader will search for it.
# Constructing a Reusable Registry Class
After getting two or three registries mendacity round, you’ll get uninterested in rewriting the identical decorator boilerplate. Wrap it in a small class and also you get collision detection, higher error messages, and a clear API without cost:
class Registry:
"""A reusable name-to-object registry."""
def __init__(self, title):
self.title = title
self._registry = {}
def register(self, key):
def decorator(obj):
if key in self._registry:
increase KeyError(
f"{key!r} already registered in {self.title!r}"
)
self._registry[key] = obj
return obj
return decorator
def get(self, key):
if key not in self._registry:
increase KeyError(
f"{key!r} not present in {self.title!r}. "
f"Out there: {listing(self._registry)}"
)
return self._registry[key]
def __contains__(self, key):
return key in self._registry
def keys(self):
return self._registry.keys()
Now use it to construct a text-processing pipeline pushed fully by config:
transforms = Registry("transforms")
@transforms.register("lowercase")
def to_lower(textual content):
return textual content.decrease()
@transforms.register("strip")
def strip_whitespace(textual content):
return textual content.strip()
@transforms.register("remove_digits")
def remove_digits(textual content):
return "".be a part of(c for c in textual content if not c.isdigit())
# The pipeline is now simply information. It might come from a YAML file,
# a CLI argument, or a database row.
pipeline = ["strip", "lowercase", "remove_digits"]
textual content = " Order #4521 CONFIRMED "
for step in pipeline:
textual content = transforms.get(step)(textual content)
print(repr(textual content))
Output:
'order # confirmed'
That is the place the sample pays for itself. The conduct of this system is now described by information — a listing of strings — not by code. Reordering the pipeline, including a step, or handing the entire thing to a non-programmer via a config file all change into trivial.
# Auto-Registering Lessons With __init_subclass__
When your registry holds courses as an alternative of features, Python has an excellent slicker trick. The __init_subclass__ hook (obtainable since Python 3.6) fires mechanically each time a subclass is outlined, so subclasses register themselves with no decorator in any respect:
class DataLoader:
_registry = {}
def __init_subclass__(cls, fmt=None, **kwargs):
tremendous().__init_subclass__(**kwargs)
if fmt:
DataLoader._registry[fmt] = cls
@classmethod
def get_loader(cls, fmt):
if fmt not in cls._registry:
increase ValueError(
f"No loader for {fmt!r}. "
f"Out there: {listing(cls._registry)}"
)
return cls._registry[fmt]
class CSVLoader(DataLoader, fmt="csv"):
def load(self, path):
return f"Loading CSV from {path}"
class JSONLoader(DataLoader, fmt="json"):
def load(self, path):
return f"Loading JSON from {path}"
class ParquetLoader(DataLoader, fmt="parquet"):
def load(self, path):
return f"Loading Parquet from {path}"
loader = DataLoader.get_loader("parquet")
print(loader.load("gross sales.parquet")) # Loading Parquet from gross sales.parquet
No decorator wherever. Subclassing DataLoader with a fmt= argument is sufficient to register the brand new class. That is how a whole lot of frameworks construct their plugin techniques underneath the hood.
# The place the Registry Sample Is Helpful in Observe
This isn’t a tutorial train. It’s the spine of instruments you already use.
- Machine studying experiment configs. Hugging Face Transformers, Detectron2, and MMDetection all use registries so you’ll be able to choose a mannequin, optimizer, or augmentation by string title in a YAML file.
build_model({"mannequin": "resnet50"})beats a largeif spine == ...block, and it lets researchers add architectures with out ever touching the coach. - File format and parser dispatch. Map extensions like
"csv","json", and"parquet"to loader courses. Supporting a brand new format turns into “write one class,” not “edit the loader.” - Net framework routing. Flask‘s
@app.route("/customers")and Click on‘s@cli.command()are registries in disguise. The decorator maps a URL or command title to the perform that handles it. - Plugin architectures. Any “drop a file on this folder and it simply works” system — whether or not pytest fixtures, Airflow operators, or serializer backends — is nearly all the time a registry amassing elements at import time.
- Occasion handlers and state machines. Map occasion names or states to handler features as an alternative of branching on them. The transition desk turns right into a readable dictionary relatively than a nest of conditionals.
# Sensible Concerns and Issues to Watch Out For
- Registration solely occurs on import. A decorator runs when Python executes the file it lives in. In case your handlers sit in
handlers/apple_pay.pyand nothing ever imports that module, the@registerdecorator by no means fires and the handler quietly goes lacking. The repair is to verify registration modules get imported — normally via an express import in a package deal’s__init__.py, or a small discovery loop withpkgutil.iter_modulesthat imports every thing in a plugin folder. - Guard in opposition to silent overwrites. With a plain dict, two elements registering the identical key clobber one another with no peep. Because the
Registryclass above exhibits, elevating on a reproduction key turns a baffling runtime bug into an apparent error at import time. - Present individuals what is on the market. At all times expose the keys with
listing(registry.keys())and put them in your error messages. “Unknown mannequin: ‘lgbm’. Out there: [‘logreg’, ‘random_forest’, ‘xgboost’]” saves much more debugging time than a nakedKeyError. - Don’t attain for it too early. A registry is overkill for 2 or three steady branches whose logic genuinely differs. If the branches share no widespread signature, or the circumstances are ranges relatively than discrete keys (
if rating > 0.9 ... elif rating > 0.5 ...), a plain conditional is clearer. The registry wins in a single particular state of affairs: you’re dispatching on a discrete key to interchangeable behaviors, and also you anticipate that set of behaviors to develop.
# Wrapping Up
The registry sample trades a rising, central, hard-to-extend if/elif/else chain for a lookup desk that elements fill in themselves. The payoff is concrete. Your dispatcher stops altering. New options present up as new recordsdata as an alternative of edits to previous ones. Habits turns into one thing you’ll be able to drive from a config. And customers of your code get an actual extension level as an alternative of a locked door.
Begin small. Subsequent time you catch your self typing a 3rd elif title == ..., cease and ask whether or not a dictionary would do. Normally it could. From there, the decorator and sophistication variations are a brief hop away.
# Earlier than
if form == "a": ...
elif form == "b": ...
elif form == "c": ...
# After
@registry.register("a")
def handle_a(): ...
Your future self, scrolling previous a four-line dispatcher as an alternative of a 200-line ladder, will thanks.
Kanwal Mehreen is a machine studying engineer and a technical author with a profound ardour for information science and the intersection of AI with medication. She co-authored the book “Maximizing Productiveness with ChatGPT”. As a Google Era Scholar 2022 for APAC, she champions range and tutorial excellence. She’s additionally acknowledged as a Teradata Range in Tech Scholar, Mitacs Globalink Analysis Scholar, and Harvard WeCode Scholar. Kanwal is an ardent advocate for change, having based FEMCodes to empower ladies in STEM fields.

# Introduction
Each Python codebase has this downside. A perform that begins small. Two branches, perhaps three. Then somebody provides a case, another person provides one other, and a 12 months later you’ve got received 200 strains of if/elif/else that no person desires to the touch. Here is an instance:
def get_model(title):
if title == "logreg":
return LogisticRegression()
elif title == "random_forest":
return RandomForestClassifier()
elif title == "svm":
return SVC()
elif title == "xgboost":
return XGBClassifier()
# ... 15 extra branches
else:
increase ValueError(f"Unknown mannequin: {title}")
And yeah, it really works. Nevertheless it additionally breaks the Open/Closed Precept, which states that software program entities (courses, modules, and features) ought to be open for extension however closed for modification. There’s a higher technique to deal with this downside: the registry sample. This text covers what the registry sample is, the right way to construct it up from a five-line dictionary to a production-grade reusable class, and when it really earns its place in your code. So, let’s get began.
# The Drawback With If-Else Chains
An extended conditional chain fails in a number of particular methods:
- It violates the Open/Closed Precept. New case, new edit to a perform that already labored. Yesterday’s examined code will get cracked open, retested, and reviewed once more. The unit of change ought to be “add a file,” not “modify the central dispatcher.”
- It piles unrelated logic into one place. Say your cost dispatcher covers bank cards, PayPal, and crypto. Now three domains that don’t have anything to do with one another are sharing one perform. The
elifladder forces them to share a room anyway. - It scales badly. Each new department provides to the cognitive weight of the entire perform. Twenty branches is twenty issues to scroll previous each time you’re debugging department quantity three.
- It can’t be prolonged from exterior. Ship a library with a hardcoded
get_model()chain and your customers are caught. They can’t add their very own mannequin with out monkey-patching or forking. The logic is sealed shut.
The registry sample fixes all 4 by flipping the connection. As a substitute of the dispatcher understanding about each possibility, every possibility proclaims itself to the dispatcher.
What’s the registry sample?
It’s principally a central lookup desk that maps keys to things (features, courses, situations), the place every object registers itself as an alternative of being hardcoded into some conditional. In Python, that lookup desk is nearly all the time a dictionary, and “registering” is normally achieved with a decorator.
# Going From If-Else to a Dictionary
The smallest attainable win is to swap the chain for a dictionary lookup. One step, and the linear scan is gone:
MODEL_REGISTRY = {
"logreg": LogisticRegression,
"random_forest": RandomForestClassifier,
"svm": SVC,
"xgboost": XGBClassifier,
}
def get_model(title):
attempt:
return MODEL_REGISTRY[name]
besides KeyError:
increase ValueError(
f"Unknown mannequin: {title!r}. "
f"Out there: {listing(MODEL_REGISTRY)}"
) from None
That is already a registry — only a hand-maintained one. Dispatch is O(1), the choices are introspectable with listing(MODEL_REGISTRY), and the dispatcher by no means adjustments. One wart stays: each new mannequin nonetheless means modifying the dict and importing its class on the high of the file. You are able to do higher by letting every element register itself.
# Constructing a Decorator-Primarily based Registry
That is the model you will really use daily. Registration occurs in a decorator, so each perform or class declares its personal key proper the place it’s outlined:
PAYMENT_HANDLERS = {}
def register(payment_type):
def decorator(func):
PAYMENT_HANDLERS[payment_type] = func
return func
return decorator
@register("credit_card")
def charge_credit_card(quantity):
return f"Charged ${quantity} to bank card"
@register("paypal")
def charge_paypal(quantity):
return f"Charged ${quantity} through PayPal"
@register("crypto")
def charge_crypto(quantity):
return f"Charged ${quantity} in crypto"
def process_payment(payment_type, quantity):
handler = PAYMENT_HANDLERS.get(payment_type)
if handler is None:
increase ValueError(f"Unknown cost sort: {payment_type!r}")
return handler(quantity)
Have a look at what modified. The process_payment dispatcher is 4 strains, and it’ll by no means develop. Need Apple Pay? Write a brand new perform, slap @register("apple_pay") on it, put it in no matter file you want, and also you’re achieved. No central listing to edit. No merge battle. No reopening examined code. The handler sits proper subsequent to its personal key, which is strictly the place the following reader will search for it.
# Constructing a Reusable Registry Class
After getting two or three registries mendacity round, you’ll get uninterested in rewriting the identical decorator boilerplate. Wrap it in a small class and also you get collision detection, higher error messages, and a clear API without cost:
class Registry:
"""A reusable name-to-object registry."""
def __init__(self, title):
self.title = title
self._registry = {}
def register(self, key):
def decorator(obj):
if key in self._registry:
increase KeyError(
f"{key!r} already registered in {self.title!r}"
)
self._registry[key] = obj
return obj
return decorator
def get(self, key):
if key not in self._registry:
increase KeyError(
f"{key!r} not present in {self.title!r}. "
f"Out there: {listing(self._registry)}"
)
return self._registry[key]
def __contains__(self, key):
return key in self._registry
def keys(self):
return self._registry.keys()
Now use it to construct a text-processing pipeline pushed fully by config:
transforms = Registry("transforms")
@transforms.register("lowercase")
def to_lower(textual content):
return textual content.decrease()
@transforms.register("strip")
def strip_whitespace(textual content):
return textual content.strip()
@transforms.register("remove_digits")
def remove_digits(textual content):
return "".be a part of(c for c in textual content if not c.isdigit())
# The pipeline is now simply information. It might come from a YAML file,
# a CLI argument, or a database row.
pipeline = ["strip", "lowercase", "remove_digits"]
textual content = " Order #4521 CONFIRMED "
for step in pipeline:
textual content = transforms.get(step)(textual content)
print(repr(textual content))
Output:
'order # confirmed'
That is the place the sample pays for itself. The conduct of this system is now described by information — a listing of strings — not by code. Reordering the pipeline, including a step, or handing the entire thing to a non-programmer via a config file all change into trivial.
# Auto-Registering Lessons With __init_subclass__
When your registry holds courses as an alternative of features, Python has an excellent slicker trick. The __init_subclass__ hook (obtainable since Python 3.6) fires mechanically each time a subclass is outlined, so subclasses register themselves with no decorator in any respect:
class DataLoader:
_registry = {}
def __init_subclass__(cls, fmt=None, **kwargs):
tremendous().__init_subclass__(**kwargs)
if fmt:
DataLoader._registry[fmt] = cls
@classmethod
def get_loader(cls, fmt):
if fmt not in cls._registry:
increase ValueError(
f"No loader for {fmt!r}. "
f"Out there: {listing(cls._registry)}"
)
return cls._registry[fmt]
class CSVLoader(DataLoader, fmt="csv"):
def load(self, path):
return f"Loading CSV from {path}"
class JSONLoader(DataLoader, fmt="json"):
def load(self, path):
return f"Loading JSON from {path}"
class ParquetLoader(DataLoader, fmt="parquet"):
def load(self, path):
return f"Loading Parquet from {path}"
loader = DataLoader.get_loader("parquet")
print(loader.load("gross sales.parquet")) # Loading Parquet from gross sales.parquet
No decorator wherever. Subclassing DataLoader with a fmt= argument is sufficient to register the brand new class. That is how a whole lot of frameworks construct their plugin techniques underneath the hood.
# The place the Registry Sample Is Helpful in Observe
This isn’t a tutorial train. It’s the spine of instruments you already use.
- Machine studying experiment configs. Hugging Face Transformers, Detectron2, and MMDetection all use registries so you’ll be able to choose a mannequin, optimizer, or augmentation by string title in a YAML file.
build_model({"mannequin": "resnet50"})beats a largeif spine == ...block, and it lets researchers add architectures with out ever touching the coach. - File format and parser dispatch. Map extensions like
"csv","json", and"parquet"to loader courses. Supporting a brand new format turns into “write one class,” not “edit the loader.” - Net framework routing. Flask‘s
@app.route("/customers")and Click on‘s@cli.command()are registries in disguise. The decorator maps a URL or command title to the perform that handles it. - Plugin architectures. Any “drop a file on this folder and it simply works” system — whether or not pytest fixtures, Airflow operators, or serializer backends — is nearly all the time a registry amassing elements at import time.
- Occasion handlers and state machines. Map occasion names or states to handler features as an alternative of branching on them. The transition desk turns right into a readable dictionary relatively than a nest of conditionals.
# Sensible Concerns and Issues to Watch Out For
- Registration solely occurs on import. A decorator runs when Python executes the file it lives in. In case your handlers sit in
handlers/apple_pay.pyand nothing ever imports that module, the@registerdecorator by no means fires and the handler quietly goes lacking. The repair is to verify registration modules get imported — normally via an express import in a package deal’s__init__.py, or a small discovery loop withpkgutil.iter_modulesthat imports every thing in a plugin folder. - Guard in opposition to silent overwrites. With a plain dict, two elements registering the identical key clobber one another with no peep. Because the
Registryclass above exhibits, elevating on a reproduction key turns a baffling runtime bug into an apparent error at import time. - Present individuals what is on the market. At all times expose the keys with
listing(registry.keys())and put them in your error messages. “Unknown mannequin: ‘lgbm’. Out there: [‘logreg’, ‘random_forest’, ‘xgboost’]” saves much more debugging time than a nakedKeyError. - Don’t attain for it too early. A registry is overkill for 2 or three steady branches whose logic genuinely differs. If the branches share no widespread signature, or the circumstances are ranges relatively than discrete keys (
if rating > 0.9 ... elif rating > 0.5 ...), a plain conditional is clearer. The registry wins in a single particular state of affairs: you’re dispatching on a discrete key to interchangeable behaviors, and also you anticipate that set of behaviors to develop.
# Wrapping Up
The registry sample trades a rising, central, hard-to-extend if/elif/else chain for a lookup desk that elements fill in themselves. The payoff is concrete. Your dispatcher stops altering. New options present up as new recordsdata as an alternative of edits to previous ones. Habits turns into one thing you’ll be able to drive from a config. And customers of your code get an actual extension level as an alternative of a locked door.
Begin small. Subsequent time you catch your self typing a 3rd elif title == ..., cease and ask whether or not a dictionary would do. Normally it could. From there, the decorator and sophistication variations are a brief hop away.
# Earlier than
if form == "a": ...
elif form == "b": ...
elif form == "c": ...
# After
@registry.register("a")
def handle_a(): ...
Your future self, scrolling previous a four-line dispatcher as an alternative of a 200-line ladder, will thanks.
Kanwal Mehreen is a machine studying engineer and a technical author with a profound ardour for information science and the intersection of AI with medication. She co-authored the book “Maximizing Productiveness with ChatGPT”. As a Google Era Scholar 2022 for APAC, she champions range and tutorial excellence. She’s additionally acknowledged as a Teradata Range in Tech Scholar, Mitacs Globalink Analysis Scholar, and Harvard WeCode Scholar. Kanwal is an ardent advocate for change, having based FEMCodes to empower ladies in STEM fields.















