home / skills / carlos-asg / tu-voz-en-ruta / python

python skill

/skills/python

This skill enforces modern Python typing, dataclass patterns, and protocol-based design to improve safety, readability, and maintainability.

npx playbooks add skill carlos-asg/tu-voz-en-ruta --skill python

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.5 KB
---
name: python
description: >
  Python modern patterns and best practices using type hints, dataclasses, and Protocol-based design.
  Trigger: When implementing or refactoring Python in .py files (type hints, dataclasses, protocols, enums, type guards, removing Any, tightening types).
license: Apache-2.0
metadata:
  author: Carlos
  version: "1.0"
  scope: [root]
  auto_invoke: "Writing Python types/dataclasses"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
---

## Enum Pattern (REQUIRED)

```python
# ✅ ALWAYS: Use Enum for constants with type safety
from enum import Enum, auto

class Status(str, Enum):
    """String-based enum for JSON serialization."""
    ACTIVE = "active"
    INACTIVE = "inactive"
    PENDING = "pending"

class Priority(Enum):
    """Auto-valued enum."""
    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()

# ❌ NEVER: Bare string literals or constants dict
STATUS_ACTIVE = "active"  # NO!
STATUS = {"ACTIVE": "active"}  # NO!
```

**Why?** Type safety, IDE autocomplete, exhaustiveness checking, prevents typos.

## Dataclasses Over Dicts (REQUIRED)

```python
# ✅ ALWAYS: Use dataclasses for structured data
from dataclasses import dataclass, field

@dataclass(frozen=True)  # Immutable by default
class UserAddress:
    street: str
    city: str
    zip_code: str
    country: str = "Mexico"  # Default value

@dataclass
class User:
    id: str
    name: str
    email: str
    address: UserAddress  # Nested dataclass
    tags: list[str] = field(default_factory=list)  # Mutable default
    created_at: str | None = None

# ❌ NEVER: Plain dicts for structured data
user = {
    "id": "123",
    "name": "Carlos",
    "address": {"street": "...", "city": "..."}  # NO!
}
```

**Why?** Type checking, validation, immutability, clear structure, IDE support.

## Type Hints (REQUIRED)

```python
# ✅ ALWAYS: Use modern type hints (Python 3.10+)
from typing import Protocol, TypeAlias
from collections.abc import Sequence, Mapping, Callable

# Modern union syntax
def process_user(user_id: str | None) -> User | None:
    pass

# Type aliases for complex types
UserId: TypeAlias = str
UserMap: TypeAlias = dict[UserId, User]

# Generic collections (no typing.List/Dict)
def filter_active(users: list[User]) -> list[User]:
    return [u for u in users if u.status == Status.ACTIVE]

# Callable types
Validator: TypeAlias = Callable[[str], bool]

def validate_email(email: str, validator: Validator) -> bool:
    return validator(email)

# ❌ NEVER: Missing type hints or old syntax
def process_user(user_id):  # NO!
    pass

from typing import List, Dict  # Deprecated since Python 3.9
def get_users() -> List[Dict]:  # NO! Use list[dict]
    pass
```

## Never Use `Any`

```python
from typing import Any, TypeVar, Protocol

# ✅ Use Protocol for structural typing
class Serializable(Protocol):
    def to_dict(self) -> dict[str, object]:
        ...

def serialize(obj: Serializable) -> str:
    return json.dumps(obj.to_dict())

# ✅ Use TypeVar for generics
T = TypeVar("T")

def first(items: Sequence[T]) -> T | None:
    return items[0] if items else None

# ✅ Use object for truly unknown types (prefer Protocol)
def log(value: object) -> None:
    print(str(value))

# ❌ NEVER
def process(data: Any) -> Any:  # NO!
    pass
```

## Protocol-Based Design (Duck Typing)

```python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    """Anything that can be drawn."""
    def draw(self) -> None:
        ...

@runtime_checkable
class Saveable(Protocol):
    """Anything that can be saved."""
    def save(self, path: str) -> None:
        ...

# Works with any object implementing the protocol
def render(obj: Drawable) -> None:
    obj.draw()

# Runtime checking
def process(obj: object) -> None:
    if isinstance(obj, Drawable):
        obj.draw()
```

## Type Guards

```python
from typing import TypeGuard

def is_user(value: object) -> TypeGuard[User]:
    """Type guard for User validation."""
    return (
        isinstance(value, dict)
        and "id" in value
        and "name" in value
        and isinstance(value["id"], str)
        and isinstance(value["name"], str)
    )

def process_data(data: object) -> None:
    if is_user(data):
        # Type checker knows data is User here
        print(data["name"])
```

## Pattern Matching (Python 3.10+)

```python
def process_response(response: dict[str, object]) -> str:
    match response:
        case {"status": "success", "data": data}:
            return f"Success: {data}"
        case {"status": "error", "message": msg}:
            return f"Error: {msg}"
        case _:
            return "Unknown response"

def process_user(user: User | None) -> str:
    match user:
        case User(status=Status.ACTIVE, name=name):
            return f"Active user: {name}"
        case User(status=Status.INACTIVE):
            return "Inactive user"
        case None:
            return "No user"
```

## Result Pattern (No Exceptions for Flow Control)

```python
from typing import Generic, TypeVar
from dataclasses import dataclass

T = TypeVar("T")
E = TypeVar("E")

@dataclass(frozen=True)
class Ok(Generic[T]):
    value: T

@dataclass(frozen=True)
class Err(Generic[E]):
    error: E

Result = Ok[T] | Err[E]

def divide(a: float, b: float) -> Result[float, str]:
    if b == 0:
        return Err("Division by zero")
    return Ok(a / b)

# Usage
match divide(10, 2):
    case Ok(value):
        print(f"Result: {value}")
    case Err(error):
        print(f"Error: {error}")
```

## Dataclass Validation

```python
from dataclasses import dataclass, field
from typing import Self

@dataclass(frozen=True)
class Email:
    """Value object with validation."""
    address: str
    
    def __post_init__(self) -> None:
        if "@" not in self.address:
            raise ValueError(f"Invalid email: {self.address}")
        # Use object.__setattr__ for frozen dataclasses
        object.__setattr__(self, "address", self.address.lower())
    
    @classmethod
    def create(cls, address: str) -> Self:
        """Factory method with validation."""
        return cls(address=address.strip().lower())

@dataclass
class User:
    id: str
    name: str
    email: Email
    
    def __post_init__(self) -> None:
        if len(self.name) < 2:
            raise ValueError("Name must be at least 2 characters")
```

## Import Organization

```python
# Standard library
import json
import logging
from pathlib import Path
from typing import Protocol, TypeAlias
from dataclasses import dataclass
from enum import Enum

# Django (if applicable)
from django.db import models
from django.http import HttpRequest, HttpResponse

# Third-party
import httpx
from pydantic import BaseModel

# Local
from .models import User
from .services import UserService
from .types import UserId, UserMap
```

## Modern Python Features Checklist

- ✅ Type hints on all functions and methods
- ✅ `dataclass` for DTOs and value objects
- ✅ `Enum` for constants
- ✅ `Protocol` for duck typing
- ✅ Pattern matching for complex conditionals
- ✅ `|` syntax for unions (not `Union`)
- ✅ `list[T]` not `List[T]` (Python 3.9+)
- ✅ `TypeAlias` for complex types
- ✅ `TypeGuard` for type narrowing
- ✅ `frozen=True` for immutability
- ✅ `field(default_factory=...)` for mutable defaults
- ✅ Context managers for resource management
- ✅ Decorators with proper type hints
- ✅ Functional programming patterns
- ❌ Never use `Any` (use `object` or `Protocol`)
- ❌ Never use bare `dict` for structured data
- ❌ Never use string literals for constants

Overview

This skill guides implementing modern Python patterns for safer, clearer code using type hints, dataclasses, Enums, and Protocol-based design. It focuses on tightening types, removing Any, and applying patterns like Result objects, type guards, and pattern matching to real .py files. The goal is readable, IDE-friendly, and maintainable code suitable for production services.

How this skill works

It inspects Python files and recommends replacing ad-hoc dicts and string constants with dataclasses and Enums, adding precise type hints and TypeAlias declarations, and introducing Protocols instead of Any. It suggests type guards, pattern matching, and Result types for flow control, and enforces modern collection and union syntax. Recommendations include import organization and immutable value object patterns with dataclass validation.

When to use it

  • When refactoring functions or modules that use plain dicts for structured data
  • When you see bare strings or constants representing states or categories
  • When functions lack precise type hints or use Any
  • When designing interfaces for loosely coupled components (replace with Protocols)
  • When handling success/error flows where exceptions are used for control
  • When adding validation or immutability to domain objects

Best practices

  • Use Enum (str or auto) for constants to enable type safety and exhaustiveness checks
  • Prefer dataclasses (frozen when possible) over dicts; use field(default_factory=...) for mutable defaults
  • Annotate every public function and method with modern type hints (list[T], dict[K,V], | unions)
  • Avoid Any; use Protocol, TypeVar, or object. Add TypeGuard functions for runtime-safe narrowing
  • Use Result (Ok/Err) types for predictable flow control instead of exceptions for expected failures
  • Organize imports: stdlib, framework, third-party, then local modules

Example use cases

  • Convert complaint payloads from JSON into a frozen dataclass with validated Email and Address value objects
  • Replace string status values in complaint records with a Status enum for reliable comparisons and JSON-safe serialization
  • Introduce a ComplaintRepository Protocol so multiple storage backends (DB, file, mock) implement the same interface
  • Add is_user type guard to validate incoming request bodies before processing
  • Refactor a function that returns None or raises into a Result[Value, Error] pattern and use pattern matching to handle outcomes

FAQ

What if external libraries use dicts or Any?

Wrap library outputs in dataclasses or adapter functions that validate and convert to typed objects; use Protocols to describe expected behavior rather than relying on Any.

Can I mix dataclasses and ORMs?

Yes. Keep domain dataclasses separate from ORM models and write mappers between them to preserve validation and immutability.