Refactoring Python Shells: From Messy Classes to Plugin Architectures
Overview: The Power of Clean Separation
Refactoring a command-line application involves more than just fixing bugs; it is about establishing a sustainable architecture. When a project grows, monolithic classes and tangled dependencies often lead to "code rot." By shifting toward a functional, plugin-based approach, you create a system where features like

Prerequisites
To follow this walkthrough, you should have a solid grasp of
Key Libraries & Tools
- Typing: Used for
CallableandListhints to ensure the shell's command registry remains type-safe. - Hashlib: The standard library for implementing secure hashing algorithms.
- GitHub Copilot: An AI-assisted tool used to accelerate the creation of repetitive dictionary mappings.
Code Walkthrough: Centralizing Logic
1. Consolidating Algorithms
Initially, algorithms were scattered across lists and dictionaries. We simplify this by using single-source-of-truth dictionaries where keys are lowercase strings and values are the function references.
# algorithms.py
enconding_algorithms = {
"base64": base64.b64encode,
"base16": base16.b16encode,
}
2. Building the API Wrapper
Instead of the shell calling algorithms directly, we use a middle layer. This handles input cleaning like lower() and strip() to prevent user errors from crashing the program.
def encode_text(text: str, algo_name: str) -> bytes:
func = encoding_algorithms.get(algo_name.lower().strip())
return func(text.encode())
3. The Plugin Mechanism
We move away from a hard-coded "Interface" class. Instead, we create a core registry where we can register new commands on the fly. This decouples the shell's execution loop from the specific logic of each command.
# core.py
commands: dict[str, Callable] = {}
def add_command(key: str, func: Callable):
commands[key] = func
def run_shell():
while True:
user_input = input("> ").split()
# Logic to look up key in commands and execute
Syntax Notes: Callables and Dictionaries
Note the use of the Callable[[List[str]], None] type hint. This explicitly tells the developer that any function registered to the shell must accept a list of strings (the arguments) and return nothing. Using dictionaries to map strings to functions is a classic if/elif chains.
Practical Examples
This architecture is ideal for building extensible developer tools. For instance, if you wanted to add a "Network Scan" command, you wouldn't touch the shell's core loop. You would simply write the function and call add_command("scan", scan_func). It effectively turns your application into a platform.
Tips & Gotchas
- Input Sanitization: Always strip and lowercase user-provided command names to ensure "Hash" and "hash" behave identically.
- Argument Validation: Check the length of the
argslist inside the command function itself. If it's wrong, print the documentation immediately to guide the user.

Fancy watching it?
Watch the full video and context