Implementing a Dynamic Plugin Architecture in Python
Overview
A
Prerequisites
To follow this guide, you should be comfortable with:
- Basic Python syntax and Object-Oriented Programming (OOP).
- The concept of JSONfor data storage.
- Familiarity with Python's
typingmodule, specificallystructural typing.

Key Libraries & Tools
- importlib: A built-in Python library used to import modules programmatically.
- typing.Protocol: Used for structural typing to define an interface that plugins must adhere to.
- dataclasses: Simplifies the creation of classes that primarily store data.
Code Walkthrough
1. Defining the Interface
We start by defining what a "character" looks like using a make_noise.
from typing import Protocol
class GameCharacter(Protocol):
def make_noise(self) -> None:
...
2. Building the Factory
The factory acts as a registry. It maintains a dictionary mapping string keys (from our JSON level definition) to creation functions.
from typing import Callable, Any
character_creation_funcs: dict[str, Callable[..., GameCharacter]] = {}
def register(character_type: str, creation_func: Callable[..., GameCharacter]):
character_creation_funcs[character_type] = creation_func
def create(arguments: dict[str, Any]) -> GameCharacter:
args_copy = arguments.copy()
char_type = args_copy.pop("type")
try:
creation_func = character_creation_funcs[char_type]
return creation_func(**args_copy)
except KeyError:
raise ValueError(f"Unknown character type {char_type}")
3. The Dynamic Loader
This is the heart of the plugin system. It uses initialize function.
import importlib
def load_plugins(plugins: list[str]) -> None:
for plugin_name in plugins:
module = importlib.import_module(plugin_name)
module.initialize()
4. Creating a Plugin
A plugin is just a separate Python file. For example, plugins/bard.py defines a new class and registers it back to the core factory.
from dataclasses import dataclass
from game import factory
@dataclass
class Bard:
name: str
instrument: str = "flute"
def make_noise(self) -> None:
print(f"{self.name} plays the {self.instrument}!")
def initialize():
factory.register("bard", Bard)
Syntax Notes
We use structural typing via typing.Protocol. Unlike traditional inheritance, a class doesn't need to explicitly inherit from GameCharacter. As long as it implements make_noise, Python treats it as a valid implementation. We also utilize **kwargs unpacking in the factory's create method to pass JSON data directly into class constructors.
Practical Examples
- Game Modding: Allow players to drop a
.pyfile into a folder to add custom items. - Data Pipelines: Add support for new file formats (CSV, Parquet, Avro) by creating reader plugins.
- CLI Tools: Let users add custom commands to a central utility script without changing the core binary.
Tips & Gotchas
Always use a try-except block when accessing the factory dictionary to provide clear error messages for missing types. When popping the type key from arguments, make a copy of the dictionary first to avoid side effects that might break other parts of your application.

Fancy watching it?
Watch the full video and context