Skip to content

Code Guidelines

These guidelines cover coding conventions for the Python, TypeScript, and C# Prompty runtimes. Following them keeps the codebase consistent and makes PRs easier to review.

runtime/
├── python/prompty/ # Python runtime (pip: prompty)
│ ├── prompty/ # Package source
│ └── tests/ # Unit + integration tests
├── typescript/ # TypeScript monorepo
│ └── packages/
│ ├── core/ # @prompty/core
│ ├── openai/ # @prompty/openai
│ ├── foundry/ # @prompty/foundry
│ └── anthropic/ # @prompty/anthropic
└── csharp/ # C# solution
├── Prompty.Core/ # Core runtime
├── Prompty.OpenAI/ # OpenAI provider
├── Prompty.Foundry/ # Foundry provider
├── Prompty.Anthropic/ # Anthropic provider
└── Prompty.*.Tests/ # Test projects

  • Python ≥ 3.11 — use modern syntax (X | Y unions, match/case, f-strings).
  • Use uv exclusively for environment and package management. Never use pip or python -m venv directly.
Terminal window
cd runtime/python/prompty
uv venv
uv pip install -e ".[dev,all]"

We use Ruff for both linting and formatting. Configuration is in pyproject.toml:

SettingValue
Line length120
Targetpy311
RulesE, F, I (isort), UP (pyupgrade)
IgnoredE501 (line length — handled by formatter)
Terminal window
# Check for lint issues
uv run ruff check .
# Auto-format
uv run ruff format .
  • from __future__ import annotations at the top of every module.
  • Type hints everywhere — all function signatures, return types, and class attributes.
  • Docstrings — every module gets a module-level docstring. Public classes and functions get docstrings too.
  • __all__ — define in modules that export public API.
  • Private names — prefix internal helpers with _ (e.g., _message_to_wire).
  • No bare except: — always catch specific exceptions (except Exception: at minimum).
  • ConstantsUPPER_SNAKE_CASE at module level.
# Internal code → relative imports
from .types import Message
from ..tracing.tracer import trace
# Tests → absolute imports
from prompty.core.types import Message
from prompty.tracing.tracer import Tracer
# Optional dependencies → lazy imports inside functions
def execute(self, agent, messages):
from openai import OpenAI # only imported when actually called

Import sort order is enforced by Ruff’s I (isort) rule: stdlib → third-party → local, alphabetized within each group.

Every public pipeline function has a sync and async variant:

# Sync
messages = prepare("prompt.prompty", inputs={"name": "Jane"})
# Async
messages = await prepare_async("prompt.prompty", inputs={"name": "Jane"})

Protocol classes define both render() / render_async(), parse() / parse_async(), etc.

  • Framework: pytest + pytest-asyncio
  • Run unit tests: uv run pytest tests/ -q
  • Run integration tests: pytest tests/integration/ -v -o "addopts="
  • Mock external services — never make real API calls in unit tests.
  • Async tests use @pytest.mark.asyncio with asyncio_mode = "strict".

Integration tests require API keys in a .env file and are excluded by default via the integration marker.

  • Required deps go in [project.dependencies].
  • Optional deps go in [project.optional-dependencies] — never add optional deps to the required list.
  • Entry points in pyproject.toml register invokers for the plugin discovery system.
  • After changing entry points or deps, reinstall: uv pip install -e ".[dev,all]".
Error TypeWhen to Use
InvokerErrorMissing invoker registrations
ValueErrorInvalid frontmatter, missing env vars without defaults
FileNotFoundErrorMissing .prompty files or ${file:...} references
DeprecationWarningLegacy property migrations (via warnings.warn)

  • Node.js ≥ 18
  • Standard npm workspace monorepo
Terminal window
cd runtime/typescript
npm install
npm run build

Each package follows an identical layout:

packages/<name>/
├── src/ # Source code
├── tests/ # Vitest test files
├── dist/ # Build output (ESM + CJS)
├── package.json
├── tsconfig.json
└── tsup.config.ts

Packages extend tsconfig.base.json:

SettingValue
TargetES2022
ModuleES2022
Module Resolutionbundler
Stricttrue
Terminal window
# Build all packages
npm run build
# Type-check (used as lint)
tsc --noEmit
# Run tests
npm run test

The project uses tsup for bundling — dual ESM + CJS output with declaration files, targeting node18.

  • Named exports only — no default exports.
  • Barrel exports — each directory has an index.ts that re-exports public API.
  • Explicit .js extensions in relative imports (required for ESM compatibility).
  • Strong typingstrict: true in tsconfig, avoid any where possible.
// ✅ Named export with .js extension
export { PromptyChatParser } from './parsers/prompty.js';
// ❌ Don't use default exports
export default class MyParser { ... }
  • Framework: Vitest
  • Test files: tests/**/*.test.ts
  • Run: npm run test (across all workspaces)
  • Tests use Vitest’s built-in mocking (vi.fn(), vi.mock()).
  • Coverage is collected via the v8 provider.
  1. Create packages/<provider>/ following the existing package structure.
  2. Add src/index.ts with named exports for your executor and processor.
  3. Add a tsup.config.ts and vitest.config.ts matching the existing packages.
  4. Register the package in the root package.json workspaces array.

  • .NET 9 — use the latest SDK features and C# 13 syntax.
  • Solution file at runtime/csharp/prompty.sln.
Terminal window
cd runtime/csharp
dotnet build
dotnet test
ProjectDescription
Prompty.CoreCore runtime — loader, pipeline, protocols, types
Prompty.OpenAIOpenAI provider (executor + processor)
Prompty.FoundryAzure AI Foundry provider
Prompty.AnthropicAnthropic Claude provider
Prompty.Core.TestsCore unit tests (xUnit)
Prompty.Providers.TestsProvider integration tests (xUnit)
Terminal window
# Build the solution
dotnet build
# Run all tests
dotnet test
# Check formatting (CI uses --verify-no-changes)
dotnet format --verify-no-changes
# Auto-format
dotnet format
  • Fluent builder pattern for provider registration: new PromptyBuilder().AddOpenAI().
  • Extension methods for provider registration — each provider package exposes AddXxx() extensions on PromptyBuilder.
  • Nullable reference types enabled — annotate all public APIs.
  • IAsyncEnumerable<T> for streaming responses.
  • File-scoped namespaces — one namespace per file, no extra nesting.
// ✅ Fluent builder registration
var builder = new PromptyBuilder()
.AddOpenAI()
.AddFoundry();
// ✅ Extension method pattern
public static class OpenAIExtensions
{
public static PromptyBuilder AddOpenAI(this PromptyBuilder builder)
{
builder.RegisterExecutor("openai", new OpenAIExecutor());
builder.RegisterProcessor("openai", new OpenAIProcessor());
return builder;
}
}
  • Framework: xUnit + FluentAssertions
  • Run tests: dotnet test
  • Tests use xUnit’s [Fact] and [Theory] attributes.
  • Mock external services with NSubstitute — never make real API calls in unit tests.
  • Integration tests use the [Trait("Category", "Integration")] attribute and are excluded by default.

NuGet packages are published via CI. Version numbers follow the solution-level Directory.Build.props.


  • Use clear, descriptive commit messages.
  • Include a Co-authored-by trailer when pair-programming or using AI assistance.
  • Run all linters and tests locally before opening a PR.
  • Keep PRs focused — one feature or fix per PR.
  • Update documentation if your change affects user-facing behavior.
  • Add tests for new functionality.
  1. Create prompty/providers/<name>/ with __init__.py, executor.py, processor.py.
  2. Implement the ExecutorProtocol and ProcessorProtocol interfaces.
  3. Register entry points in pyproject.toml:
[project.entry-points."prompty.executors"]
my_provider = "prompty.providers.my_provider.executor:MyExecutor"
[project.entry-points."prompty.processors"]
my_provider = "prompty.providers.my_provider.processor:MyProcessor"
  1. Add optional dependency: my_provider = ["my-provider-sdk"]
  2. Reinstall: uv pip install -e ".[dev,all]"

Want to Contribute To the Project?