Python Development
Description
Python coding guide for EloPhanto — covers plugin development, async patterns, error handling, testing with pytest, and the BaseTool interface.
Triggers
- python
- build plugin
- create plugin
- create tool
- modify source
- pytest
- async
- pip
- pyproject
Instructions
1. Before Writing Code
- Read existing code in the target area (self_read_source or file_read).
- Match the patterns already in use — naming, error handling, imports.
- Check if something similar exists (self_list_capabilities for tools,
file_list with
*.py pattern for general code).
- Identify edge cases upfront: empty input, missing params, file not found,
permission denied, timeouts.
2. Style
- Python 3.12+ — use
str | None not Optional[str]
from __future__ import annotations at the top of every file
- Type hints on ALL function signatures (parameters AND return types)
- Import order: stdlib → third-party → project (ruff enforces this)
- Use
pathlib.Path instead of os.path
- Use f-strings for formatting
- Line length: 100 characters max
- No dead code — remove commented-out blocks and unused imports
3. EloPhanto Plugin Interface
Every tool must implement the BaseTool abstract class:
from __future__ import annotations
from typing import Any
from tools.base import BaseTool, PermissionLevel, ToolResult
class MyTool(BaseTool):
@property
def name(self) -> str:
return "my_tool" # snake_case, unique across all tools
@property
def description(self) -> str:
return "Clear, actionable description the LLM reads to decide when to use this tool."
@property
def input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"param": {"type": "string", "description": "What this does"},
},
"required": ["param"],
}
@property
def permission_level(self) -> PermissionLevel:
return PermissionLevel.MODERATE # SAFE | MODERATE | DESTRUCTIVE | CRITICAL
async def execute(self, params: dict[str, Any]) -> ToolResult:
try:
result = await do_something(params["param"])
return ToolResult(success=True, data={"output": result})
except Exception as e:
return ToolResult(success=False, error=f"Failed: {e}")
Critical rules:
- NEVER raise from
execute() — catch all exceptions, return ToolResult(success=False, error=...)
- Use
async/await for ALL I/O (file, network, subprocess)
description is what the LLM reads — write it like a help string, not a code comment
input_schema must be valid JSON Schema with descriptions per property
- Keep dependencies minimal — prefer stdlib over external packages
4. Error Handling
# Specific exceptions with informative messages
try:
data = json.loads(response)
except json.JSONDecodeError as e:
return ToolResult(success=False, error=f"Invalid JSON: {e}")
# Early returns for validation (guard clauses)
async def execute(self, params):
path = Path(params["path"])
if not path.exists():
return ToolResult(success=False, error=f"Not found: {path}")
if not path.is_file():
return ToolResult(success=False, error=f"Not a file: {path}")
# main logic after guards pass
Anti-patterns:
- Bare
except: — always catch specific exceptions
- Swallowing errors silently — always log or return the error
- Raising from execute() — the agent loop expects ToolResult, not exceptions
5. Async Patterns
import asyncio
# Subprocess with timeout
proc = await asyncio.create_subprocess_exec(
"command", "arg1",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30)
except asyncio.TimeoutError:
proc.kill()
await proc.communicate()
return ToolResult(success=False, error="Command timed out")
# Parallel operations
results = await asyncio.gather(task_a(), task_b(), return_exceptions=True)
# File I/O (sync is fine for small files in asyncio context)
content = Path("file.txt").read_text(encoding="utf-8")
6. Testing
- Framework: pytest with
pytest-asyncio (asyncio_mode="auto")
@pytest.mark.asyncio on async test functions
- Test structure: interface properties → happy path → error cases
- Run:
self_run_tests or python -m pytest tests/ -v --tb=short
- Keep tests isolated — no external service dependencies
import pytest
from plugins.my_tool.plugin import MyTool
@pytest.mark.asyncio
async def test_execute_success():
tool = MyTool()
result = await tool.execute({"param": "valid_input"})
assert result.success
assert "output" in result.data
@pytest.mark.asyncio
async def test_execute_missing_param():
tool = MyTool()
result = await tool.execute({})
assert not result.success
7. Code Review Checklist
- Security: Input validation, no credential leaks, path traversal checks
- Resources: Files/connections closed (use
with or try/finally)
- Edge cases: Empty input, missing params, timeouts, large files
- Error handling: Graceful failures, informative error messages
- Types: All signatures typed, mypy passes
- Tests: New code has test coverage
8. Tooling
- ruff for linting (rules: E, F, I, UP, B) — run with
ruff check .
- mypy for type checking (Python 3.12 target, strict=false)
- pytest for testing (asyncio_mode="auto")
- uv for package management
Verify
- The code was actually executed (or type-checked / linted as appropriate) and the command output is captured
- Dependencies and runtime versions used are pinned and recorded (e.g., requirements.txt, package.json + lockfile, .nvmrc)
- Errors or warnings emitted by the run are addressed or explicitly accepted with a reason
- New external I/O (network, filesystem, DB) has timeouts and error handling, not silent failure
- Tests for the change were run and the pass/fail count is in the transcript
- Secrets and credentials are read from env/secret store, not hard-coded, and
.env files are not committed
Notes
EloPhanto plugins live in plugins/<name>/plugin.py. They are registered in
core/registry.py and their dependencies injected in core/agent.py. Use
self_read_source to study existing tools before creating new ones.
1---2name: python3description: Python Development4---5# Python Development67## Description89Python coding guide for EloPhanto — covers plugin development, async patterns, error handling, testing with pytest, and the BaseTool interface.1011## Triggers1213- python14- build plugin15- create plugin16- create tool17- modify source18- pytest19- async20- pip21- pyproject2223## Instructions2425### 1. Before Writing Code26271. Read existing code in the target area (self_read_source or file_read).282. Match the patterns already in use — naming, error handling, imports.293. Check if something similar exists (self_list_capabilities for tools,30 file_list with `*.py` pattern for general code).314. Identify edge cases upfront: empty input, missing params, file not found,32 permission denied, timeouts.3334### 2. Style3536- Python 3.12+ — use `str | None` not `Optional[str]`37- `from __future__ import annotations` at the top of every file38- Type hints on ALL function signatures (parameters AND return types)39- Import order: stdlib → third-party → project (ruff enforces this)40- Use `pathlib.Path` instead of `os.path`41- Use f-strings for formatting42- Line length: 100 characters max43- No dead code — remove commented-out blocks and unused imports4445### 3. EloPhanto Plugin Interface4647Every tool must implement the BaseTool abstract class:4849```python50from __future__ import annotations51from typing import Any52from tools.base import BaseTool, PermissionLevel, ToolResult5354class MyTool(BaseTool):55 @property56 def name(self) -> str:57 return "my_tool" # snake_case, unique across all tools5859 @property60 def description(self) -> str:61 return "Clear, actionable description the LLM reads to decide when to use this tool."6263 @property64 def input_schema(self) -> dict[str, Any]:65 return {66 "type": "object",67 "properties": {68 "param": {"type": "string", "description": "What this does"},69 },70 "required": ["param"],71 }7273 @property74 def permission_level(self) -> PermissionLevel:75 return PermissionLevel.MODERATE # SAFE | MODERATE | DESTRUCTIVE | CRITICAL7677 async def execute(self, params: dict[str, Any]) -> ToolResult:78 try:79 result = await do_something(params["param"])80 return ToolResult(success=True, data={"output": result})81 except Exception as e:82 return ToolResult(success=False, error=f"Failed: {e}")83```8485**Critical rules:**86- NEVER raise from `execute()` — catch all exceptions, return `ToolResult(success=False, error=...)`87- Use `async`/`await` for ALL I/O (file, network, subprocess)88- `description` is what the LLM reads — write it like a help string, not a code comment89- `input_schema` must be valid JSON Schema with descriptions per property90- Keep dependencies minimal — prefer stdlib over external packages9192### 4. Error Handling9394```python95# Specific exceptions with informative messages96try:97 data = json.loads(response)98except json.JSONDecodeError as e:99 return ToolResult(success=False, error=f"Invalid JSON: {e}")100101# Early returns for validation (guard clauses)102async def execute(self, params):103 path = Path(params["path"])104 if not path.exists():105 return ToolResult(success=False, error=f"Not found: {path}")106 if not path.is_file():107 return ToolResult(success=False, error=f"Not a file: {path}")108 # main logic after guards pass109```110111**Anti-patterns:**112- Bare `except:` — always catch specific exceptions113- Swallowing errors silently — always log or return the error114- Raising from execute() — the agent loop expects ToolResult, not exceptions115116### 5. Async Patterns117118```python119import asyncio120121# Subprocess with timeout122proc = await asyncio.create_subprocess_exec(123 "command", "arg1",124 stdout=asyncio.subprocess.PIPE,125 stderr=asyncio.subprocess.PIPE,126)127try:128 stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30)129except asyncio.TimeoutError:130 proc.kill()131 await proc.communicate()132 return ToolResult(success=False, error="Command timed out")133134# Parallel operations135results = await asyncio.gather(task_a(), task_b(), return_exceptions=True)136137# File I/O (sync is fine for small files in asyncio context)138content = Path("file.txt").read_text(encoding="utf-8")139```140141### 6. Testing142143- Framework: pytest with `pytest-asyncio` (asyncio_mode="auto")144- `@pytest.mark.asyncio` on async test functions145- Test structure: interface properties → happy path → error cases146- Run: `self_run_tests` or `python -m pytest tests/ -v --tb=short`147- Keep tests isolated — no external service dependencies148149```python150import pytest151from plugins.my_tool.plugin import MyTool152153@pytest.mark.asyncio154async def test_execute_success():155 tool = MyTool()156 result = await tool.execute({"param": "valid_input"})157 assert result.success158 assert "output" in result.data159160@pytest.mark.asyncio161async def test_execute_missing_param():162 tool = MyTool()163 result = await tool.execute({})164 assert not result.success165```166167### 7. Code Review Checklist168169- **Security**: Input validation, no credential leaks, path traversal checks170- **Resources**: Files/connections closed (use `with` or try/finally)171- **Edge cases**: Empty input, missing params, timeouts, large files172- **Error handling**: Graceful failures, informative error messages173- **Types**: All signatures typed, mypy passes174- **Tests**: New code has test coverage175176### 8. Tooling177178- **ruff** for linting (rules: E, F, I, UP, B) — run with `ruff check .`179- **mypy** for type checking (Python 3.12 target, strict=false)180- **pytest** for testing (asyncio_mode="auto")181- **uv** for package management182183## Verify184185- The code was actually executed (or type-checked / linted as appropriate) and the command output is captured186- Dependencies and runtime versions used are pinned and recorded (e.g., requirements.txt, package.json + lockfile, .nvmrc)187- Errors or warnings emitted by the run are addressed or explicitly accepted with a reason188- New external I/O (network, filesystem, DB) has timeouts and error handling, not silent failure189- Tests for the change were run and the pass/fail count is in the transcript190- Secrets and credentials are read from env/secret store, not hard-coded, and `.env` files are not committed191192## Notes193194EloPhanto plugins live in `plugins/<name>/plugin.py`. They are registered in195`core/registry.py` and their dependencies injected in `core/agent.py`. Use196self_read_source to study existing tools before creating new ones.