Define contracts. Verify providers. Catch breaking changes before they ship.
Quick Start | CLI | Matchers | API | Architecture
Microservices break in production when providers change their APIs without telling consumers. The consumer expects GET /users/1 to return { id, name, email } -- the provider ships a rename from name to full_name and three downstream services crash at 2 AM.
Existing contract testing tools solve this -- but they bring heavyweight infrastructure with them. Pact JVM needs a broker server. Spring Cloud Contract requires a JVM toolchain. Both demand CI/CD plumbing that takes longer to set up than the contracts themselves.
pactship is a zero-infrastructure, file-based contract testing tool for Python. Write contracts in YAML or JSON, verify them against live providers with async HTTP, diff versions to detect breaking changes, and store everything locally -- no broker server needed, no background processes, no Docker containers.
- No infrastructure -- contracts live as files in your repo, verified locally or in CI
- Fluent Python DSL -- build contracts programmatically with type-checked builders
- 16 matcher types -- from exact match to regex, UUID, email, ISO dates, nullable, and range
- Breaking change detection -- diff two contract versions with breaking/non-breaking classification
- Python 3.10+
- Dependencies:
click,pyyaml,jsonschema,rich,httpx
pip install pactshipconsumer: order-service
provider: user-api
interactions:
- description: Get user by ID
request:
method: GET
path: /users/1
response:
status: 200
body:
id: 1
name: Alice
email: alice@example.comfrom pactship import ContractBuilder, InteractionBuilder
from pactship import like, email_match, integer_match
contract = (
ContractBuilder("order-service", "user-api")
.add_interaction(
InteractionBuilder("Get user by ID")
.given("user 1 exists")
.with_request("GET", "/users/1")
.will_respond_with(200)
.with_response_body(
{"id": 1, "name": "Alice", "email": "alice@example.com"},
matchers={
"body.id": integer_match(),
"body.name": like("string"),
"body.email": email_match(),
},
)
.build()
)
.build()
)pactship verify contract.yaml http://localhost:8080pactship diff old-contract.yaml new-contract.yamlpactship publish contract.yaml --broker-dir .pactship
pactship list --broker-dir .pactshipDefine Verify Diff Report
────────── ────────── ────────── ──────────
YAML/JSON → Provider → Version A → Breaking
or DSL Verification vs B changes
(async HTTP) classified
- Define -- write contracts as YAML/JSON files or build them with the fluent Python DSL
- Verify -- run contracts against a live provider using async HTTP via
httpx - Diff -- compare two contract versions to detect breaking vs non-breaking changes
- Report -- get results in JSON, JUnit XML, Markdown, or TAP format
- Fluent DSL --
ContractBuilderandInteractionBuilderwith method chaining - YAML and JSON -- read and write contracts in both formats via
load_contract/save_contract - Provider states -- define preconditions with
.given("user 1 exists") - Request/response specs -- method, path, headers, query params, body, status code
- Type matching --
like("string")matches any string,integer_match()matches any int - Regex patterns --
regex(r"\d{3}-\d{4}")for custom format validation - Structural matchers --
array_like(min_len),each_like(example)for arrays - Domain matchers --
email_match(),uuid_match(),iso_date(),iso_datetime() - Constraint matchers --
range_match(0, 100),any_of(["a", "b"]),nullable("string")
- Async HTTP verification -- verify contracts against running providers using
httpx - Mock provider -- in-process mock for consumer-side testing without a real server
- Request matching -- method, path, headers, query params validated against spec
- Matcher evaluation -- response body verified against all declared matchers
- Provider state setup -- optional setup URL for test data preparation
- Timeout configuration -- per-verification timeout control
- Contract diffing --
diff_contracts(old, new)returns a structured diff report - Change classification -- each change tagged as
breakingornon-breaking - Interaction-level diff -- detects added, removed, and modified interactions
- Field-level diff -- tracks changes to individual request/response fields
- Filesystem-based storage -- contracts stored as files in a configurable directory
- Versioning -- publish contracts with version numbers, retrieve specific versions
- Verification history -- track which provider versions were verified against which contracts
- No server needed -- everything runs locally, works offline, no network dependency
- REST best practices -- validate path naming, HTTP method usage, status codes
- Custom lint rules -- extensible linting with severity-based issue reporting
- Pre-publish validation -- catch contract quality issues before sharing
- OpenAPI 3.x conversion -- convert OpenAPI specs to pactship contracts automatically
- Path and method extraction -- generates interactions from OpenAPI path definitions
- Response schema mapping -- maps OpenAPI response schemas to pactship response specs
- CRUD generator --
generate_crud_contract()creates full CRUD contracts from resource specs - Endpoint generator --
generate_from_endpoints()builds contracts from endpoint definitions - Customizable templates -- configure generated interactions per HTTP method
- Dependency visualization -- build service dependency graphs from contract sets
- Mermaid diagram output -- generate Mermaid diagrams for documentation
- Cycle detection -- identify circular dependencies between services
- Version tracking --
CompatibilityMatrixtracks which consumer/provider versions work together - Matrix queries -- check compatibility between specific version pairs
- History management -- add, query, and export compatibility records
- JSON reports -- structured verification results as JSON
- JUnit XML -- integrate with CI systems expecting JUnit format
- Markdown reports -- human-readable reports for PR comments
- TAP output -- Test Anything Protocol for pipeline integration
- File-based config --
.pactship.yamlor.pactship.jsonproject configuration - Environment variables --
PACTSHIP_BROKER_DIR,PACTSHIP_TIMEOUT, etc. - Priority ordering -- env vars override file config, file config overrides defaults
- Method distribution -- analyze HTTP method usage across contracts
- Path coverage -- track which API paths are covered by contracts
- Complexity metrics -- measure contract complexity and matcher density
- Before/after verification -- run custom logic around verification cycles
- Setup/teardown -- provider state preparation and cleanup
- Hook registration -- register hooks via the
HookRegistry
- Path rewriting -- transform contract paths for different environments
- Header injection -- add/modify headers across all interactions
- Body transformation -- apply transforms to request/response bodies
- Interaction filters -- filter by HTTP method, path pattern, or description
- Tag-based filtering -- filter contracts by metadata tags
- Composable filters -- combine multiple filters with AND/OR logic
# Validate a contract file
pactship validate contract.yaml
# Verify against a live provider
pactship verify contract.yaml http://localhost:8080 \
--timeout 30 \
--header "Authorization:Bearer token" \
--setup-url http://localhost:8080/_setup \
--output report.json
# Diff two contract versions
pactship diff v1/contract.yaml v2/contract.yaml
# Publish to local broker
pactship publish contract.yaml \
--broker-dir .pactship \
--version 1.0.0 \
--tag production
# List contracts in broker
pactship list --broker-dir .pactship
# Convert between formats
pactship convert contract.yaml contract.json| Command | Description |
|---|---|
pactship validate <file> |
Validate contract file syntax and structure |
pactship verify <file> <url> |
Verify contract against a running provider |
pactship diff <old> <new> |
Compare two contract versions for breaking changes |
pactship publish <file> |
Publish contract to local filesystem broker |
pactship list |
List all contracts stored in the broker |
pactship convert <in> <out> |
Convert between YAML and JSON formats |
| Matcher | Description | Example |
|---|---|---|
exact(value) |
Exact value match | exact("hello") |
like(type) |
Type-based match | like("string") |
regex(pattern) |
Regex pattern match | regex(r"\d{3}-\d{4}") |
range_match(min, max) |
Numeric range constraint | range_match(0, 100) |
array_like(min_len) |
Array with minimum length | array_like(1) |
each_like(example) |
Each element matches structure | each_like({"id": 0}) |
any_of(values) |
One of allowed values | any_of(["a", "b"]) |
nullable(type) |
Null or specified type | nullable("string") |
iso_date() |
ISO 8601 date string | iso_date() |
iso_datetime() |
ISO 8601 datetime string | iso_datetime() |
uuid_match() |
UUID v4 format | uuid_match() |
email_match() |
Email address format | email_match() |
integer_match() |
Integer value | integer_match() |
decimal_match() |
Decimal number | decimal_match() |
boolean_match() |
Boolean value | boolean_match() |
string_match() |
String value | string_match() |
from pactship import ContractBuilder, InteractionBuilder
from pactship import like, regex, integer_match, email_match
contract = (
ContractBuilder("order-service", "user-api")
.with_metadata({"version": "1.0.0"})
.add_interaction(
InteractionBuilder("Get user by ID")
.given("user 1 exists")
.with_request("GET", "/users/1")
.with_request_header("Accept", "application/json")
.will_respond_with(200)
.with_response_header("Content-Type", "application/json")
.with_response_body(
{"id": 1, "name": "Alice", "email": "alice@example.com"},
matchers={
"body.id": integer_match(),
"body.name": like("string"),
"body.email": email_match(),
},
)
.build()
)
.build()
)from pactship import save_contract, load_contract
# Save to YAML or JSON (auto-detected from extension)
save_contract(contract, "contracts/user-api.yaml")
# Load from file
loaded = load_contract("contracts/user-api.yaml")from pactship import ProviderVerifier, MockProvider
# Verify against a live provider
verifier = ProviderVerifier(base_url="http://localhost:8080", timeout=30.0)
report = await verifier.verify(contract)
print(f"Passed: {report.success}")
for result in report.results:
print(f" {result.interaction}: {'PASS' if result.passed else 'FAIL'}")
# Use mock provider for consumer testing
mock = MockProvider(contract)
response = mock.handle_request("GET", "/users/1")
assert response.status == 200from pactship import diff_contracts
diff = diff_contracts(old_contract, new_contract)
print(f"Breaking changes: {diff.has_breaking_changes}")
for change in diff.changes:
print(f" [{change.change_type}] {change.description}")from pactship import ContractBroker
broker = ContractBroker(broker_dir=".pactship")
broker.publish(contract, version="1.0.0", tags=["production"])
contracts = broker.list_contracts()
specific = broker.get_contract("order-service", "user-api", version="1.0.0")from pactship.openapi import openapi_to_contracts
contracts = openapi_to_contracts("openapi.yaml", consumer="my-service")
for contract in contracts:
save_contract(contract, f"contracts/{contract.provider}.yaml")from pactship import ServiceGraph
graph = ServiceGraph()
graph.add_contract(contract)
mermaid = graph.to_mermaid()
print(mermaid)
# graph TD
# order-service --> user-apifrom pactship import lint_contract
result = lint_contract(contract)
print(f"Passed: {result.passed}")
for issue in result.issues:
print(f" [{issue.severity}] {issue.rule}: {issue.message}")from pactship.reporting import (
report_json,
report_junit,
report_markdown,
report_tap,
)
# Generate reports in multiple formats
json_report = report_json(verification_report)
junit_xml = report_junit(verification_report)
markdown = report_markdown(verification_report)
tap_output = report_tap(verification_report)from pactship.stats import contract_stats
stats = contract_stats(contract)
print(f"Methods: {stats['method_distribution']}")
print(f"Paths: {stats['path_count']}")
print(f"Matchers: {stats['matcher_count']}")pactship/
__init__.py # Public API exports (54 symbols)
models.py # Core data models (Contract, Interaction, Matcher, etc.)
dsl.py # Fluent builder DSL (ContractBuilder, InteractionBuilder)
matchers.py # 16 matcher types (exact, like, regex, range, etc.)
validator.py # Contract structure validation
verifier.py # Async HTTP provider verification + MockProvider
contract_io.py # YAML/JSON serialization and deserialization
schema.py # JSON Schema generation from contracts
diff.py # Contract version diffing with change classification
broker.py # Filesystem-based contract broker with versioning
cli.py # Click CLI (validate, verify, diff, publish, list, convert)
config.py # File + env var configuration loading
generator.py # CRUD and endpoint-based contract generation
graph.py # Service dependency graph with Mermaid output
matrix.py # Consumer/provider compatibility matrix
openapi.py # OpenAPI 3.x to pactship contract conversion
linter.py # Contract linting with REST best practice rules
reporting.py # Multi-format reports (JSON, JUnit, Markdown, TAP)
stats.py # Contract statistics and complexity metrics
hooks.py # Lifecycle hook registry (before/after verification)
transform.py # Contract transformation (paths, headers, bodies)
filters.py # Interaction filtering (method, path, tags)
┌─────────────┐
│ YAML/JSON │
│ Contract │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
│ Validator │ │ Diff │ │ Linter │
└─────┬─────┘ └───┬───┘ └─────┬─────┘
│ │ │
┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
│ Verifier │ │Report │ │ Issues │
│(async HTTP)│ │ │ │ │
└─────┬─────┘ └───────┘ └───────────┘
│
┌─────▼─────┐
│ Report │
│JSON/JUnit │
│ MD / TAP │
└───────────┘
name: Contract Tests
on: [push, pull_request]
jobs:
contracts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pactship
- run: |
for f in contracts/*.yaml; do
pactship validate "$f"
done
- run: pactship diff contracts/v1.yaml contracts/v2.yaml || true#!/bin/sh
for f in contracts/*.yaml; do
pactship validate "$f" || exit 1
doneMIT