Modern Python development with Python 3.12+, Django, FastAPI, async patterns, and production best practices. Use for Python projects, APIs, data processing, or automation scripts.
Install
npx skillscat add saajunaid/caddis-plugin/python-development Install via the SkillsCat registry.
SKILL.md
Python Development
Project Setup
Modern Python Project Structure
my-project/
├── src/
│ └── my_project/
│ ├── __init__.py
│ ├── main.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ └── test_main.py
├── pyproject.toml
├── README.md
└── .gitignorepyproject.toml
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.100.0",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"ruff>=0.1.0",
"mypy>=1.0",
]
[tool.ruff]
line-length = 88
select = ["E", "F", "I", "N", "W"]
[tool.mypy]
strict = trueType Hints
from typing import TypeVar, Generic
from collections.abc import Sequence
T = TypeVar('T')
def process_items(items: Sequence[str]) -> list[str]:
return [item.upper() for item in items]
class Repository(Generic[T]):
def get(self, id: int) -> T | None: ...
def save(self, item: T) -> T: ...Async Patterns
import asyncio
from collections.abc import AsyncIterator
async def fetch_all(urls: list[str]) -> list[dict]:
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls]
return await asyncio.gather(*tasks)
async def stream_data() -> AsyncIterator[bytes]:
async with aiofiles.open('large_file.txt', 'rb') as f:
async for chunk in f:
yield chunkFastAPI Patterns
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
email: str
name: str
class UserResponse(BaseModel):
id: int
email: str
name: str
@app.post("/users", response_model=UserResponse)
async def create_user(
user: UserCreate,
db: Database = Depends(get_db)
) -> UserResponse:
result = await db.users.create(user.model_dump())
return UserResponse(**result)Testing
import pytest
from unittest.mock import AsyncMock, patch
@pytest.fixture
def mock_db():
db = AsyncMock()
db.users.get.return_value = {"id": 1, "name": "Test"}
return db
@pytest.mark.asyncio
async def test_get_user(mock_db):
result = await get_user(1, db=mock_db)
assert result["name"] == "Test"
mock_db.users.get.assert_called_once_with(1)Core Principles
Readability First
Python prioritizes readability. Code should be obvious and easy to understand.
# Good: Clear and readable
def get_active_users(users: list[User]) -> list[User]:
"""Return only active users from the provided list."""
return [user for user in users if user.is_active]
# Bad: Clever but confusing
def get_active_users(u):
return [x for x in u if x.a]EAFP Over LBYL
Python prefers exception handling over checking conditions upfront.
# Good: EAFP style (Easier to Ask Forgiveness than Permission)
def get_value(dictionary: dict, key: str, default=None):
try:
return dictionary[key]
except KeyError:
return default
# Bad: LBYL (Look Before You Leap) style
def get_value(dictionary: dict, key: str, default=None):
if key in dictionary:
return dictionary[key]
return defaultError Handling Patterns
Specific Exception Handling
# Good: Catch specific exceptions with chaining
def load_config(path: str) -> Config:
try:
with open(path) as f:
return Config.from_json(f.read())
except FileNotFoundError as e:
raise ConfigError(f"Config file not found: {path}") from e
except json.JSONDecodeError as e:
raise ConfigError(f"Invalid JSON in config: {path}") from e
# Bad: Bare except with silent failure
def load_config(path: str) -> Config:
try:
with open(path) as f:
return Config.from_json(f.read())
except:
return NoneCustom Exception Hierarchy
class AppError(Exception):
"""Base exception for all application errors."""
class ValidationError(AppError):
"""Raised when input validation fails."""
class NotFoundError(AppError):
"""Raised when a requested resource is not found."""
def get_user(user_id: str) -> User:
user = db.find_user(user_id)
if not user:
raise NotFoundError(f"User not found: {user_id}")
return userContext Managers
from contextlib import contextmanager
# Custom context manager for timing
@contextmanager
def timer(name: str):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f"{name} took {elapsed:.4f} seconds")
# Class-based context manager for transactions
class DatabaseTransaction:
def __init__(self, connection):
self.connection = connection
def __enter__(self):
self.connection.begin_transaction()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.connection.commit()
else:
self.connection.rollback()
return False # Don't suppress exceptionsData Classes and Validation
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
id: str
name: str
email: str
created_at: datetime = field(default_factory=datetime.now)
is_active: bool = True
def __post_init__(self):
if "@" not in self.email:
raise ValueError(f"Invalid email: {self.email}")Decorators
import functools
# Function decorator with wraps
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
# Parameterized decorator
def retry(max_attempts: int = 3, delay: float = 1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
time.sleep(delay * (2 ** attempt))
return wrapper
return decoratorComprehensions and Generators
# List comprehensions for simple transforms
names = [user.name for user in users if user.is_active]
# Generator expressions for lazy evaluation (memory efficient)
total = sum(x * x for x in range(1_000_000))
# Generator functions for large data
def read_large_file(path: str) -> Iterator[str]:
with open(path) as f:
for line in f:
yield line.strip()Concurrency Patterns
import concurrent.futures
# Threading for I/O-bound tasks
def fetch_all_urls(urls: list[str]) -> dict[str, str]:
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
future_to_url = {executor.submit(fetch_url, url): url for url in urls}
results = {}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
results[url] = future.result()
except Exception as e:
results[url] = f"Error: {e}"
return results
# Multiprocessing for CPU-bound tasks
def process_all(datasets: list[list[int]]) -> list[int]:
with concurrent.futures.ProcessPoolExecutor() as executor:
return list(executor.map(process_data, datasets))Memory and Performance
# Use __slots__ for memory efficiency in many-instance classes
class Point:
__slots__ = ['x', 'y']
def __init__(self, x: float, y: float):
self.x = x
self.y = y
# Use join instead of concatenation in loops
result = "".join(str(item) for item in items) # O(n)
# NOT: result += str(item) # O(n²)Anti-Patterns to Avoid
# Bad: Mutable default arguments
def append_to(item, items=[]): # Shared across calls!
items.append(item)
return items
# Good: Use None sentinel
def append_to(item, items=None):
if items is None:
items = []
items.append(item)
return items
# Bad: type(obj) == list → Good: isinstance(obj, list)
# Bad: value == None → Good: value is None
# Bad: from module import * → Good: from module import specific_name
# Bad: bare except: → Good: except SpecificError as e:Python Tooling
# Formatting and linting
ruff check . && ruff format .
mypy .
# Testing with coverage
pytest --cov=mypackage --cov-report=term-missing
# Security scanning
bandit -r .
pip-auditBest Practices
- Use
rufffor linting and formatting - Use
mypywith strict mode - Prefer
pathlib.Pathoveros.path - Use dataclasses or Pydantic for data structures
- Use
asynciofor I/O-bound operations - Use
contextlib.asynccontextmanagerfor async resources - Use Protocol classes for structural subtyping (duck typing)
- Prefer generators over lists for large datasets
- Follow import order: stdlib, third-party, local (use isort)