I'm going through a 16-week Agentic AI syllabus right now, and Week 1 is "Python for Agentic Systems" — OOP, typing, decorators. Instead of just reading about it, I built a small CLI toolkit that mimics how real agent frameworks register and run tools.
This post is about one piece of it: how a global tool registry works using __init_subclass__, and why agent frameworks need this pattern at all.
Repo's at the bottom. Code below is real, from the actual project — not pseudocode.
The problem
Agent frameworks (LangGraph, CrewAI, PydanticAI) all need the same thing: a way for an LLM or planner to discover "what tools exist" without you manually maintaining a list somewhere.
The naive way is a dict you update by hand:
TOOLS = {
"search": SearchTool,
"summarize": SummarizeTool,
}
This works until you forget to add an entry. Then your planner silently can't find a tool that exists in your codebase.
The fix: self-registering classes
Here's the actual registry from my project:
class ToolRegistry:
__slots__ = ()
_tools: dict[str, type[Any]] = {}
@classmethod
def register(cls, name: str, tool_cls: type[Any]) -> None:
existing = cls._tools.get(name)
if existing is not None and existing is not tool_cls:
raise DuplicateToolError(
f"tool name {name!r} is already registered by {existing.__name__}"
)
cls._tools[name] = tool_cls
And the part that actually calls register() — __init_subclass__ on the base class:
class BaseTool(LoggingMixin, RetryMixin, MetricsMixin, ABC):
def __init_subclass__(
cls,
*,
tool_name: str | None = None,
description: str = "",
streamable: bool = False,
abstract: bool = False,
**kwargs: Any,
) -> None:
super().__init_subclass__(**kwargs)
if abstract:
return
if tool_name is None:
raise TypeError(f"{cls.__name__} must define tool_name='...'")
cls._tool_name = tool_name.strip().lower()
cls.description = description.strip()
cls._streamable = streamable
ToolRegistry.register(cls._tool_name, cls)
__init_subclass__ fires automatically the moment Python defines a subclass — before you ever instantiate it. So a tool just declares itself:
class SearchTool(
BaseTool,
tool_name="search",
description="Searches a small in-memory knowledge base.",
streamable=True,
):
def execute(self, context: ToolContext) -> str:
...
The moment this class body is parsed, SearchTool is in the registry. No manual list. No import-time side-effect hacks. Forget tool_name= and you get a TypeError immediately — not a silent miss three files away.
Why this matters for agent frameworks specifically
Once tools self-register, a CLI (or a planner LLM) can just ask "what do you have":
def _list_tools() -> None:
for name, tool_cls in ToolRegistry.items():
tool = tool_cls()
print(f"{name:<12} {tool.metadata['description']}")
$ python main.py list-tools
search Searches a small in-memory knowledge base and returns ranked notes.
summarize Creates a compact extractive summary of user-provided text.
translate Translates common demo phrases to Spanish or Urdu using a local lexicon.
This is structurally the same problem LangGraph and CrewAI solve with their own tool-discovery mechanisms. Different implementation, same underlying need: a single source of truth that updates itself.
What's also in the project
This registry is one piece. The same codebase has:
-
Descriptors (
ValidatedField,IdentifierField,IntegerRange) validatingToolConfigat assignment time -
Mixins + MRO —
BaseTool(LoggingMixin, RetryMixin, MetricsMixin, ABC)composes logging, retries, and metrics without inheritance spaghetti -
ParamSpec-based decorators (@log_execution,@measure_time) that wrap methods without breaking their signatures for mypy -
Generator-based streaming —
stream()yields tokens instead of faking it with string slicing
I'll cover each of these in upcoming posts as I move through the syllabus.
Try it
git clone https://github.com/Sajid0875/agentic-systems-bootcamp
cd agent-ready-cli-toolkit
python main.py list-tools
python main.py describe search
python main.py run summarize "Agent frameworks register tools and stream results." --stream
If you're also learning agentic systems and want to compare notes on how different frameworks (CrewAI, PydanticAI, LangGraph) handle tool registration internally, drop it in the comments — genuinely curious how close/far off this mental model is from the real implementations.
Top comments (0)