DEV Community

Shubham Anandani
Shubham Anandani

Posted on Originally published at imshubh.com on

Designing for Other People’s Code: A Plugin API That Welcomes Contributions

The best way to attract contributions is to make the path small, clear, and safe. Big, powerful extension APIs are intimidating and fragile. A plugin interface is easier to adopt when it has a small typed contract, a complete example, and clear isolation boundaries for contributed code.

The failure mode

You want contributions, so you design a rich, flexible plugin API. The first contributor struggles to understand how the parts fit together. Another contributor introduces side effects that break core behavior. You spend time triaging issues rather than merging useful additions. The fix is to narrow the contract and improve examples.

What I learned

Keep the interface tiny

Use a single method with a small, typed result. Avoid optional hooks and deep callback trees. If you need flexibility, let plugins implement it internally. Keep core expectations documented and enforced with tests.

Illustrative plugin contract

from dataclasses import dataclass
from typing import Literal, Protocol

@dataclass(frozen=True)
class EvidenceResult:
    status: Literal["present", "missing", "unverifiable"]
    note: str = ""

class EvidenceSource(Protocol):
    name: str
    def evaluate(self, profile: dict[str, object]) -> EvidenceResult: ...
Enter fullscreen mode Exit fullscreen mode

Documentation-by-example beats long narratives

Write a short “Adding a plugin” doc that starts with a complete code example and a test. Follow with two variations that show common patterns (e.g., reading a field, combining small signals). Keep the words sparse and the code complete.

Sandboxing contributed code: practical guardrails

Run plugins within constraints:

  • Set a timeout per plugin evaluation.
  • Limit memory-intensive operations or external calls in the demo environment.
  • Validate types and restrict dangerous imports in plugin modules if possible.

A thread timeout is not a security sandbox: a host process cannot safely terminate arbitrary plugin code. For untrusted extensions, use a separate process or container with resource limits and restricted credentials, or load only trusted plugins.

Versioning and compatibility checks (keep them light)

Require plugins to declare supported interface versions and run contract tests. Keep the declaration simple, and make the CI failure message actionable (e.g., “Plugin supports 1.1.0; core is 1.2.0 — update SUPPORTED_VERSIONS”).

Golden paths and anti-patterns

Show five “golden path” examples contributors should follow:

  1. Pure function: Read inputs, return EvidenceResult, no side effects.
  2. Lightweight computation: Avoid heavy loops or large allocations.
  3. Deterministic outputs: No random behavior or time-dependent outputs.
  4. Typed result: Always return one of the three states with a note.
  5. No external calls: For demo simplicity, avoid network calls.

If you allow external calls, define strict limits and document them clearly.

Error messages designed for contributors

When a plugin fails a contract test, the message should name the plugin, the failing expectation, and the fix. Prefer helpful failures over terse stack traces. Provide a “common issues” page with targeted guidance.

“Hello world” PR built into the docs

Include a numbered guide that results in a merged PR:

  1. Create the plugin file.
  2. Add a minimal test.
  3. Run pytest -q.
  4. Open a PR with the template.
  5. Review the expected CI jobs and how to interpret them.
  6. Review the merge expectations.

Trade-offs and failure modes

Trade-offs

  • Small interface vs expressiveness: Narrow contracts limit advanced features; they also reduce fragility and cognitive load. For small projects, this trade-off is worth it.
  • Sandboxing vs performance: Timeouts and thread isolation add overhead. Keep limits modest and configurable.
  • Documentation-by-example vs thorough reference: Examples speed onboarding; deep reference can come later if demand arises.

Failure modes

  • “Stealth side effects”: Plugins write to global state or files unexpectedly. Solution: sandbox and contract tests; make side-effect-free behavior a rule.
  • “Doc drift”: Examples fall behind reality. Solution: run example snippets in CI and fail when they don’t compile.
  • “Silent timeouts”: Sandboxing hides errors without surfacing enough context. Solution: attach structured error notes to EvidenceResult.

Try it yourself

  1. Shrink your plugin interface to a single method that returns a small typed object.
  2. Add a sandbox wrapper with timeouts and typed error handling. Run plugins through it in CI contract tests.
  3. Write a “hello world plugin” guide with a copy-paste example and a minimal test, then merge a sample PR to validate the path.

Further reading

Top comments (0)