LESSON 60 · ADVANCED4 min read

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.

Python 3.12+Free to readModule 13 of 19
YOUR NEXT SMALL WINS

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.

contract.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

registry.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

strategies.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

entry_points.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
LET IT CLICK

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.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

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.

01

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.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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.

02

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.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What does a Protocol require from an implementation?
02Why reject a duplicate exporter name?
03What is the main risk of loading third-party plugins?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

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.

EXPECTED OUTPUT
MarkdownExporter().export(rows)
main.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

MODULE 13 · PRACTICAL SELF-CHECK

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 module

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.