---
name: python-backend-standards
description: Python/FastAPI backend coding standards including Pydantic v2, SQLAlchemy 2.0, PostgreSQL patterns, and no-hardcoding rules
---

# Python Backend Standards

이 Skill은 Python/FastAPI 백엔드 코드 품질 표준을 정의합니다.

---

## No Hardcoding Rules

### Magic Numbers → Named Constants
```python
# ❌ BAD
if retry_count > 3:
    time.sleep(30)

# ✅ GOOD
MAX_RETRY_COUNT = 3
RETRY_DELAY_SECONDS = 30

if retry_count > MAX_RETRY_COUNT:
    time.sleep(RETRY_DELAY_SECONDS)
```

### String Literals → Enums or Constants
```python
# ❌ BAD
if status == "active":
    ...
if role == "admin":
    ...

# ✅ GOOD
class UserStatus(str, Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"

class UserRole(str, Enum):
    ADMIN = "admin"
    USER = "user"

if status == UserStatus.ACTIVE:
    ...
```

### URLs/Endpoints → Configuration
```python
# ❌ BAD
response = requests.get("https://api.example.com/v1/users")

# ✅ GOOD
response = requests.get(f"{settings.external_api.base_url}/users")
```

### Error Messages → ErrorCode Enums
```python
# ❌ BAD
raise HTTPException(400, "User not found")

# ✅ GOOD
class ErrorCode(str, Enum):
    USER_NOT_FOUND = "USER_NOT_FOUND"
    
raise HTTPException(400, ErrorCode.USER_NOT_FOUND)
```

---

## Pydantic v2 Standards

### Migration from v1
```python
# ❌ OLD (Pydantic v1)
class UserSchema(BaseModel):
    class Config:
        orm_mode = True
    
    @validator("email")
    def validate_email(cls, v):
        return v.lower()
    
    def to_dict(self):
        return self.dict()

# ✅ NEW (Pydantic v2)
class UserSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        return v.lower()
    
    def to_dict(self):
        return self.model_dump()
```

### Method Mapping
| Pydantic v1 | Pydantic v2 |
|-------------|-------------|
| `@validator` | `@field_validator` |
| `Config` class | `model_config = ConfigDict(...)` |
| `orm_mode = True` | `from_attributes=True` |
| `.dict()` | `.model_dump()` |
| `.json()` | `.model_dump_json()` |
| `.parse_obj()` | `.model_validate()` |

---

## SQLAlchemy 2.0 Standards

### Model Definition
```python
# ❌ OLD (SQLAlchemy 1.x)
class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    email = Column(String(255), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)

# ✅ NEW (SQLAlchemy 2.0)
class User(Base):
    __tablename__ = "users"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    email: Mapped[str] = mapped_column(String(255))
    created_at: Mapped[datetime] = mapped_column(default=func.now())
```

### Query Patterns
```python
# ❌ OLD
session.query(User).filter(User.id == user_id).first()

# ✅ NEW
from sqlalchemy import select
stmt = select(User).where(User.id == user_id)
result = session.execute(stmt).scalar_one_or_none()
```

---

## PostgreSQL Standards

### UUID Generation
```python
# ❌ BAD: Python-generated UUID
id: Mapped[UUID] = mapped_column(default=uuid.uuid4)

# ✅ GOOD: Database-generated UUID
id: Mapped[UUID] = mapped_column(
    server_default=text("gen_random_uuid()"),
    primary_key=True
)
```

### JSONB for Flexible Data
```python
# ✅ GOOD: Use JSONB
from sqlalchemy.dialects.postgresql import JSONB

metadata: Mapped[dict] = mapped_column(JSONB, default=dict)
```

### Index Standards
```python
# ✅ Create appropriate indexes
__table_args__ = (
    Index("ix_users_email", "email", unique=True),
    Index("ix_users_created_at", "created_at"),
)
```

---

## Type Hints

### Required Throughout
```python
# ❌ BAD: No type hints
def get_user(user_id):
    return repo.get(user_id)

# ✅ GOOD: Full type hints
async def get_user(user_id: int) -> UserResponse | None:
    return await repo.get(user_id)
```

### Optional Handling
```python
# ✅ Use modern syntax (Python 3.10+)
def process(data: str | None = None) -> dict[str, Any]:
    ...
```

---

## Async/Await Best Practices

### Proper Usage
```python
# ❌ BAD: Blocking call in async function
async def get_data():
    response = requests.get(url)  # Blocking!
    return response.json()

# ✅ GOOD: Async HTTP client
async def get_data():
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.json()
```

### Batch Operations
```python
# ✅ GOOD: Concurrent execution
results = await asyncio.gather(
    service.get_user(user_id),
    service.get_orders(user_id),
    service.get_notifications(user_id)
)
```

---

## Code Quality Checklist

| Check | Severity |
|-------|----------|
| No magic numbers | 🟡 WARNING |
| No hardcoded strings | 🟡 WARNING |
| Pydantic v2 syntax | 🟡 WARNING |
| SQLAlchemy 2.0 syntax | 🟡 WARNING |
| Type hints present | 🟡 WARNING |
| Proper async usage | 🟡 WARNING |
| PostgreSQL best practices | 💡 SUGGESTION |
