Project: A Plugin System with Patterns
Build a registry of export strategies, protect the contract with a Protocol, and add new formats without editing the calling code.
By the end, you’ll be able to…
- Define a narrow plugin contract with a Protocol.
- Implement a registry that rejects duplicate and unknown names.
- Use the strategy pattern to swap output formats at runtime.
- Test plugins in isolation and load third-party entry points deliberately.
01Define the smallest useful contract
A plugin system begins with a contract, not a directory of files. The caller needs a name for lookup and an export method that turns rows into text. A Protocol captures exactly that shape. Implementations do not inherit from a framework base class, so a test double or a third-party class can participate without modification.
Keep the contract narrow. If the caller only needs export, do not require a configure method, a version attribute, and a lifecycle hook. Every required member is a cost paid by every plugin author. Add a member when a real caller needs it, and document the meaning of the input and the returned text.
02Use a registry as a factory
A registry maps a stable name to an implementation. Registration validates the name and rejects duplicates so two plugins cannot silently shadow each other. Lookup returns the Exporter contract, which means the caller never branches on a concrete class. Adding a format becomes a registration, not an edit to an if/elif chain.
Keep registration explicit. Import-time decorators are convenient but can make behavior depend on import order and can run side effects merely by importing a module. An explicit register call in a composition root is easier to test and easier to reason about. If you later load entry points, the registry is still the single place that enforces uniqueness.
03Swap strategies at the boundary
The strategy pattern moves a family of algorithms behind one interface. CSV and JSON exporters are strategies: the report builder selects one by name and calls export without knowing how the text is produced. The selection can come from a CLI flag, a configuration file, or an HTTP parameter.
Keep the strategies independent. A CSV exporter should not import the JSON exporter, and neither should know about the registry. Shared helpers such as escaping belong in a small module both can use. Independence makes each strategy testable with a literal list of rows and keeps a change to one format from breaking another.
04Test the boundary and load plugins deliberately
Test each strategy with empty input, one row, and a value containing a comma or quote. Test the registry with a duplicate name, an unknown name, and a blank name. These tests protect the contract that every future plugin depends on. They do not need a real report file or a CLI.
Third-party plugins can be discovered through importlib.metadata entry points. Treat discovery as untrusted input: load only a documented group, catch import failures, validate the name, and report which plugin failed. A plugin runs with your process's permissions, so installation is a supply-chain decision, not a casual convenience.
The ideas worth keeping.
- A narrow Protocol lets unrelated classes satisfy the same extension point.
- A registry centralizes lookup and makes duplicate names an explicit error.
- Strategies keep formats independent; loading plugins is a security decision.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Add a Markdown exporter
Implement MarkdownExporter with name = 'markdown' and a table header, separator, and one row per record. Register it beside CSV and JSON without changing the registry.
Explore a worked solution
class MarkdownExporter: name = "markdown" def export(self, rows): if not rows: return "" headers = list(rows[0]) lines = ["| " + " | ".join(headers) + " |"] lines.append("| " + " | ".join("---" for _ in headers) + " |") for row in rows: lines.append("| " + " | ".join(row[key] for key in headers) + " |") return "\n".join(lines)rows = [{"id": "1", "title": "Read"}]print(MarkdownExporter().export(rows))The new strategy satisfies the same contract and needs no registry change. A registry that required edits for every format would be a factory with extra steps, not an open extension point.
Reject duplicate registrations
Create a registry with a register method that raises ValueError when a name already exists. Verify that the first exporter is still the one returned.
Explore a worked solution
class Registry: def __init__(self): self.items = {} def register(self, exporter): if exporter.name in self.items: raise ValueError(f"duplicate exporter: {exporter.name}") self.items[exporter.name] = exporterclass First: name = "csv"class Second: name = "csv"registry = Registry()first = First()registry.register(first)try: registry.register(Second())except ValueError as exc: print(exc)print(registry.items["csv"] is first)Rejecting the duplicate keeps the registered object stable. Silently replacing it would make behavior depend on import or registration order.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Add a Markdown exporter
Implement MarkdownExporter with name = 'markdown' and a table header, separator, and one row per record. Register it beside CSV and JSON without changing the registry.
MarkdownExporter().export(rows)
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
Pause. Put the pieces together.
You’ve reached the end of Design Patterns in Python. Before moving on, use this task to check what you can do independently.
Refactor existing code to use appropriate design patterns and explain how the pattern improves maintainability and flexibility.
Try it in your own environment and write down the result. This is a self-check; reading completion does not evaluate your code.
Review this moduleOne lesson closer.
Reading is a start. Trying it yourself makes the connection.