Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Python/FastAPI coding standards including async patterns, Pydantic v2, SQLAlchemy 2.0, and project structure. Use when writing Python code, reviewing FastAPI projects, or learning FastAPI conventions.
.claude/skills/kunanonj-python-best-practices/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | -8% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 2% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 3% | 0% |
| case-03 | ✗→✓ | ▲ Improved | -25% | 0% |
| case-05 | ✗→✓ | ▲ Improved | -24% | 0% |
Router → Service → Repository → Database. Each layer only calls the one below it.
> See code-patterns.md for full project structure and layer examples.
Separate Create/Update/Response schemas. Use ConfigDict(from_attributes=True) for ORM integration. Use str | None syntax (not Optional[str]).
> See code-patterns.md for schema examples.
async def for I/O routes, plain def for CPU-bound. Use lifespan context manager (not on_event). Use httpx.AsyncClient for external HTTP calls.
> See code-patterns.md for async examples.
Use a SoftDeleteMixin on SQLAlchemy models. Filter where(Model.deleted_at.is_(None)) in all queries.
> See code-patterns.md for mixin and repository patterns.
Use pydantic-settings for all config. Never hardcode secrets, URLs, or magic numbers.
> See code-patterns.md for Settings class pattern.
Use a generic PaginatedResponse[T] for all list endpoints. Always return total, page, limit, has_more.
> See code-patterns.md for the pattern.
async def vs def matters for performance. An async def route that calls blocking code (like time.sleep() or sync DB drivers) blocks the entire event loop. Use plain def for CPU-bound work — FastAPI runs it in a threadpool. Use async def only when you await something.datetime.utcnow() is deprecated since Python 3.12. Use datetime.now(UTC) instead. The old function returns a naive datetime (no timezone), which causes comparison bugs. The new one returns timezone-aware UTC.tags: list[str] = [] works in Pydantic (it copies the default). But tags: list[str] = Field(default_factory=list) is explicit and safer for nested models. For simple fields, either works. For complex nested defaults, always use default_factory.from_attributes=True replaces orm_mode=True. Pydantic v2 changed the config API. Using the old orm_mode silently does nothing — your ORM objects won't serialize correctly.Column() is legacy. Use Mapped[type] with mapped_column() for SQLAlchemy 2.0. The old Column(String) still works but loses type checker support and IDE autocomplete.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-11 | fail→pass | 11,137 | 7,083 | -36% | 1 | 1 | 0% | 2,296 | 2,115 | -8% | 0 | 0 | — |
case-01 | fail→pass | 15,206 | 13,008 | -14% | 1 | 1 | 0% | 3,490 | 3,557 | +2% | 0 | 0 | — |
case-02 | fail→pass | 14,262 | 10,914 | -23% | 1 | 1 | 0% | 2,906 | 2,991 | +3% | 0 | 0 | — |
case-16 | pass→pass | 13,875 | 10,931 | -21% | 1 | 1 | 0% | 2,948 | 3,102 | +5% | 0 | 0 | — |
case-03 | fail→pass | 18,252 | 10,957 | -40% | 1 | 1 | 0% | 4,048 | 3,020 | -25% | 0 | 0 | — |
case-04 | pass→pass | 15,913 | 11,022 | -31% | 1 | 1 | 0% | 3,354 | 3,189 | -5% | 0 | 0 | — |
case-05 | fail→pass | 11,814 | 6,048 | -49% | 1 | 1 | 0% | 2,377 | 1,806 | -24% | 0 | 0 | — |
case-06 | pass→pass | 6,385 | 4,627 | -28% | 1 | 1 | 0% | 1,417 | 1,402 | -1% | 0 | 0 | — |
case-07 | fail→fail | 13,262 | 10,278 | -23% | 1 | 1 | 0% | 2,524 | 2,624 | +4% | 0 | 0 | — |
case-08 | pass→pass | 10,989 | 9,864 | -10% | 1 | 1 | 0% | 2,284 | 2,748 | +20% | 0 | 0 | — |
case-09 | fail→pass | 8,178 | 4,675 | -43% | 1 | 1 | 0% | 1,764 | 1,553 | -12% | 0 | 0 | — |
case-10 | pass→pass | 9,913 | 5,795 | -42% | 1 | 1 | 0% | 2,024 | 1,820 | -10% | 0 | 0 | — |
case-12 | pass→pass | 10,704 | 5,558 | -48% | 1 | 1 | 0% | 2,209 | 1,709 | -23% | 0 | 0 | — |
case-13 | pass→pass | 5,477 | 4,618 | -16% | 1 | 1 | 0% | 991 | 1,558 | +57% | 0 | 0 | — |
case-14 | pass→pass | 4,849 | 3,916 | -19% | 1 | 1 | 0% | 885 | 1,332 | +51% | 0 | 0 | — |
case-15 | pass→pass | 13,401 | 8,752 | -35% | 1 | 1 | 0% | 2,834 | 2,497 | -12% | 0 | 0 | — |
case-17 | pass→pass | 5,182 | 4,731 | -9% | 1 | 1 | 0% | 1,126 | 1,546 | +37% | 0 | 0 | — |
case-18 | pass→pass | 8,314 | 7,068 | -15% | 1 | 1 | 0% | 1,732 | 2,327 | +34% | 0 | 0 | — |
case-19 | pass→pass | 10,825 | 6,611 | -39% | 1 | 1 | 0% | 2,350 | 1,958 | -17% | 0 | 0 | — |
case-20 | pass→pass | 5,823 | 4,794 | -18% | 1 | 1 | 0% | 1,239 | 1,633 | +32% | 0 | 0 | — |
case-21 | pass→pass | 11,034 | 8,600 | -22% | 1 | 1 | 0% | 2,283 | 2,392 | +5% | 0 | 0 | — |
case-22 | pass→pass | 14,374 | 9,533 | -34% | 1 | 1 | 0% | 2,982 | 2,664 | -11% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.