Skill v1.0.1
currentAutomated scan100/1001 files
version: "1.0.1" name: python-best-practices description: Pythonic code with modern type hints, dataclasses, async patterns, packaging, and testing
Python Best Practices
Type Hints (3.12+ Syntax)
# Use built-in generics (3.9+), no need for typing.List, typing.Dictdef process_items(items: list[str]) -> dict[str, int]:return {item: len(item) for item in items}# Union with | syntax (3.10+)def find_user(user_id: int) -> User | None:...# Type parameter syntax (3.12+)type Vector[T] = list[T]type Matrix[T] = list[Vector[T]]def first[T](items: list[T]) -> T:return items[0]# TypedDict for structured dictsfrom typing import TypedDictclass UserResponse(TypedDict):id: intname: stremail: stractive: bool
Always type function signatures. Use mypy --strict or pyright in CI. Use type: ignore comments sparingly with justification.
Dataclasses vs Pydantic
Dataclasses (internal data, no validation needed)
from dataclasses import dataclass, field@dataclass(frozen=True, slots=True)class Point:x: floaty: floatdef distance_to(self, other: "Point") -> float:return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5@dataclassclass Config:host: str = "localhost"port: int = 8080tags: list[str] = field(default_factory=list)
Use frozen=True for immutable value objects. Use slots=True for memory efficiency.
Pydantic (external input, validation required)
from pydantic import BaseModel, Field, field_validatorclass CreateUserRequest(BaseModel):model_config = {"strict": True}email: str = Field(max_length=255)name: str = Field(min_length=1, max_length=100)age: int = Field(ge=13, le=150)@field_validator("email")@classmethoddef validate_email(cls, v: str) -> str:if "@" not in v:raise ValueError("Invalid email format")return v.lower()
Rule: Use dataclasses for domain models and internal structs. Use Pydantic for API boundaries, config files, and external data parsing.
Async Patterns
import asyncioimport httpxasync def fetch_user(client: httpx.AsyncClient, user_id: int) -> User:response = await client.get(f"/users/{user_id}")response.raise_for_status()return User(**response.json())async def fetch_all_users(user_ids: list[int]) -> list[User]:async with httpx.AsyncClient(base_url="https://api.example.com") as client:tasks = [fetch_user(client, uid) for uid in user_ids]return await asyncio.gather(*tasks)async def process_with_semaphore(items: list[str], max_concurrent: int = 10):semaphore = asyncio.Semaphore(max_concurrent)async def bounded_process(item: str):async with semaphore:return await process_item(item)return await asyncio.gather(*[bounded_process(i) for i in items])
Rules:
- Use
httpxinstead ofrequestsfor async HTTP - Use
asyncio.gatherfor concurrent tasks,asyncio.Semaphorefor rate limiting - Never call blocking I/O in async functions (use
asyncio.to_threadfor legacy code) - Use
async withfor resource management (connections, sessions)
Project Structure
my-project/src/my_project/__init__.pymain.pymodels.pyservices/__init__.pyuser_service.pyapi/__init__.pyroutes.pytests/conftest.pytest_models.pytest_services/test_user_service.pypyproject.toml
Use src layout to prevent accidental imports from the project root.
pyproject.toml
[build-system]requires = ["hatchling"]build-backend = "hatchling.build"[project]name = "my-project"version = "1.0.0"requires-python = ">=3.12"dependencies = ["httpx>=0.27","pydantic>=2.0",][project.optional-dependencies]dev = ["pytest>=8.0","pytest-cov","pytest-asyncio","mypy","ruff",][project.scripts]my-project = "my_project.main:cli"[tool.ruff]line-length = 100target-version = "py312"[tool.ruff.lint]select = ["E", "F", "I", "N", "UP", "B", "SIM", "RUF"][tool.mypy]strict = true[tool.pytest.ini_options]asyncio_mode = "auto"testpaths = ["tests"]
Use pyproject.toml for all tool configuration. Use Ruff instead of flake8 + isort + black (single tool, 10-100x faster).
Virtual Environments
# Use uv for fast dependency managementuv venvuv pip install -e ".[dev]"# Or standard venvpython -m venv .venvsource .venv/bin/activatepip install -e ".[dev]"
Always use virtual environments. Never install packages globally. Pin exact versions in a lockfile (uv.lock or requirements.txt generated from pip freeze).
Testing with pytest
import pytestfrom unittest.mock import AsyncMock, patch@pytest.fixturedef user_service(db_session):return UserService(session=db_session)async def test_create_user_returns_user_with_hashed_password(user_service):user = await user_service.create(email="test@example.com", password="secret")assert user.email == "test@example.com"assert user.password_hash != "secret"async def test_create_user_rejects_duplicate_email(user_service):await user_service.create(email="test@example.com", password="secret")with pytest.raises(DuplicateEmailError):await user_service.create(email="test@example.com", password="other")@pytest.fixturedef mock_http_client():client = AsyncMock(spec=httpx.AsyncClient)client.get.return_value = httpx.Response(200, json={"id": 1, "name": "Alice"})return clientasync def test_fetch_user_parses_response(mock_http_client):user = await fetch_user(mock_http_client, user_id=1)assert user.name == "Alice"mock_http_client.get.assert_called_once_with("/users/1")
Use conftest.py for shared fixtures. Use pytest.mark.parametrize for test variations. Use tmp_path fixture for file system tests.
Pythonic Idioms
# Unpackingfirst, *rest = itemsx, y = point# Comprehensions over map/filtersquares = [x**2 for x in numbers if x > 0]lookup = {u.id: u for u in users}# Context managers for resource cleanupwith open(path) as f:data = f.read()# Walrus operator for assign-and-testif (match := pattern.search(text)) is not None:process(match.group(1))# Structural pattern matching (3.10+)match command:case {"action": "move", "direction": d}:move(d)case {"action": "quit"}:sys.exit(0)case _:raise ValueError(f"Unknown command: {command}")
Error Handling
class AppError(Exception):def __init__(self, message: str, code: str):super().__init__(message)self.code = codeclass NotFoundError(AppError):def __init__(self, resource: str, id: str):super().__init__(f"{resource} {id} not found", "NOT_FOUND")# Specific exceptions, never bare excepttry:user = await get_user(user_id)except NotFoundError:return {"error": "User not found"}, 404except DatabaseError as e:logger.exception("Database error fetching user")return {"error": "Internal error"}, 500
Never use bare except:. Catch the most specific exception. Use logger.exception() to include tracebacks. Define custom exception hierarchies for your application.