plugins/python-development/skills/pydantic-v2/SKILL.md
Pydantic v2 patterns for production Python: validators (`@field_validator`, `@model_validator`), computed fields, strict types, discriminated unions, settings management, `model_validate` / `model_dump`, `condecimal` and `Annotated[Decimal, ...]` for money, performance tips, and a v1 to v2 migration checklist. Also covers FastAPI integration (response_model serialization, request validation, error envelope customization). TRIGGER WHEN: writing or refactoring Pydantic models in Python 3.10+; migrating a codebase from Pydantic v1 to v2; choosing between `Annotated[Decimal, ...]` vs `condecimal`; hitting v2 performance or serialization surprises; designing FastAPI request/response schemas or error envelopes with Pydantic. DO NOT TRIGGER WHEN: the task is Python testing (use python-tdd), generic typing unrelated to Pydantic (use mypy / typing docs), or non-Python schema work (use typescript-development for Zod / io-ts).
npx skillsauth add acaprino/alfio-claude-plugins pydantic-v2Install this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Pydantic v2 (released 2023-06, current stable 2.13 as of 2026-04-19, paired with Python 3.10-3.14 including 3.14 free-threaded builds) is a near-complete rewrite on pydantic-core (Rust). API is similar but not identical to v1 -- several v1 patterns silently break or behave differently. This skill documents the v2 idioms, the v1 migration gotchas, and the FastAPI integration surface.
Notable recent releases:
validate_by_alias / validate_by_name / serialize_by_alias config (populate_by_name pending v3 deprecation), Path and deque no longer accept constraints (2.11 release).MISSING sentinel, exclude_if on fields, ensure_ascii on JSON output, serialize_as_any unified behavior, @model_validator(mode="after") classmethod deprecated -- write as instance method (2.12 release).model_dump(polymorphic_serialization=True)), exclude_if extended to computed fields, ascii_only in StringConstraints, model_fields_set tracks post-instantiation extras (2.13 release).from datetime import datetime
from decimal import Decimal
from typing import Annotated, Literal
from pydantic import (
BaseModel,
ConfigDict,
Field,
StringConstraints,
computed_field,
field_validator,
model_validator,
)
# Type aliases with constraints -- preferred in v2 over `constr()` / `conint()`
NonEmptyStr = Annotated[str, StringConstraints(min_length=1, strip_whitespace=True)]
USDAmount = Annotated[Decimal, Field(max_digits=14, decimal_places=2, ge=0)]
class LineItem(BaseModel):
# model_config replaces inner `Config` class
model_config = ConfigDict(
strict=True, # refuse string "1" for int field
frozen=True, # hashable + immutable
populate_by_name=True, # accept alias AND field name
str_strip_whitespace=True, # strip on all str fields
extra="forbid", # reject unknown keys
)
sku: NonEmptyStr
quantity: int = Field(ge=1)
unit_price: USDAmount
currency: Literal["USD", "EUR", "GBP"] = "USD"
@computed_field
@property
def total(self) -> USDAmount:
return self.unit_price * self.quantity
@field_validator("sku")
@classmethod
def sku_format(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("sku must be alphanumeric")
return v.upper()
@model_validator(mode="after")
def cross_field_check(self) -> "LineItem":
if self.currency == "USD" and self.unit_price > Decimal("10000"):
raise ValueError("USD line item exceeds single-item limit")
return self
model_config = ConfigDict(...) replaces the v1 inner class Config: pattern. Importing ConfigDict gives you autocomplete.Annotated[T, Field(...)] or Annotated[T, StringConstraints(...)] is the idiomatic v2 way to attach constraints -- prefer over constr(), conint(), condecimal() wrappers (still supported, but Annotated composes better with Python's type system).@computed_field replaces the v1 @property + validator(always=True) dance; shows up in model_dump() automatically.@field_validator("field_name") replaces v1 @validator("field_name"). Decorator is classmethod; must have @classmethod decorator after @field_validator.@model_validator(mode="before" | "after") replaces v1 @root_validator(pre=True | False). In mode="after" the method returns self, not a dict. Note (2.12+): @classmethod on mode="after" is deprecated -- write as a plain instance method.validate_by_alias / validate_by_name / serialize_by_alias (2.11+) over the legacy populate_by_name=True. The latter is pending deprecation in v3.Path and deque no longer accept Field constraints (2.11+) -- use a custom validator for length / pattern checks.MISSING sentinel (2.12, experimental) lets you distinguish "not provided" from "explicit default" at validator time.| Mode | When to use | Performance | Returns |
|------|------------|-------------|---------|
| after (default) | 90% of cases -- logic on already-coerced value | Fast | Same type as field |
| before | Reshape raw input (string split, legacy key mapping) | Python callback overhead | Any (will be type-coerced afterward) |
| wrap | Catch ValidationError, add logging, force PydanticUseDefault | Slowest -- materializes data in Python | Same type as field |
| plain | Fully custom, no coercion, no downstream validators | N/A (terminates validation) | Trusted as-is -- dangerous for type safety |
Performance rule: express constraints in Field(...) or Annotated metadata whenever possible -- the Rust engine handles them. Python-level validators (@field_validator, mode="before", mode="wrap") incur per-value Python-call overhead (Performance guide).
from pydantic import BaseModel, ValidationInfo, field_validator
class User(BaseModel):
email: str
password: str
@field_validator("email")
@classmethod
def email_lowercase(cls, v: str) -> str:
return v.lower()
@field_validator("password")
@classmethod
def password_strength(cls, v: str, info: ValidationInfo) -> str:
# info.data: already-validated fields (in declaration order -- cannot access later fields)
# info.context: user-supplied dict via Model.model_validate(data, context={...})
# info.mode: "python" | "json" | "strings"
# info.field_name: the field being validated
if len(v) < 12:
raise ValueError("password must be at least 12 chars")
return v
Prefer Annotated metadata over decorator validators when the same logic applies across multiple models:
from typing import Annotated
from pydantic import AfterValidator, BeforeValidator, WrapValidator, BaseModel
def lower(v: str) -> str:
return v.lower()
def ensure_https(v: str) -> str:
if not v.startswith("https://"):
raise ValueError("URL must be https")
return v
LowerStr = Annotated[str, AfterValidator(lower)]
HttpsUrl = Annotated[str, AfterValidator(ensure_https)]
# Reuse across any model
class User(BaseModel):
email: LowerStr
avatar_url: HttpsUrl
Ordering rule: Before and Wrap validators run right-to-left; After validators run left-to-right; decorator-style @field_validators are appended last.
Stable API across 2.x. Context passes through to custom serializers (2.7+):
user = User.model_validate(payload, context={"tenant_id": request.tenant_id})
# Inside a field_validator / model_validator / field_serializer:
# info.context["tenant_id"] is available
Use context for tenant-aware validation, feature flags, or bypass switches -- NOT for data that should be on the model.
@model_validator(mode="before")
@classmethod
def normalize_input(cls, data: dict) -> dict:
# mode="before": classmethod, first arg cls, receives raw input (often dict)
# Do NOT mutate then raise -- mutations leak to downstream validators
if isinstance(data, dict) and "userEmail" in data and "user_email" not in data:
data["user_email"] = data.pop("userEmail")
return data
@model_validator(mode="after")
def check_date_order(self) -> "Event":
# mode="after": INSTANCE method (not classmethod; @classmethod deprecated in 2.12)
# returns self, not a dict
if self.start_date > self.end_date:
raise ValueError("start_date must be <= end_date")
return self
from pydantic_core import PydanticCustomError
@field_validator("sku")
@classmethod
def sku_format(cls, v: str) -> str:
if not v.isalnum():
raise PydanticCustomError(
"invalid_sku", # machine-readable error type
"{field} must be alphanumeric", # template (formatted by Pydantic)
{"field": "sku"}, # context dict
)
return v
ValueError auto-wraps into a Pydantic error -- simplest path.assert -- skipped under python -O.loc (error location) is determined by validator position; not user-editable at raise site.Base-class model validators run on subclass instances. Overriding in a subclass replaces (does not extend) the parent. Prefer composition via Annotated validators over deep inheritance.
model_dump, model_dump_json, model_validatev2 renamed the v1 methods:
| v1 | v2 |
|----|----|
| model.dict() | model.model_dump() |
| model.json() | model.model_dump_json() |
| Model.parse_obj(...) | Model.model_validate(...) |
| Model.parse_raw(...) | Model.model_validate_json(...) |
| Model.construct(...) | Model.model_construct(...) |
| Model.schema() | Model.model_json_schema() |
| Model.__fields__ | Model.model_fields |
All deprecated v1 names still work with deprecation warnings. Production: use the v2 names.
model.model_dump(mode="python") # native Python types (Decimal stays Decimal)
model.model_dump(mode="json") # JSON-compatible types (Decimal -> str, UUID -> str, datetime -> ISO)
model.model_dump(mode="strings") # everything coerced to str (CSV / form encodings)
model.model_dump(exclude={"password_hash"})
model.model_dump(include={"id", "name"})
model.model_dump(exclude_none=True)
model.model_dump(by_alias=True) # use field aliases as keys
model.model_dump(round_trip=True) # output can be fed back into model_validate
# 2.13+ polymorphic: subclass instances serialize by their actual type, exposing subclass-only fields
model.model_dump(polymorphic_serialization=True)
Three flavors, use the one that matches the shape:
from typing import Annotated
from pydantic import (
BaseModel, field_serializer, model_serializer,
PlainSerializer, WrapSerializer,
SerializerFunctionWrapHandler,
)
class Order(BaseModel):
total: Decimal
created_at: datetime
# Per-field decorator
@field_serializer("total", when_used="json")
def serialize_total(self, v: Decimal) -> str:
return f"${v:.2f}"
# Whole-model wrap: call handler() for default output, then post-process
@model_serializer(mode="wrap")
def add_envelope(self, handler: SerializerFunctionWrapHandler) -> dict:
return {"version": 1, "data": handler(self)}
# Reusable Annotated serializers
UsdAmount = Annotated[
Decimal,
PlainSerializer(lambda v: f"${v:.2f}", return_type=str, when_used="json"),
]
when_used values: "always" (default), "json", "json-unless-none", "unless-none".
model_json_schema(mode="validation" | "serialization") returns different shapes:
Model.model_json_schema(mode="validation") # request shape -- computed_fields absent, input-only aliases
Model.model_json_schema(mode="serialization") # response shape -- computed_fields required, output aliases
FastAPI uses both: validation mode for request schemas, serialization mode for response schemas. Pydantic v2 targets OpenAPI 3.1 natively.
| Field arg | Affects |
|-----------|---------|
| alias="x" | Both validation input AND serialization output |
| validation_alias="x" | Input only |
| serialization_alias="x" | Output only |
Pydantic 2.11 added per-config control:
class ApiResponse(BaseModel):
model_config = ConfigDict(
validate_by_alias=True, # accept aliases in input
validate_by_name=True, # also accept field names in input
serialize_by_alias=True, # emit aliases in output
)
user_id: str = Field(alias="userId")
populate_by_name still works but is pending v3 deprecation -- migrate now.
For hot paths, skip the Python dict materialization:
# Standard
raw = model.model_dump_json() # already faster than json.dumps(model.model_dump())
# Fast-path: bytes directly from Rust
raw_bytes = MyModel.__pydantic_serializer__.to_json(model)
Cache __pydantic_serializer__ at module scope for ultra-hot paths (SSE, WebSocket, bulk export).
model.model_dump(round_trip=True) guarantees Model.model_validate(dumped) yields an equivalent instance for RootModels, discriminated unions, and aliased fields. Mutable cached state (computed_fields with side-effects) is not guaranteed to survive round-trip.
The pattern for "this field determines which model to parse":
from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field
class EmailEvent(BaseModel):
type: Literal["email"]
to: str
subject: str
class SmsEvent(BaseModel):
type: Literal["sms"]
to: str
body: str
Event = Annotated[Union[EmailEvent, SmsEvent], Field(discriminator="type")]
class Envelope(BaseModel):
id: str
event: Event
Envelope.model_validate({"id": "1", "event": {"type": "email", "to": "x@y", "subject": "hi"}})
Faster than v1 unions (pydantic-core picks the variant without trying each) and produces cleaner error messages.
Never use float for money. Two idiomatic v2 options:
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, Field, condecimal
# Option A (preferred): Annotated with Field constraints
USDAmount = Annotated[Decimal, Field(max_digits=14, decimal_places=2, ge=0)]
# Option B: condecimal() wrapper (still supported)
USDAmountV1Style = condecimal(max_digits=14, decimal_places=2, ge=0)
class Invoice(BaseModel):
subtotal: USDAmount
tax: USDAmount
total: USDAmount
For currency arithmetic, pair with decimal.getcontext() for rounding mode:
from decimal import ROUND_HALF_EVEN, getcontext
getcontext().rounding = ROUND_HALF_EVEN # banker's rounding for financial totals
See senior-review:defect-taxonomy CWE-681 / CWE-682 for the full monetary-precision rules.
pydantic-settings)v2 extracted settings into a separate package pydantic-settings:
# pip install pydantic-settings
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="APP_",
case_sensitive=False,
)
database_url: str
stripe_secret_key: SecretStr # redacted in repr / model_dump_json
debug: bool = False
port: int = Field(default=8000, ge=1, le=65535)
settings = AppSettings()
SecretStr / SecretBytes hide the value in logs / tracebacks -- use for every API key, password, or tokensettings.stripe_secret_key.get_secret_value()from fastapi import FastAPI
from pydantic import BaseModel
class UserOut(BaseModel):
id: int
email: str
class UserDB(UserOut):
password_hash: str # server-side only, never exposed
app = FastAPI()
@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: int) -> UserDB:
# Returning the broader UserDB is fine; FastAPI projects through UserOut.
return await fetch_user(user_id)
FastAPI uses the response_model for serialization, which strips password_hash. Prefer this to manual dict construction.
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"error": "validation_failed",
"details": exc.errors(), # Pydantic v2 error format
"request_id": request.headers.get("x-request-id"),
},
)
v2 error shape differs from v1: ctx, input, and url fields changed. If you serialize errors to clients, pin the shape in an integration test.
model_dump_json() is faster than json.dumps(model.model_dump()) for streaming. For SSE / WebSocket, cache the serializer:
from typing import Any
serializer = MyModel.__pydantic_serializer__
raw = serializer.to_json(instance) # bytes
Ordered guide from the official Performance concept:
model_validate_json(raw_bytes) over model_validate(json.loads(raw_bytes)) -- skips Python dict materialization; pydantic-core streams directly from bytes.TypeAdapter at module scope, never inside request handlers. Each instantiation rebuilds validators.list[int] beats Sequence[int]; dict beats Mapping.TypedDict via TypeAdapter is ~2.5x faster than an equivalent nested BaseModel for pure-data shapes.Field(ge=...)) >> Python @field_validator -- express ranges, lengths, regex patterns as metadata.FailFast annotation on sequences (2.8+) stops validation on first failure instead of collecting all errors:
from typing import Annotated
from pydantic import FailFast
ids: Annotated[list[int], FailFast()]
cache_strings="all" (default) -- Pydantic caches repeated strings across validation calls. Memory-safe for typical API payloads; set to "none" only for adversarial input with high string entropy.defer_build=True in ConfigDict -- delay schema build until first validation. Useful when you define many models at import but use only a subset per request. 2.11 extended to @validate_call; 2.8 added experimental TypeAdapter support. Harmful if you want startup-time errors on malformed schemas.MyModel.__pydantic_serializer__.to_json(instance) -- bytes fast-path for SSE / WebSocket / bulk export. Cache the serializer at module scope.model_construct(**data) -- skips all validation and coercion. Safe for already-validated data (DB rows matching schema, internal cloning, test fixtures). Unsafe for any input that has crossed a trust boundary.Pydantic 2.11 added experimental free-threaded (nogil) build support; 2.12 ships official builds for Python 3.14 free-threaded. Treat as preview, not production, until a stability signal from the Pydantic team.
model_rebuild()Needed when a forward-referenced model is defined out of order or when forward refs haven't resolved. Harmful if called in a hot path -- rebuilds the Rust-side schema from scratch.
SecretStr / SecretBytesfrom pydantic import BaseModel, SecretStr
class Credentials(BaseModel):
api_key: SecretStr
c = Credentials(api_key="sk-real-secret-123")
print(c) # api_key=SecretStr('**********')
print(c.model_dump_json()) # {"api_key":"**********"}
c.api_key.get_secret_value() # the real value, use only at the trusted sink
Redacted in repr() and JSON output by default -- stops accidental logging and traceback leaks. To expose to a trusted sink (secret manager persistence), use a custom @field_serializer("api_key", when_used="always") that calls .get_secret_value().
TypeAdapter.validate_python(untrusted) has the same safety model as BaseModel.model_validate -- no more, no less permissive.model_validate_json(raw_bytes) streams through pydantic-core with built-in depth / size bounds; prefer over json.loads + model_validate.mode="before" validator that counts.model_dump() output alias back to the model (shallow copy) -- see gotcha above.Two first-party tools worth knowing when building LLM-driven features:
logfire.instrument_pydantic(record="failure") traces every failed validation in production with span metadata; record="all" traces successful validations too (use sparingly). logfire.instrument_pydantic_ai() instruments PydanticAI agents end-to-end (tool calls, LLM requests, token usage).Run bump-pydantic for the mechanical transforms:
uvx bump-pydantic src/
Then handle the cases the bot can't:
@validator -> @field_validator + @classmethod decorator above the validator@root_validator(pre=True) -> @model_validator(mode="before"), method is classmethod, first arg is cls, takes raw dict@root_validator (no pre) -> @model_validator(mode="after"), method returns self and is a plain instance method (2.12+ deprecated the classmethod form)Config.allow_population_by_field_name -> ConfigDict(validate_by_name=True) (and validate_by_alias=True, serialize_by_alias=True as needed). The older populate_by_name=True still works but is pending v3 deprecation.Config.orm_mode -> model_config = ConfigDict(from_attributes=True)Config.schema_extra -> model_config = ConfigDict(json_schema_extra={...})Config.extra = "allow"|"forbid"|"ignore" -> ConfigDict(extra=...)parse_obj -> model_validate; parse_raw -> model_validate_json; dict() -> model_dump(); json() -> model_dump_json()BaseModel[T] -- v2 uses native Python generics, not GenericModel; PEP 695 syntax supported since 2.11Field(..., env="FOO") in settings -> moved to pydantic-settings package (BaseSettings is no longer in pydantic)__fields__ -> model_fields (the shape changed: FieldInfo objects, not ModelField)__root__ model -> use RootModel[T] (from pydantic import RootModel)Optional[X] = None without default -> still optional but now must have default; v1 allowed no default on Optional, v2 requires itsmart_union -> default behavior in v2; old smart_union=True is a no-opValidationError.errors() format changed: msg, type, loc, input, ctx, url -- regenerate any error-envelope testsparse_obj_as(list[Foo], data) -> TypeAdapter(list[Foo]).validate_python(data) (instantiate adapter at module scope)bump-pydantic missesRequires manual conversion:
__get_validators__ / __modify_schema__ -> __get_pydantic_core_schema__ + __get_pydantic_json_schema__each_item=True validators on sequence fieldsjson_encoders in Config -> @field_serializer or Annotated PlainSerializer@validator(always=True) on computed fields -> @computed_fieldAlways diff the changes manually after running bump-pydantic.
strict=True breaks JSON API boundaries: by default, FastAPI coerces "1" -> 1 for path/query params. If you set strict=True globally, int query params must be sent as JSON numbers. Apply strict selectively (per-field or per-call).extra="forbid" + evolving clients: rejects unknown fields. Fine for internal APIs, risky for public APIs where clients may send forward-compatible fields. Use "ignore" (default) for public ingress.extra="allow" + model_dump(): v2 excludes extras from dumps by default (v1 included them). 2.13 tracks post-instantiation extra assignments in model_fields_set; set ConfigDict(extra="allow") and explicitly include extras when needed (issue #5683).@computed_field with expensive work: runs on every model_dump(). Cache if expensive, or use a regular property (which won't be serialized).model_copy(update={...}) is now required to "change" one -- can't just assign.model_validate_json raw bytes vs str: accepts both; bytes path avoids decode cost on hot paths.Literal, not str: Field(discriminator="type") requires each union member to declare type: Literal["..."], not type: str.__setattr__ by default: assigning to a mutable model attribute after construction skips validation. Set ConfigDict(validate_assignment=True) if you need it.@model_validator(mode="after") classmethod is deprecated (2.12+): write as a plain instance method returning self.populate_by_name pending v3 deprecation: use validate_by_name=True (and optionally validate_by_alias=True, serialize_by_alias=True) instead.AwareDatetime coerces naive input by treating it as local time then converting to UTC; NaiveDatetime rejects tz-aware input. Date-only strings (YYYY-MM-DD) can produce surprising aware datetimes -- pin test cases (issue #8859). 2.12 added explicit timestamp-unit control (seconds vs ms) -- no more ambiguous inference.model_dump() is shallow: mutating a nested custom-class object in the dumped dict mutates the original model instance. Use model.model_copy(deep=True).model_dump() if downstream code mutates (issue #10735).Path and deque no longer accept constraints (2.11+): use a custom validator for length / pattern checks.arbitrary_types_allowed=True is a pass-through: the attached value receives no validation. Necessary for stdlib types Pydantic doesn't know and third-party ORM rows; dangerous because it bypasses the safety model.from __future__ import annotations (PEP 563): may require model_rebuild() or module-scope model definitions for forward refs to resolve. Pyright handles it cleanly; mypy may need the plugin.Field(default_factory=list) for readability.python-development:python-engineer agentpython-development:python-tdd skillpython-development:python-scaffold command (--type fastapi)senior-review:defect-taxonomy (CWE-681 / CWE-682)typescript-development:mastering-typescript skilldevelopment
Quality gates for multi-reviewer code review pipelines: adversarial verification panel, completeness critic, reviewer pipeline conventions, and the context sharing pattern for parallel reviewers. TRIGGER WHEN: running /senior-review:team-review quality gates; running /senior-review:code-review Steps 4b/4c (adversarial verification and completeness check); consolidating or deduplicating findings from multiple parallel reviewers. DO NOT TRIGGER WHEN: single-reviewer style review without a consolidation phase, or generic team coordination (the upstream agent-teams skills cover that).
development
Knowledge base for pure-architecture decisions on when to unify duplicated logic into a shared abstraction versus leave it duplicated. Covers the canonical theory (Rule of Three, DRY/WET/AHA, Wrong Abstraction, Locality of Behaviour, Bounded Contexts, Tidy First options framing, CUPID vs SOLID), 12 essential-duplication patterns that justify unification, 12 wrong-abstraction patterns that justify inlining or decomposition, an operational decision frame, and a verified reading list. TRIGGER WHEN: the user is making an architectural decision about whether to centralize, extract, or remove a layer; reviewing an abstraction for premature generality; auditing scattered cross-cutting concerns; spawned by the abstraction-architect agent during /abstraction-architect:audit or as the Abstraction dimension of /senior-review:team-review or /senior-review:code-review; the user asks "should I extract this into a service" / "is this DRY enough" / "is this wrong abstraction". DO NOT TRIGGER WHEN: the task is code formatting and readability cleanup (use clean-code:clean-code), Python-specific refactoring with metrics (use python-development:python-refactor), generic dead-code removal (use senior-review:cleanup-dead-code), security review (use senior-review:security-auditor), or pure pattern-consistency review without an architecture lens (use senior-review:code-auditor).
development
Unified web frontend knowledge base covering CSS architecture, UX psychology, UI components, distinctive aesthetics, and interface design generation. TRIGGER WHEN: working on web styling, design systems, component decisions, responsive strategy, distinctive frontend aesthetics, or exploring multiple interface designs. DO NOT TRIGGER WHEN: the task is purely backend or unrelated to web frontend.
development
Stripe payments knowledge base - API patterns, checkout optimization, subscription lifecycle, pricing strategies, webhook reliability, Firebase integration, cost analysis, and revenue modeling. Loaded by stripe-integrator and revenue-optimizer agents; also consumable directly when the user asks for Stripe-specific patterns without needing an agent. TRIGGER WHEN: working with Stripe API (Payment Intents, Customers, Subscriptions, Checkout Sessions, Connect, webhooks, tax, usage-based billing), pricing strategy, or revenue modeling. DO NOT TRIGGER WHEN: payment work is non-Stripe (PayPal, Square, crypto) or the task is generic e-commerce unrelated to payments.