
Python Best Practices
- 7 installs
- 32 repo stars
- Updated June 22, 2026
- jkitchin/skillz
Write professional Python following PEP 8, type hints, TDD, and modern tooling like Ruff, Black, Mypy, and uv.
About
A skill giving guidance for writing and refactoring maintainable Python with PEP 8, testing, type hints, and error handling. A developer uses it when writing new Python code or setting up a project.
- PEP 8, type hints, and TDD emphasis
- Modern tooling: uv, Ruff, Black, Mypy
Python Best Practices by the numbers
- 7 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #219 of 290 Python skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jkitchin/skillz --skill python-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 32 |
| Last updated | June 22, 2026 |
| Repository | jkitchin/skillz ↗ |
What it does
Write professional Python following PEP 8, type hints, TDD, and modern tooling like Ruff, Black, Mypy, and uv.
Files
Python Best Practices Skill
This skill provides expert guidance for writing professional, maintainable Python code that follows industry best practices and standards.
When to Use This Skill
Use this skill when:
- Writing new Python functions, classes, or modules
- Refactoring existing Python code for better quality
- Setting up a new Python project with proper structure
- Implementing unit tests or adopting TDD
- Adding type hints for better code clarity
- Configuring linting, formatting, and type checking tools
- Managing dependencies and virtual environments
- Improving code readability and maintainability
- Following PEP 8 style guidelines
Core Principles
1. PEP 8: Style Guide for Python Code
Key Guidelines:
- Indentation: Use 4 spaces per indentation level (never tabs)
- Line Length: Limit lines to 79 characters (99 for code, 72 for docstrings/comments)
- Blank Lines: 2 blank lines between top-level functions/classes, 1 within classes
- Imports:
- One import per line
- Order: standard library, third-party, local (each group separated by blank line)
- Avoid wildcard imports (
from module import *) - Naming Conventions:
snake_casefor functions, variables, methodsPascalCasefor classesUPPER_CASEfor constants- Leading underscore
_privatefor internal use - Whitespace:
- No trailing whitespace
- One space around operators:
x = 1, notx=1 - No space before function parentheses:
func(x), notfunc (x)
Example:
"""Module docstring describing purpose."""
import os
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from mypackage.module import MyClass
# Constants
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
class DataProcessor:
"""Process and analyze data sets.
Attributes:
name: Processor name
threshold: Minimum value threshold
"""
def __init__(self, name: str, threshold: float = 0.5):
"""Initialize processor.
Args:
name: Name of the processor
threshold: Threshold value for filtering (default: 0.5)
"""
self.name = name
self.threshold = threshold
def process_data(self, data: list[float]) -> list[float]:
"""Process data by filtering values below threshold.
Args:
data: List of numeric values to process
Returns:
Filtered list containing only values >= threshold
Raises:
ValueError: If data is empty
"""
if not data:
raise ValueError("Data cannot be empty")
return [x for x in data if x >= self.threshold]2. Readability and Clarity
Write Self-Documenting Code:
# Bad: unclear variable names
def calc(x, y, z):
return x * y / z
# Good: descriptive names
def calculate_unit_price(total_cost: float, quantity: int, tax_rate: float) -> float:
"""Calculate price per unit including tax."""
return total_cost * (1 + tax_rate) / quantityUse Docstrings:
def fetch_user_data(user_id: int, include_history: bool = False) -> dict:
"""Fetch user data from the database.
Args:
user_id: Unique identifier for the user
include_history: Whether to include transaction history
Returns:
Dictionary containing user information with keys:
- 'name': User's full name
- 'email': User's email address
- 'history': List of transactions (if include_history=True)
Raises:
UserNotFoundError: If user_id doesn't exist
DatabaseError: If connection fails
Example:
>>> user = fetch_user_data(123, include_history=True)
>>> print(user['name'])
'John Doe'
"""
# Implementation...Prefer Explicit Over Implicit:
# Bad: implicit behavior
def process(items):
return [x for x in items if x]
# Good: explicit intention
def filter_non_empty_items(items: list) -> list:
"""Remove None and empty string values from items."""
return [item for item in items if item is not None and item != ""]3. Modularity and Reusability (DRY Principle)
Single Responsibility Principle:
# Bad: function does too much
def process_and_save_report(data):
# Process data
cleaned = [x.strip() for x in data]
filtered = [x for x in cleaned if len(x) > 0]
# Calculate statistics
total = sum(len(x) for x in filtered)
avg = total / len(filtered)
# Format report
report = f"Total: {total}, Average: {avg}"
# Save to file
with open('report.txt', 'w') as f:
f.write(report)
return report
# Good: separate concerns
def clean_data(data: list[str]) -> list[str]:
"""Remove whitespace and empty strings."""
cleaned = [item.strip() for item in data]
return [item for item in cleaned if item]
def calculate_statistics(data: list[str]) -> dict:
"""Calculate length statistics for strings."""
lengths = [len(item) for item in data]
return {
'total': sum(lengths),
'average': sum(lengths) / len(lengths) if lengths else 0,
'count': len(lengths)
}
def format_report(stats: dict) -> str:
"""Format statistics as a readable report."""
return f"Total: {stats['total']}, Average: {stats['average']:.2f}"
def save_report(content: str, filepath: Path) -> None:
"""Save report content to file."""
filepath.write_text(content)
# Usage
cleaned = clean_data(data)
stats = calculate_statistics(cleaned)
report = format_report(stats)
save_report(report, Path('report.txt'))Avoid Duplication:
# Bad: repeated logic
def calculate_circle_area(radius):
return 3.14159 * radius * radius
def calculate_circle_circumference(radius):
return 2 * 3.14159 * radius
# Good: reusable constants and functions
import math
def calculate_circle_area(radius: float) -> float:
"""Calculate area of circle."""
return math.pi * radius ** 2
def calculate_circle_circumference(radius: float) -> float:
"""Calculate circumference of circle."""
return 2 * math.pi * radius
def calculate_circle_properties(radius: float) -> dict:
"""Calculate all circle properties."""
return {
'area': calculate_circle_area(radius),
'circumference': calculate_circle_circumference(radius)
}Use Classes for Related Functionality:
class DataValidator:
"""Validate data according to defined rules."""
def __init__(self, min_length: int = 0, max_length: int = 100):
self.min_length = min_length
self.max_length = max_length
def validate_length(self, value: str) -> bool:
"""Check if string length is within bounds."""
return self.min_length <= len(value) <= self.max_length
def validate_email(self, email: str) -> bool:
"""Check if email format is valid."""
return '@' in email and '.' in email.split('@')[1]
def validate_all(self, data: dict) -> dict[str, bool]:
"""Validate all fields in data dictionary."""
return {
'email': self.validate_email(data.get('email', '')),
'name': self.validate_length(data.get('name', ''))
}4. Testing and TDD
Write Testable Code:
# Bad: hard to test (depends on external state)
def get_config_value(key):
with open('/etc/myapp/config.ini') as f:
for line in f:
if line.startswith(key):
return line.split('=')[1].strip()
# Good: testable with dependency injection
def get_config_value(key: str, config_path: Path) -> str:
"""Get configuration value from file."""
content = config_path.read_text()
for line in content.splitlines():
if line.startswith(key):
return line.split('=')[1].strip()
raise KeyError(f"Config key '{key}' not found")Unit Test Structure:
import pytest
from mymodule import calculate_unit_price, UserNotFoundError
class TestCalculateUnitPrice:
"""Test suite for calculate_unit_price function."""
def test_basic_calculation(self):
"""Test basic price calculation without tax."""
result = calculate_unit_price(100.0, 10, 0.0)
assert result == 10.0
def test_with_tax(self):
"""Test price calculation with tax included."""
result = calculate_unit_price(100.0, 10, 0.2)
assert result == 12.0
def test_zero_quantity_raises_error(self):
"""Test that zero quantity raises ValueError."""
with pytest.raises(ZeroDivisionError):
calculate_unit_price(100.0, 0, 0.1)
@pytest.mark.parametrize("total,qty,tax,expected", [
(100, 10, 0.0, 10.0),
(100, 10, 0.1, 11.0),
(50, 5, 0.2, 12.0),
])
def test_multiple_scenarios(self, total, qty, tax, expected):
"""Test multiple calculation scenarios."""
assert calculate_unit_price(total, qty, tax) == pytest.approx(expected)TDD Approach:
# Step 1: Write the test first
def test_parse_csv_line():
"""Test CSV line parsing."""
result = parse_csv_line('John,Doe,30')
assert result == {'first': 'John', 'last': 'Doe', 'age': 30}
# Step 2: Implement minimal code to pass
def parse_csv_line(line: str) -> dict:
"""Parse CSV line into dictionary."""
parts = line.split(',')
return {
'first': parts[0],
'last': parts[1],
'age': int(parts[2])
}
# Step 3: Refactor while keeping tests green
def parse_csv_line(line: str, headers: list[str] = None) -> dict:
"""Parse CSV line into dictionary with optional headers."""
if headers is None:
headers = ['first', 'last', 'age']
parts = line.split(',')
result = {}
for i, header in enumerate(headers):
value = parts[i].strip()
# Convert to int if header is 'age'
result[header] = int(value) if header == 'age' else value
return result5. Error Handling
Use Specific Exceptions:
# Bad: generic exceptions
def divide(a, b):
if b == 0:
raise Exception("Can't divide by zero")
return a / b
# Good: specific exceptions
class DivisionByZeroError(ValueError):
"""Raised when attempting to divide by zero."""
pass
def divide(a: float, b: float) -> float:
"""Divide two numbers.
Args:
a: Numerator
b: Denominator
Returns:
Result of division
Raises:
DivisionByZeroError: If denominator is zero
"""
if b == 0:
raise DivisionByZeroError(f"Cannot divide {a} by zero")
return a / bProper Exception Handling:
# Bad: bare except
try:
result = risky_operation()
except:
print("Error occurred")
# Good: specific exceptions with context
import logging
logger = logging.getLogger(__name__)
def process_file(filepath: Path) -> dict:
"""Process file and return parsed data."""
try:
content = filepath.read_text()
return parse_content(content)
except FileNotFoundError:
logger.error(f"File not found: {filepath}")
raise
except PermissionError:
logger.error(f"Permission denied: {filepath}")
raise
except ValueError as e:
logger.error(f"Invalid content in {filepath}: {e}")
raise
except Exception as e:
logger.exception(f"Unexpected error processing {filepath}")
raiseContext Managers for Resource Management:
# Good: automatic cleanup
from pathlib import Path
from contextlib import contextmanager
@contextmanager
def open_database(db_path: Path):
"""Context manager for database connections."""
conn = connect_to_database(db_path)
try:
yield conn
finally:
conn.close()
# Usage
with open_database(Path('data.db')) as db:
results = db.query('SELECT * FROM users')6. Virtual Environments and Dependency Management
Using uv (Modern, Fast Package Manager):
# Create new project with uv
uv venv
# Activate virtual environment
source .venv/bin/activate # Linux/Mac
# or
.venv\Scripts\activate # Windows
# Install dependencies
uv pip install pandas numpy pytest
# Install with specific version
uv pip install "requests>=2.28.0,<3.0"
# Install development dependencies
uv pip install -e ".[dev]"
# Create requirements file
uv pip freeze > requirements.txt
# Install from requirements
uv pip install -r requirements.txtProject Structure with pyproject.toml:
[project]
name = "my-project"
version = "0.1.0"
description = "A well-structured Python project"
authors = [{name = "Your Name", email = "you@example.com"}]
requires-python = ">=3.11"
dependencies = [
"pandas>=2.0.0",
"numpy>=1.24.0",
"requests>=2.28.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.black]
line-length = 99
target-version = ['py311']
[tool.ruff]
line-length = 99
target-version = "py311"
select = ["E", "F", "I", "N", "W", "B", "C4"]
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_functions = "test_*"7. Modern Python Tooling
Ruff: Fast Linter and Formatter
# Install
uv pip install ruff
# Check code
ruff check .
# Auto-fix issues
ruff check --fix .
# Format code
ruff format .
# Configuration in pyproject.toml
[tool.ruff]
line-length = 99
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"W", # pycodestyle warnings
"B", # flake8-bugbear
"C4", # flake8-comprehensions
]
ignore = ["E501"] # line too long (handled by formatter)Black: Code Formatter
# Install
uv pip install black
# Format files
black myproject/
# Check without modifying
black --check myproject/
# Configuration
[tool.black]
line-length = 99
target-version = ['py311']
include = '\.pyi?$'Mypy: Static Type Checker
# Install
uv pip install mypy
# Check types
mypy myproject/
# Configuration
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = trueType Hints Examples:
from typing import Protocol, TypeVar, Generic
from collections.abc import Sequence, Callable
# Basic type hints
def greet(name: str) -> str:
return f"Hello, {name}"
# Collections
def process_items(items: list[int]) -> dict[str, int]:
return {'total': sum(items), 'count': len(items)}
# Optional values
from typing import Optional
def find_user(user_id: int) -> Optional[dict]:
"""Return user dict or None if not found."""
# ...
# Union types (Python 3.10+)
def parse_value(value: str | int) -> float:
return float(value)
# Callable
def apply_function(func: Callable[[int], int], value: int) -> int:
return func(value)
# Generic types
T = TypeVar('T')
def first_element(items: Sequence[T]) -> T | None:
return items[0] if items else None
# Protocol (structural subtyping)
class Drawable(Protocol):
def draw(self) -> None:
...
def render(obj: Drawable) -> None:
obj.draw()Best Practices Workflow
When Writing New Code:
1. Start with type hints and docstrings 2. Write tests first (TDD) - define expected behavior 3. Implement minimal code to pass tests 4. Refactor while keeping tests green 5. Run linter (ruff check) 6. Format code (ruff format or black) 7. Check types (mypy) 8. Run tests (pytest)
When Refactoring Existing Code:
1. Add tests if they don't exist 2. Run existing tests to establish baseline 3. Refactor incrementally (small changes) 4. Run tests after each change 5. Improve type coverage 6. Apply linter fixes 7. Update documentation
Common Patterns
Configuration Management:
from dataclasses import dataclass
from pathlib import Path
import tomllib
@dataclass
class Config:
"""Application configuration."""
database_url: str
api_key: str
timeout: int = 30
debug: bool = False
def load_config(config_path: Path) -> Config:
"""Load configuration from TOML file."""
with config_path.open('rb') as f:
data = tomllib.load(f)
return Config(**data)Logging:
import logging
from pathlib import Path
def setup_logging(log_level: str = "INFO", log_file: Path | None = None) -> None:
"""Configure application logging."""
handlers: list[logging.Handler] = [logging.StreamHandler()]
if log_file:
handlers.append(logging.FileHandler(log_file))
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=handlers
)
# Usage
logger = logging.getLogger(__name__)
logger.info("Application started")
logger.error("Error occurred", exc_info=True)CLI with argparse:
import argparse
from pathlib import Path
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description="Process data files")
parser.add_argument('input', type=Path, help="Input file path")
parser.add_argument('-o', '--output', type=Path, help="Output file path")
parser.add_argument('-v', '--verbose', action='store_true', help="Verbose output")
return parser.parse_args()
def main() -> None:
"""Main entry point."""
args = parse_args()
if args.verbose:
setup_logging("DEBUG")
process_file(args.input, args.output)
if __name__ == '__main__':
main()Instructions for Code Reviews
When reviewing or generating Python code, check for:
1. PEP 8 Compliance:
- Correct naming conventions
- Proper indentation (4 spaces)
- Appropriate line length
- Correct import ordering
2. Type Hints:
- All function signatures have type hints
- Return types are specified
- Complex types use proper typing constructs
3. Documentation:
- All public functions have docstrings
- Docstrings include Args, Returns, Raises sections
- Complex logic has explanatory comments
4. Error Handling:
- Specific exceptions are used
- Resources are properly cleaned up
- Error messages are informative
5. Testing:
- Tests exist for new functionality
- Edge cases are covered
- Tests are clear and maintainable
6. Code Quality:
- No code duplication
- Functions have single responsibility
- Magic numbers are replaced with named constants
- No overly complex functions (consider cyclomatic complexity)
Resources and Tools
Essential Tools:
- uv: Fast package installer and resolver
- Ruff: Fast Python linter and formatter (Rust-based)
- Black: Opinionated code formatter
- Mypy: Static type checker
- Pytest: Testing framework
- Pre-commit: Git hooks for code quality
Official References:
- PEP 8: https://peps.python.org/pep-0008/
- PEP 257: Docstring Conventions
- Python Type Hints: PEP 484, 585, 604
- Python Enhancement Proposals: https://peps.python.org/
Limitations
This skill focuses on general Python best practices. For specialized domains:
- Scientific computing: Consider numpy/scipy conventions
- Web development: Framework-specific patterns (Django, FastAPI)
- Data science: Jupyter notebook best practices
- Async programming: asyncio patterns and best practices
For these specialized areas, combine this skill with domain-specific skills or documentation.
"""Comprehensive demonstration of Python error handling best practices.
This module showcases:
- Custom exception hierarchies
- Proper exception handling patterns
- Context managers for resource management
- Error logging
- Recovery strategies
- Validation and defensive programming
"""
import logging
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import Any, TypeAlias
from collections.abc import Iterator
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# 1. Custom Exception Hierarchy
class ApplicationError(Exception):
"""Base exception for all application errors.
All application-specific exceptions should inherit from this.
"""
def __init__(self, message: str, details: dict[str, Any] | None = None):
"""Initialize exception.
Args:
message: Human-readable error message
details: Optional additional error context
"""
super().__init__(message)
self.message = message
self.details = details or {}
class ValidationError(ApplicationError):
"""Raised when data validation fails."""
def __init__(self, field: str, message: str, value: Any = None):
"""Initialize validation error.
Args:
field: Name of field that failed validation
message: Validation error message
value: The invalid value (optional)
"""
details = {"field": field, "value": value}
super().__init__(f"Validation failed for '{field}': {message}", details)
self.field = field
self.value = value
class ResourceNotFoundError(ApplicationError):
"""Raised when a required resource cannot be found."""
def __init__(self, resource_type: str, resource_id: str):
"""Initialize resource not found error.
Args:
resource_type: Type of resource (e.g., 'user', 'file')
resource_id: Identifier for the resource
"""
details = {"resource_type": resource_type, "resource_id": resource_id}
super().__init__(f"{resource_type} not found: {resource_id}", details)
class DatabaseError(ApplicationError):
"""Raised for database-related errors."""
pass
class ConfigurationError(ApplicationError):
"""Raised for configuration-related errors."""
pass
# 2. Validation Functions
def validate_email(email: str) -> str:
"""Validate email address.
Args:
email: Email address to validate
Returns:
Validated email address
Raises:
ValidationError: If email format is invalid
"""
if not email:
raise ValidationError("email", "Email cannot be empty", email)
if "@" not in email:
raise ValidationError("email", "Email must contain @", email)
parts = email.split("@")
if len(parts) != 2:
raise ValidationError("email", "Email must have exactly one @", email)
local, domain = parts
if not local or not domain:
raise ValidationError("email", "Email parts cannot be empty", email)
if "." not in domain:
raise ValidationError("email", "Domain must contain a dot", email)
return email.lower()
def validate_age(age: int) -> int:
"""Validate age value.
Args:
age: Age to validate
Returns:
Validated age
Raises:
ValidationError: If age is invalid
"""
if not isinstance(age, int):
raise ValidationError("age", "Age must be an integer", age)
if age < 0:
raise ValidationError("age", "Age cannot be negative", age)
if age > 150:
raise ValidationError("age", "Age is unrealistically high", age)
return age
# 3. Error Handling Patterns
def safe_divide(a: float, b: float) -> float | None:
"""Safely divide two numbers.
Args:
a: Numerator
b: Denominator
Returns:
Result of division, or None if division fails
Example:
>>> safe_divide(10, 2)
5.0
>>> safe_divide(10, 0)
None
"""
try:
return a / b
except ZeroDivisionError:
logger.warning(f"Division by zero attempted: {a} / {b}")
return None
except TypeError as e:
logger.error(f"Type error in division: {e}")
return None
def read_file_safe(filepath: Path) -> str | None:
"""Safely read file content.
Args:
filepath: Path to file
Returns:
File content or None if reading fails
"""
try:
return filepath.read_text(encoding="utf-8")
except FileNotFoundError:
logger.warning(f"File not found: {filepath}")
return None
except PermissionError:
logger.error(f"Permission denied reading: {filepath}")
return None
except UnicodeDecodeError:
logger.error(f"Failed to decode file as UTF-8: {filepath}")
return None
except Exception as e:
logger.exception(f"Unexpected error reading {filepath}")
return None
def process_file_with_recovery(filepath: Path) -> dict[str, Any]:
"""Process file with error recovery.
Args:
filepath: Path to file to process
Returns:
Processing results
Raises:
ApplicationError: If processing fails critically
"""
try:
if not filepath.exists():
raise ResourceNotFoundError("file", str(filepath))
content = filepath.read_text(encoding="utf-8")
# Process content
lines = [line.strip() for line in content.splitlines()]
return {"status": "success", "lines": len(lines), "size": filepath.stat().st_size}
except ResourceNotFoundError:
# This is expected, re-raise
raise
except PermissionError as e:
logger.error(f"Permission denied: {filepath}")
raise ApplicationError(f"Cannot access file: {filepath}") from e
except UnicodeDecodeError as e:
logger.error(f"Encoding error in {filepath}")
# Try recovery with different encoding
try:
content = filepath.read_text(encoding="latin-1")
logger.info(f"Successfully recovered using latin-1 encoding")
return {
"status": "recovered",
"lines": len(content.splitlines()),
"size": filepath.stat().st_size,
"encoding": "latin-1",
}
except Exception as recovery_error:
raise ApplicationError(f"Failed to read file with any encoding: {filepath}") from e
except Exception as e:
logger.exception(f"Unexpected error processing {filepath}")
raise ApplicationError(f"Processing failed: {filepath}") from e
# 4. Context Managers for Resource Management
@contextmanager
def open_file_safe(filepath: Path, mode: str = "r") -> Iterator[Any]:
"""Context manager for safe file handling.
Args:
filepath: Path to file
mode: File mode ('r', 'w', etc.)
Yields:
File handle
Raises:
ApplicationError: If file operations fail
"""
file_handle = None
try:
file_handle = filepath.open(mode, encoding="utf-8")
logger.debug(f"Opened file: {filepath}")
yield file_handle
except FileNotFoundError as e:
raise ResourceNotFoundError("file", str(filepath)) from e
except PermissionError as e:
raise ApplicationError(f"Permission denied: {filepath}") from e
finally:
if file_handle is not None:
try:
file_handle.close()
logger.debug(f"Closed file: {filepath}")
except Exception as e:
logger.error(f"Error closing file {filepath}: {e}")
@contextmanager
def transaction_scope(connection: Any) -> Iterator[None]:
"""Context manager for database transactions.
Args:
connection: Database connection
Yields:
None (transaction is active)
Raises:
DatabaseError: If transaction fails
"""
try:
logger.info("Starting transaction")
# connection.begin()
yield
# If no exception, commit
# connection.commit()
logger.info("Transaction committed")
except Exception as e:
# On any error, rollback
logger.error(f"Transaction failed, rolling back: {e}")
# connection.rollback()
raise DatabaseError("Transaction failed") from e
# 5. Retry Logic
def retry_on_failure(func: Any, max_attempts: int = 3, delay: float = 1.0) -> Any:
"""Retry function on failure.
Args:
func: Function to retry
max_attempts: Maximum number of attempts
delay: Delay between attempts in seconds
Returns:
Function result
Raises:
Exception: Last exception if all retries fail
"""
import time
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
logger.info(f"Attempt {attempt} of {max_attempts}")
return func()
except Exception as e:
last_exception = e
logger.warning(f"Attempt {attempt} failed: {e}", exc_info=attempt == max_attempts)
if attempt < max_attempts:
logger.info(f"Retrying in {delay} seconds...")
time.sleep(delay)
else:
logger.error("All retry attempts failed")
raise last_exception
# 6. Input Validation and Sanitization
def sanitize_filename(filename: str) -> str:
"""Sanitize filename to prevent path traversal.
Args:
filename: Original filename
Returns:
Sanitized filename
Raises:
ValidationError: If filename is invalid
"""
if not filename:
raise ValidationError("filename", "Filename cannot be empty", filename)
# Remove path separators
filename = filename.replace("/", "_").replace("\\", "_")
# Remove special characters
allowed_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-")
sanitized = "".join(c if c in allowed_chars else "_" for c in filename)
if not sanitized or sanitized in (".", ".."):
raise ValidationError("filename", "Invalid filename after sanitization", filename)
return sanitized
def validate_user_input(data: dict[str, Any]) -> dict[str, Any]:
"""Validate user input data.
Args:
data: User input dictionary
Returns:
Validated and sanitized data
Raises:
ValidationError: If validation fails
"""
validated = {}
# Required fields
required_fields = ["username", "email", "age"]
for field in required_fields:
if field not in data:
raise ValidationError(field, "Required field missing")
# Validate username
username = data["username"]
if not isinstance(username, str) or not username.strip():
raise ValidationError("username", "Username must be non-empty string", username)
if len(username) < 3:
raise ValidationError("username", "Username must be at least 3 characters", username)
if len(username) > 50:
raise ValidationError("username", "Username too long (max 50)", username)
validated["username"] = username.strip()
# Validate email
validated["email"] = validate_email(data["email"])
# Validate age
validated["age"] = validate_age(data["age"])
return validated
# 7. Error Recovery Strategies
class DataProcessor:
"""Data processor with comprehensive error handling."""
def __init__(self, strict_mode: bool = True):
"""Initialize processor.
Args:
strict_mode: If True, fail on first error. If False, collect errors.
"""
self.strict_mode = strict_mode
self.errors: list[tuple[str, Exception]] = []
def process_batch(self, items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Process batch of items with error handling.
Args:
items: List of items to process
Returns:
List of successfully processed items
Raises:
ApplicationError: If strict_mode and any item fails
"""
results = []
self.errors = []
for i, item in enumerate(items):
try:
result = self._process_item(item)
results.append(result)
except ValidationError as e:
error_context = f"Item {i}"
self.errors.append((error_context, e))
if self.strict_mode:
raise ApplicationError(f"Batch processing failed at item {i}") from e
else:
logger.warning(f"Skipping invalid item {i}: {e}")
except Exception as e:
error_context = f"Item {i}"
self.errors.append((error_context, e))
if self.strict_mode:
raise ApplicationError(f"Unexpected error at item {i}") from e
else:
logger.error(f"Error processing item {i}: {e}", exc_info=True)
if self.errors:
logger.info(f"Processed {len(results)} items successfully, {len(self.errors)} errors")
return results
def _process_item(self, item: dict[str, Any]) -> dict[str, Any]:
"""Process single item.
Args:
item: Item to process
Returns:
Processed item
Raises:
ValidationError: If item is invalid
"""
# Validate required fields
if "id" not in item:
raise ValidationError("id", "Missing required field")
# Process...
return {"id": item["id"], "processed": True}
# 8. Logging Best Practices
def demo_logging_levels():
"""Demonstrate different logging levels."""
# DEBUG: Detailed information for debugging
logger.debug("Detailed debug information")
# INFO: General informational messages
logger.info("Operation completed successfully")
# WARNING: Warning messages for unexpected but handled situations
logger.warning("Resource usage is high")
# ERROR: Error messages for failures that are handled
logger.error("Failed to process item, skipping")
# CRITICAL: Critical errors that may cause application failure
logger.critical("Database connection lost")
# Log with exception info
try:
raise ValueError("Example error")
except ValueError:
logger.exception("Error occurred during processing")
# 9. Main execution with error handling
def main() -> int:
"""Main entry point with error handling.
Returns:
Exit code (0 for success, non-zero for failure)
"""
try:
# Main application logic
logger.info("Application started")
# Example operations
result = process_file_with_recovery(Path("example.txt"))
logger.info(f"Processing result: {result}")
logger.info("Application completed successfully")
return 0
except ConfigurationError as e:
logger.error(f"Configuration error: {e}")
return 1
except ResourceNotFoundError as e:
logger.error(f"Resource not found: {e}")
return 2
except ValidationError as e:
logger.error(f"Validation error: {e}")
return 3
except ApplicationError as e:
logger.error(f"Application error: {e}")
if e.details:
logger.error(f"Error details: {e.details}")
return 4
except KeyboardInterrupt:
logger.info("Application interrupted by user")
return 130
except Exception as e:
logger.exception("Unexpected error occurred")
return 99
if __name__ == "__main__":
sys.exit(main())
"""Example of well-structured Python module following best practices.
This module demonstrates:
- PEP 8 compliance
- Proper type hints
- Comprehensive docstrings
- Error handling
- Modular design
- DRY principle
"""
import logging
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Protocol
# Constants
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
VALID_STATUSES = frozenset(["pending", "processing", "completed", "failed"])
# Configure logging
logger = logging.getLogger(__name__)
# Custom Exceptions
class DataProcessingError(Exception):
"""Base exception for data processing errors."""
pass
class ValidationError(DataProcessingError):
"""Raised when data validation fails."""
pass
class ResourceNotFoundError(DataProcessingError):
"""Raised when required resource is not found."""
pass
# Protocol for dependency injection
class DataStore(Protocol):
"""Protocol defining interface for data storage."""
def save(self, key: str, value: dict) -> None:
"""Save data to storage."""
...
def load(self, key: str) -> dict:
"""Load data from storage."""
...
# Dataclass for configuration
@dataclass
class ProcessorConfig:
"""Configuration for data processor.
Attributes:
input_dir: Directory containing input files
output_dir: Directory for processed files
timeout: Operation timeout in seconds
validate: Whether to validate data before processing
allowed_extensions: Set of allowed file extensions
"""
input_dir: Path
output_dir: Path
timeout: int = DEFAULT_TIMEOUT
validate: bool = True
allowed_extensions: set[str] = field(default_factory=lambda: {".txt", ".csv", ".json"})
def __post_init__(self):
"""Validate configuration after initialization."""
if not self.input_dir.exists():
raise ValueError(f"Input directory does not exist: {self.input_dir}")
if self.timeout <= 0:
raise ValueError(f"Timeout must be positive, got: {self.timeout}")
# Create output directory if it doesn't exist
self.output_dir.mkdir(parents=True, exist_ok=True)
# Main processing class
class DataProcessor:
"""Process data files with validation and error handling.
This class provides a robust interface for processing data files
with built-in validation, error handling, and logging.
Attributes:
config: Processor configuration
stats: Processing statistics
"""
def __init__(self, config: ProcessorConfig, store: DataStore | None = None):
"""Initialize data processor.
Args:
config: Processor configuration
store: Optional data store for persistence
"""
self.config = config
self.store = store
self.stats = {"processed": 0, "failed": 0, "skipped": 0}
logger.info(f"DataProcessor initialized with config: {config}")
def process_file(self, filepath: Path) -> dict[str, any]:
"""Process a single file.
Args:
filepath: Path to file to process
Returns:
Dictionary containing processing results with keys:
- 'status': Processing status
- 'lines': Number of lines processed
- 'timestamp': Processing timestamp
Raises:
ResourceNotFoundError: If file doesn't exist
ValidationError: If file validation fails
DataProcessingError: If processing fails
"""
if not filepath.exists():
raise ResourceNotFoundError(f"File not found: {filepath}")
if self.config.validate and not self._validate_file(filepath):
raise ValidationError(f"File validation failed: {filepath}")
try:
content = self._read_file(filepath)
processed_data = self._process_content(content)
output_path = self._write_output(filepath, processed_data)
result = {
"status": "completed",
"lines": len(content.splitlines()),
"timestamp": datetime.now().isoformat(),
"output": str(output_path),
}
self.stats["processed"] += 1
logger.info(f"Successfully processed {filepath}")
return result
except Exception as e:
self.stats["failed"] += 1
logger.error(f"Failed to process {filepath}: {e}", exc_info=True)
raise DataProcessingError(f"Processing failed for {filepath}") from e
def process_directory(self) -> list[dict[str, any]]:
"""Process all files in input directory.
Returns:
List of processing results for each file
Example:
>>> config = ProcessorConfig(Path("input"), Path("output"))
>>> processor = DataProcessor(config)
>>> results = processor.process_directory()
>>> print(f"Processed {len(results)} files")
"""
results = []
for filepath in self._get_files():
try:
result = self.process_file(filepath)
results.append(result)
except DataProcessingError as e:
logger.warning(f"Skipping file {filepath}: {e}")
self.stats["skipped"] += 1
continue
logger.info(f"Processing complete. Stats: {self.stats}")
return results
def get_statistics(self) -> dict[str, int]:
"""Get processing statistics.
Returns:
Dictionary with processing counts
"""
return self.stats.copy()
def reset_statistics(self) -> None:
"""Reset all statistics to zero."""
for key in self.stats:
self.stats[key] = 0
def _validate_file(self, filepath: Path) -> bool:
"""Validate file before processing.
Args:
filepath: Path to file
Returns:
True if file is valid
"""
# Check file extension
if filepath.suffix not in self.config.allowed_extensions:
logger.warning(
f"Invalid extension {filepath.suffix}, allowed: {self.config.allowed_extensions}"
)
return False
# Check file is not empty
if filepath.stat().st_size == 0:
logger.warning(f"File is empty: {filepath}")
return False
return True
def _read_file(self, filepath: Path) -> str:
"""Read file content.
Args:
filepath: Path to file
Returns:
File content as string
Raises:
DataProcessingError: If reading fails
"""
try:
return filepath.read_text(encoding="utf-8")
except UnicodeDecodeError as e:
raise DataProcessingError(f"Failed to decode {filepath}") from e
except IOError as e:
raise DataProcessingError(f"Failed to read {filepath}") from e
def _process_content(self, content: str) -> str:
"""Process file content.
Args:
content: Raw file content
Returns:
Processed content
"""
# Remove empty lines
lines = [line.strip() for line in content.splitlines() if line.strip()]
# Add processing metadata
header = f"# Processed at {datetime.now().isoformat()}\n"
footer = f"\n# Total lines: {len(lines)}"
return header + "\n".join(lines) + footer
def _write_output(self, original_path: Path, content: str) -> Path:
"""Write processed content to output file.
Args:
original_path: Original file path
content: Processed content
Returns:
Path to output file
"""
output_path = self.config.output_dir / f"processed_{original_path.name}"
try:
output_path.write_text(content, encoding="utf-8")
logger.debug(f"Wrote output to {output_path}")
return output_path
except IOError as e:
raise DataProcessingError(f"Failed to write output: {output_path}") from e
def _get_files(self) -> list[Path]:
"""Get list of files to process.
Returns:
Sorted list of file paths
"""
files = []
for ext in self.config.allowed_extensions:
files.extend(self.config.input_dir.glob(f"*{ext}"))
return sorted(files)
# Utility functions
def create_processor_from_config_file(config_path: Path) -> DataProcessor:
"""Create processor from configuration file.
Args:
config_path: Path to TOML configuration file
Returns:
Configured DataProcessor instance
Example:
>>> processor = create_processor_from_config_file(Path("config.toml"))
>>> results = processor.process_directory()
"""
import tomllib
with config_path.open("rb") as f:
config_data = tomllib.load(f)
config = ProcessorConfig(
input_dir=Path(config_data["input_dir"]),
output_dir=Path(config_data["output_dir"]),
timeout=config_data.get("timeout", DEFAULT_TIMEOUT),
validate=config_data.get("validate", True),
)
return DataProcessor(config)
def main() -> None:
"""Main entry point for command-line usage."""
import argparse
parser = argparse.ArgumentParser(description="Process data files")
parser.add_argument("input_dir", type=Path, help="Input directory")
parser.add_argument("output_dir", type=Path, help="Output directory")
parser.add_argument(
"--timeout",
type=int,
default=DEFAULT_TIMEOUT,
help=f"Timeout in seconds (default: {DEFAULT_TIMEOUT})",
)
parser.add_argument("--no-validate", action="store_true", help="Skip validation")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
args = parser.parse_args()
# Configure logging
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# Create processor
config = ProcessorConfig(
input_dir=args.input_dir,
output_dir=args.output_dir,
timeout=args.timeout,
validate=not args.no_validate,
)
processor = DataProcessor(config)
# Process files
try:
results = processor.process_directory()
stats = processor.get_statistics()
print(f"\nProcessing complete!")
print(f"Processed: {stats['processed']}")
print(f"Failed: {stats['failed']}")
print(f"Skipped: {stats['skipped']}")
except Exception as e:
logger.exception("Processing failed")
print(f"Error: {e}")
return
if __name__ == "__main__":
main()
"""Example test suite demonstrating Python testing best practices.
This module shows:
- pytest usage and fixtures
- Test organization and naming
- Parametrized tests
- Mocking and patching
- Test coverage patterns
- TDD workflow
"""
import pytest
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime
from typing import Any
# Sample code under test (normally in separate module)
class Calculator:
"""Simple calculator for demonstration."""
def add(self, a: float, b: float) -> float:
"""Add two numbers."""
return a + b
def subtract(self, a: float, b: float) -> float:
"""Subtract b from a."""
return a - b
def divide(self, a: float, b: float) -> float:
"""Divide a by b."""
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
def multiply(self, a: float, b: float) -> float:
"""Multiply two numbers."""
return a * b
class UserManager:
"""Manage users (example with external dependencies)."""
def __init__(self, database):
self.database = database
def create_user(self, username: str, email: str) -> dict[str, Any]:
"""Create new user."""
if self.database.user_exists(username):
raise ValueError(f"User {username} already exists")
user_data = {"username": username, "email": email, "created_at": datetime.now().isoformat()}
self.database.save_user(user_data)
return user_data
def get_user(self, username: str) -> dict[str, Any] | None:
"""Get user by username."""
return self.database.find_user(username)
# Test Suite for Calculator
class TestCalculator:
"""Test suite for Calculator class.
Demonstrates:
- Fixture usage
- Basic assertions
- Exception testing
- Parametrized tests
"""
@pytest.fixture
def calc(self) -> Calculator:
"""Provide Calculator instance for tests.
This fixture is automatically injected into test methods
that have 'calc' parameter.
"""
return Calculator()
def test_addition(self, calc: Calculator):
"""Test basic addition."""
result = calc.add(2, 3)
assert result == 5
def test_addition_negative_numbers(self, calc: Calculator):
"""Test addition with negative numbers."""
result = calc.add(-1, -1)
assert result == -2
def test_addition_floats(self, calc: Calculator):
"""Test addition with floating point numbers."""
result = calc.add(1.5, 2.5)
assert result == pytest.approx(4.0)
def test_subtraction(self, calc: Calculator):
"""Test basic subtraction."""
result = calc.subtract(5, 3)
assert result == 2
def test_multiplication(self, calc: Calculator):
"""Test basic multiplication."""
result = calc.multiply(4, 3)
assert result == 12
def test_division(self, calc: Calculator):
"""Test basic division."""
result = calc.divide(10, 2)
assert result == 5.0
def test_division_by_zero_raises_error(self, calc: Calculator):
"""Test that division by zero raises ZeroDivisionError."""
with pytest.raises(ZeroDivisionError) as exc_info:
calc.divide(10, 0)
assert "Cannot divide by zero" in str(exc_info.value)
# Parametrized tests - run same test with different inputs
@pytest.mark.parametrize(
"a,b,expected",
[
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, -50, 50),
(1.5, 2.5, 4.0),
],
)
def test_add_parametrized(self, calc: Calculator, a: float, b: float, expected: float):
"""Test addition with multiple parameter sets."""
result = calc.add(a, b)
assert result == pytest.approx(expected)
@pytest.mark.parametrize(
"a,b,expected",
[
(10, 2, 5.0),
(9, 3, 3.0),
(1, 2, 0.5),
(-10, 2, -5.0),
],
)
def test_divide_parametrized(self, calc: Calculator, a: float, b: float, expected: float):
"""Test division with multiple parameter sets."""
result = calc.divide(a, b)
assert result == pytest.approx(expected)
@pytest.mark.parametrize(
"dividend,divisor",
[
(10, 0),
(0, 0),
(-5, 0),
],
)
def test_divide_by_zero_parametrized(self, calc: Calculator, dividend: float, divisor: float):
"""Test division by zero with multiple values."""
with pytest.raises(ZeroDivisionError):
calc.divide(dividend, divisor)
# Test Suite with Mocking
class TestUserManager:
"""Test suite for UserManager class.
Demonstrates:
- Mocking external dependencies
- Patching
- Assertion on mock calls
"""
@pytest.fixture
def mock_database(self) -> Mock:
"""Provide mock database for testing."""
mock_db = Mock()
mock_db.user_exists.return_value = False
mock_db.find_user.return_value = None
return mock_db
@pytest.fixture
def user_manager(self, mock_database: Mock) -> UserManager:
"""Provide UserManager with mock database."""
return UserManager(mock_database)
def test_create_user_success(self, user_manager: UserManager, mock_database: Mock):
"""Test successful user creation."""
result = user_manager.create_user("alice", "alice@example.com")
# Assert return value
assert result["username"] == "alice"
assert result["email"] == "alice@example.com"
assert "created_at" in result
# Assert database methods were called correctly
mock_database.user_exists.assert_called_once_with("alice")
mock_database.save_user.assert_called_once()
def test_create_user_already_exists(self, user_manager: UserManager, mock_database: Mock):
"""Test that creating existing user raises error."""
# Configure mock to indicate user exists
mock_database.user_exists.return_value = True
with pytest.raises(ValueError) as exc_info:
user_manager.create_user("alice", "alice@example.com")
assert "already exists" in str(exc_info.value)
# Verify save was never called
mock_database.save_user.assert_not_called()
def test_get_user_found(self, user_manager: UserManager, mock_database: Mock):
"""Test getting existing user."""
# Configure mock return value
expected_user = {"username": "alice", "email": "alice@example.com"}
mock_database.find_user.return_value = expected_user
result = user_manager.get_user("alice")
assert result == expected_user
mock_database.find_user.assert_called_once_with("alice")
def test_get_user_not_found(self, user_manager: UserManager, mock_database: Mock):
"""Test getting non-existent user."""
mock_database.find_user.return_value = None
result = user_manager.get_user("nonexistent")
assert result is None
# Advanced Fixtures
@pytest.fixture(scope="session")
def test_data_dir(tmp_path_factory) -> Path:
"""Create temporary directory for test data (session scope).
This fixture is created once per test session and shared
across all tests.
"""
data_dir = tmp_path_factory.mktemp("test_data")
return data_dir
@pytest.fixture
def sample_file(tmp_path: Path) -> Path:
"""Create sample file for testing (function scope).
This fixture creates a new file for each test function.
"""
file_path = tmp_path / "sample.txt"
file_path.write_text("Sample content\nLine 2\nLine 3")
return file_path
@pytest.fixture(autouse=True)
def reset_state():
"""Fixture that runs automatically before each test.
Use autouse=True for setup/teardown that should always run.
"""
# Setup code here
yield
# Teardown code here (runs after test)
# Test using file fixtures
def test_read_file(sample_file: Path):
"""Test reading file content."""
content = sample_file.read_text()
assert "Sample content" in content
assert len(content.splitlines()) == 3
# Patching examples
@patch("datetime.datetime")
def test_with_patched_datetime(mock_datetime):
"""Test with patched datetime."""
# Configure mock
mock_datetime.now.return_value = datetime(2025, 1, 1, 12, 0, 0)
# Code under test would use datetime.now()
from datetime import datetime as dt
# Note: In real code, you'd patch where it's used, not where it's defined
mock_datetime.now.assert_called()
# Test with context manager patching
def test_with_context_manager_patch():
"""Test using patch as context manager."""
with patch("builtins.open", create=True) as mock_open:
mock_open.return_value.__enter__.return_value.read.return_value = "mocked content"
# Code that uses open() would get mocked version
# with open('file.txt') as f:
# content = f.read()
mock_open.assert_called()
# Markers for test organization
@pytest.mark.slow
def test_slow_operation():
"""Test marked as slow (can be skipped with -m "not slow")."""
# Simulate slow operation
import time
time.sleep(0.1)
assert True
@pytest.mark.integration
def test_integration():
"""Integration test (can run separately with -m integration)."""
assert True
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
"""Test that is currently skipped."""
pass
@pytest.mark.skipif(
condition=True, # Replace with actual condition
reason="Skipped on certain conditions",
)
def test_conditional_skip():
"""Test skipped based on condition."""
pass
@pytest.mark.xfail(reason="Known bug")
def test_known_bug():
"""Test expected to fail (xfail)."""
assert False
# Custom assertions and helpers
def assert_valid_email(email: str) -> None:
"""Custom assertion for email validation."""
assert "@" in email, f"Invalid email: {email}"
assert "." in email.split("@")[1], f"Invalid email domain: {email}"
def test_custom_assertion():
"""Test using custom assertion."""
assert_valid_email("user@example.com")
# TDD Example: Write test first
def test_parse_config_from_dict():
"""Test configuration parsing (TDD - test first)."""
from dataclasses import dataclass
@dataclass
class Config:
host: str
port: int
debug: bool
def parse_config(data: dict) -> Config:
return Config(host=data["host"], port=int(data["port"]), debug=data.get("debug", False))
# Now test the implementation
result = parse_config({"host": "localhost", "port": "8080"})
assert result.host == "localhost"
assert result.port == 8080
assert result.debug is False
# Fixture combinations
@pytest.fixture
def user_data() -> dict[str, str]:
"""Provide test user data."""
return {"username": "testuser", "email": "test@example.com", "password": "secure123"}
@pytest.fixture
def admin_user_data(user_data: dict[str, str]) -> dict[str, str]:
"""Provide admin user data (depends on user_data fixture)."""
data = user_data.copy()
data["is_admin"] = True
return data
def test_fixture_dependencies(admin_user_data: dict[str, str]):
"""Test using fixture that depends on another fixture."""
assert admin_user_data["username"] == "testuser"
assert admin_user_data["is_admin"] is True
# Conftest.py patterns (typically in conftest.py file)
def pytest_configure(config):
"""Register custom markers."""
config.addinivalue_line("markers", "slow: mark test as slow")
config.addinivalue_line("markers", "integration: mark test as integration test")
# Running tests:
# pytest # Run all tests
# pytest -v # Verbose output
# pytest -k test_addition # Run tests matching pattern
# pytest -m slow # Run tests with 'slow' marker
# pytest -m "not slow" # Skip slow tests
# pytest --cov=module # Run with coverage
# pytest --cov-report=html # Generate HTML coverage report
# pytest -x # Stop at first failure
# pytest --lf # Run last failed tests
# pytest --pdb # Drop into debugger on failure
"""Comprehensive demonstration of Python type hints and static typing.
This module showcases:
- Basic type annotations
- Collection types
- Optional and Union types
- Generic types
- Protocols (structural subtyping)
- Type aliases
- Callable types
- TypeVar and Generic classes
- Literal types
- Final types
"""
from collections.abc import Callable, Iterator, Sequence, Mapping
from dataclasses import dataclass
from typing import Any, TypeVar, Generic, Protocol, TypeAlias, Literal, Final, TypedDict, overload
# Basic type hints
def greet(name: str) -> str:
"""Simple function with basic types."""
return f"Hello, {name}!"
def calculate_total(prices: list[float], tax_rate: float = 0.1) -> float:
"""Calculate total with tax."""
subtotal = sum(prices)
return subtotal * (1 + tax_rate)
# Collection types (Python 3.9+ syntax)
def process_scores(scores: dict[str, int]) -> list[tuple[str, int]]:
"""Process and sort scores."""
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
def filter_items(items: set[str], exclude: frozenset[str]) -> set[str]:
"""Filter items excluding certain values."""
return items - exclude
# Optional types (can be None)
def find_user(user_id: int) -> dict[str, Any] | None:
"""Find user by ID, return None if not found."""
# Implementation...
return None
def get_config_value(key: str, default: str | None = None) -> str | None:
"""Get configuration value with optional default."""
# Implementation...
return default
# Union types (multiple possible types)
def parse_value(value: str | int | float) -> float:
"""Parse value to float from various types."""
return float(value)
# Use typing.Union for Python < 3.10
# from typing import Union
# def parse_value(value: Union[str, int, float]) -> float:
# return float(value)
# Type aliases for complex types
UserId: TypeAlias = int
UserData: TypeAlias = dict[str, Any]
Coordinates: TypeAlias = tuple[float, float]
JsonValue: TypeAlias = str | int | float | bool | None | dict[str, Any] | list[Any]
def get_user_location(user_id: UserId) -> Coordinates:
"""Get user coordinates."""
return (0.0, 0.0)
def parse_json(data: str) -> JsonValue:
"""Parse JSON data."""
import json
return json.loads(data)
# Callable types (functions as parameters)
def apply_operation(values: list[int], operation: Callable[[int], int]) -> list[int]:
"""Apply operation to each value."""
return [operation(x) for x in values]
def create_multiplier(factor: int) -> Callable[[int], int]:
"""Create a function that multiplies by factor."""
def multiply(x: int) -> int:
return x * factor
return multiply
# More complex callable signatures
def apply_binary_op(a: int, b: int, operation: Callable[[int, int], int]) -> int:
"""Apply binary operation."""
return operation(a, b)
# Generic TypeVar
T = TypeVar("T")
def first_element(items: Sequence[T]) -> T | None:
"""Get first element from sequence."""
return items[0] if items else None
def last_element(items: list[T]) -> T | None:
"""Get last element from list."""
return items[-1] if items else None
# Constrained TypeVar
NumberT = TypeVar("NumberT", int, float)
def add_numbers(a: NumberT, b: NumberT) -> NumberT:
"""Add two numbers of same type."""
return a + b # type: ignore
# Bounded TypeVar
class Comparable(Protocol):
"""Protocol for comparable objects."""
def __lt__(self, other: Any) -> bool: ...
def __gt__(self, other: Any) -> bool: ...
CT = TypeVar("CT", bound=Comparable)
def get_max(items: Sequence[CT]) -> CT:
"""Get maximum value from sequence."""
return max(items)
# Generic classes
class Stack(Generic[T]):
"""Generic stack implementation."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
"""Add item to stack."""
self._items.append(item)
def pop(self) -> T:
"""Remove and return top item."""
if not self._items:
raise IndexError("Stack is empty")
return self._items.pop()
def peek(self) -> T | None:
"""View top item without removing."""
return self._items[-1] if self._items else None
def is_empty(self) -> bool:
"""Check if stack is empty."""
return len(self._items) == 0
def __len__(self) -> int:
"""Get stack size."""
return len(self._items)
# Usage of generic stack
def demo_generic_stack() -> None:
"""Demonstrate generic stack usage."""
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value: int = int_stack.pop() # Type checker knows this is int
str_stack: Stack[str] = Stack()
str_stack.push("hello")
text: str = str_stack.pop() # Type checker knows this is str
# Protocols (structural subtyping / duck typing)
class Drawable(Protocol):
"""Protocol for objects that can be drawn."""
def draw(self) -> str:
"""Draw the object."""
...
def get_position(self) -> tuple[int, int]:
"""Get object position."""
...
class Circle:
"""Circle implementation (doesn't explicitly inherit Drawable)."""
def __init__(self, x: int, y: int, radius: int):
self.x = x
self.y = y
self.radius = radius
def draw(self) -> str:
"""Draw circle."""
return f"Circle at ({self.x}, {self.y}) with radius {self.radius}"
def get_position(self) -> tuple[int, int]:
"""Get circle position."""
return (self.x, self.y)
class Rectangle:
"""Rectangle implementation."""
def __init__(self, x: int, y: int, width: int, height: int):
self.x = x
self.y = y
self.width = width
self.height = height
def draw(self) -> str:
"""Draw rectangle."""
return f"Rectangle at ({self.x}, {self.y}) size {self.width}x{self.height}"
def get_position(self) -> tuple[int, int]:
"""Get rectangle position."""
return (self.x, self.y)
def render(shape: Drawable) -> None:
"""Render any drawable object."""
print(shape.draw())
def render_all(shapes: Sequence[Drawable]) -> None:
"""Render multiple shapes."""
for shape in shapes:
render(shape)
# TypedDict for structured dictionaries
class UserDict(TypedDict):
"""Type definition for user dictionary."""
username: str
email: str
age: int
is_admin: bool
class UserDictOptional(TypedDict, total=False):
"""User dict with optional fields."""
username: str
email: str
age: int
phone: str # This field is optional
def create_user(username: str, email: str, age: int) -> UserDict:
"""Create user dictionary."""
return {"username": username, "email": email, "age": age, "is_admin": False}
def process_user(user: UserDict) -> str:
"""Process user data."""
# Type checker knows all required fields exist
return f"{user['username']} ({user['email']})"
# Literal types (specific values only)
Status: TypeAlias = Literal["pending", "approved", "rejected"]
LogLevel: TypeAlias = Literal["DEBUG", "INFO", "WARNING", "ERROR"]
def set_status(item_id: int, status: Status) -> None:
"""Set item status (only specific values allowed)."""
print(f"Setting status of {item_id} to {status}")
def log_message(message: str, level: LogLevel = "INFO") -> None:
"""Log message with specific level."""
print(f"[{level}] {message}")
# Final - cannot be overridden/reassigned
MAX_SIZE: Final = 100
API_KEY: Final[str] = "secret-key"
class BaseConfig:
"""Base configuration class."""
MAX_CONNECTIONS: Final = 50 # Cannot be overridden in subclasses
# Final method (cannot be overridden)
def get_version(self) -> str:
"""Get version (cannot be overridden)."""
return "1.0.0"
# Overload - multiple signatures
@overload
def process(value: int) -> str: ...
@overload
def process(value: str) -> int: ...
def process(value: int | str) -> str | int:
"""Process value (different return type based on input)."""
if isinstance(value, int):
return str(value)
else:
return len(value)
# Complex generic with multiple type parameters
KT = TypeVar("KT")
VT = TypeVar("VT")
class BiMap(Generic[KT, VT]):
"""Bidirectional map."""
def __init__(self) -> None:
self._forward: dict[KT, VT] = {}
self._reverse: dict[VT, KT] = {}
def set(self, key: KT, value: VT) -> None:
"""Set key-value pair."""
self._forward[key] = value
self._reverse[value] = key
def get_by_key(self, key: KT) -> VT | None:
"""Get value by key."""
return self._forward.get(key)
def get_by_value(self, value: VT) -> KT | None:
"""Get key by value."""
return self._reverse.get(value)
# Iterator and Generator types
def count_up(n: int) -> Iterator[int]:
"""Generate numbers from 0 to n-1."""
for i in range(n):
yield i
def fibonacci(n: int) -> Iterator[int]:
"""Generate first n Fibonacci numbers."""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
# Complex return type
def split_by_type(items: Sequence[int | str]) -> tuple[list[int], list[str]]:
"""Split items into integers and strings."""
integers: list[int] = []
strings: list[str] = []
for item in items:
if isinstance(item, int):
integers.append(item)
elif isinstance(item, str):
strings.append(item)
return integers, strings
# Dataclass with type hints
@dataclass
class Point:
"""2D point with type annotations."""
x: float
y: float
def distance_from_origin(self) -> float:
"""Calculate distance from origin."""
return (self.x**2 + self.y**2) ** 0.5
@dataclass
class Circle2:
"""Circle with center point."""
center: Point
radius: float
def area(self) -> float:
"""Calculate circle area."""
import math
return math.pi * self.radius**2
# Context manager with type hints
from contextlib import contextmanager
from collections.abc import Iterator as IteratorType
@contextmanager
def temporary_file(path: str) -> IteratorType[str]:
"""Context manager for temporary file."""
# Setup
with open(path, "w") as f:
f.write("temp")
try:
yield path
finally:
# Cleanup
import os
os.remove(path)
# Class with type hints for all methods
class Database:
"""Database connection with full type annotations."""
def __init__(self, connection_string: str, timeout: int = 30) -> None:
"""Initialize database connection."""
self.connection_string = connection_string
self.timeout = timeout
self._connected: bool = False
def connect(self) -> bool:
"""Establish connection."""
self._connected = True
return True
def disconnect(self) -> None:
"""Close connection."""
self._connected = False
def execute(self, query: str, parameters: dict[str, Any] | None = None) -> list[dict[str, Any]]:
"""Execute query and return results."""
if parameters is None:
parameters = {}
# Implementation...
return []
def is_connected(self) -> bool:
"""Check if connected."""
return self._connected
# Using type hints with async code
async def fetch_data(url: str) -> dict[str, Any]:
"""Async function to fetch data."""
# Implementation...
return {}
async def fetch_multiple(urls: Sequence[str]) -> list[dict[str, Any]]:
"""Fetch data from multiple URLs concurrently."""
# Implementation...
return []
# Type narrowing with isinstance
def process_input(value: str | int | list[str]) -> str:
"""Process different input types with type narrowing."""
if isinstance(value, str):
# Type checker knows value is str here
return value.upper()
elif isinstance(value, int):
# Type checker knows value is int here
return str(value * 2)
else:
# Type checker knows value is list[str] here
return ", ".join(value)
# Self type for fluent interfaces
from typing import Self
class Builder:
"""Builder with fluent interface."""
def __init__(self) -> None:
self._value: str = ""
def add(self, text: str) -> Self:
"""Add text and return self."""
self._value += text
return self
def build(self) -> str:
"""Build final string."""
return self._value
# Usage demonstrates type safety:
# builder = Builder()
# result: str = builder.add("Hello").add(" World").build()
if __name__ == "__main__":
# Demonstrate various type-safe operations
print(greet("World"))
print(calculate_total([10.0, 20.0, 30.0]))
# Generic stack
demo_generic_stack()
# Protocols
shapes: list[Drawable] = [Circle(10, 10, 5), Rectangle(20, 20, 10, 15)]
render_all(shapes)
# Literal types
set_status(1, "approved")
log_message("Application started", "INFO")
Python Best Practices Quick Reference
A concise reference guide for Python best practices, tooling commands, and common patterns.
Quick Setup
Initialize New Project with uv
# Create virtual environment
uv venv
# Activate environment
source .venv/bin/activate # Unix/Mac
.venv\Scripts\activate # Windows
# Install dependencies
uv pip install pandas numpy pytest black ruff mypy
# Create requirements file
uv pip freeze > requirements.txtProject Structure
my_project/
├── pyproject.toml # Project metadata and tool config
├── README.md # Project documentation
├── .gitignore # Git ignore patterns
├── src/
│ └── my_project/ # Source code package
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── tests/ # Test files
│ ├── __init__.py
│ ├── test_core.py
│ └── test_utils.py
└── docs/ # DocumentationTooling Commands
Ruff (Linting and Formatting)
# Check code for issues
ruff check .
# Auto-fix issues
ruff check --fix .
# Format code
ruff format .
# Check formatting without changes
ruff format --check .
# Run on specific file
ruff check src/my_module.pyBlack (Formatting)
# Format all files
black .
# Check without modifying
black --check .
# Format specific file
black src/my_module.py
# Show diff
black --diff src/my_module.pyMypy (Type Checking)
# Check all files
mypy src/
# Check specific file
mypy src/my_module.py
# Ignore missing imports
mypy --ignore-missing-imports src/
# Generate report
mypy --html-report mypy_report src/Pytest (Testing)
# Run all tests
pytest
# Run with coverage
pytest --cov=src --cov-report=html
# Run specific test file
pytest tests/test_core.py
# Run specific test function
pytest tests/test_core.py::test_function_name
# Run with verbose output
pytest -v
# Run only failed tests
pytest --lf
# Run and stop at first failure
pytest -xPEP 8 Quick Reference
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Function | snake_case | def calculate_total(): |
| Variable | snake_case | user_count = 10 |
| Constant | UPPER_CASE | MAX_SIZE = 100 |
| Class | PascalCase | class UserAccount: |
| Private | _leading_underscore | def _internal_method(): |
| Module | snake_case | data_processor.py |
| Package | snake_case | mypackage/ |
Import Organization
# 1. Standard library imports
import os
import sys
from pathlib import Path
# 2. Third-party imports
import numpy as np
import pandas as pd
import requests
# 3. Local application imports
from myapp.core import process_data
from myapp.utils import validate_inputLine Length and Formatting
# Maximum 79 characters per line (or 99 for code)
# Good: break long function calls
result = some_function(
argument_one,
argument_two,
argument_three,
keyword_arg=value
)
# Good: break long conditionals
if (condition_one and condition_two
and condition_three):
do_something()
# Good: break long strings
message = (
"This is a very long message that "
"spans multiple lines for better "
"readability."
)Type Hints Cheat Sheet
Basic Types
from typing import Optional
# Simple types
name: str = "Alice"
age: int = 30
price: float = 19.99
is_active: bool = True
# Collections
numbers: list[int] = [1, 2, 3]
scores: dict[str, int] = {"alice": 90, "bob": 85}
coordinates: tuple[int, int] = (10, 20)
unique_ids: set[int] = {1, 2, 3}
# Optional (can be None)
middle_name: str | None = None # Python 3.10+
middle_name: Optional[str] = None # Older syntaxFunction Signatures
from collections.abc import Sequence, Callable
# Basic function
def greet(name: str) -> str:
return f"Hello, {name}"
# Multiple parameters
def add(a: int, b: int) -> int:
return a + b
# Default values
def repeat(text: str, times: int = 3) -> str:
return text * times
# No return value
def log_message(message: str) -> None:
print(message)
# Multiple return types
def divide(a: float, b: float) -> float | None:
return a / b if b != 0 else None
# Sequence (list, tuple, etc.)
def sum_values(values: Sequence[int]) -> int:
return sum(values)
# Callable (function as parameter)
def apply(func: Callable[[int], int], value: int) -> int:
return func(value)Advanced Types
from typing import TypeVar, Generic, Protocol
from collections.abc import Iterator
# Generic type variable
T = TypeVar('T')
def first_or_none(items: list[T]) -> T | None:
return items[0] if items else None
# Generic class
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# Protocol (structural typing)
class Drawable(Protocol):
def draw(self) -> None: ...
def render(obj: Drawable) -> None:
obj.draw()
# Iterator
def count_up(n: int) -> Iterator[int]:
for i in range(n):
yield iDocstring Templates
Function Docstring (Google Style)
def calculate_discount(price: float, discount_percent: float) -> float:
"""Calculate discounted price.
Args:
price: Original price in dollars
discount_percent: Discount percentage (0-100)
Returns:
Final price after discount
Raises:
ValueError: If discount_percent is not in range [0, 100]
Example:
>>> calculate_discount(100.0, 20.0)
80.0
"""
if not 0 <= discount_percent <= 100:
raise ValueError("Discount must be between 0 and 100")
return price * (1 - discount_percent / 100)Class Docstring
class CustomerAccount:
"""Manage customer account information.
This class handles customer data including personal information,
transaction history, and account status.
Attributes:
account_id: Unique identifier for the account
name: Customer full name
balance: Current account balance in dollars
is_active: Whether the account is currently active
Example:
>>> account = CustomerAccount("A001", "John Doe")
>>> account.deposit(100.0)
>>> print(account.balance)
100.0
"""
def __init__(self, account_id: str, name: str):
"""Initialize customer account.
Args:
account_id: Unique account identifier
name: Customer's full name
"""
self.account_id = account_id
self.name = name
self.balance = 0.0
self.is_active = TrueCommon Patterns
Context Manager
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def temporary_setting(config: dict, key: str, value: any) -> Iterator[None]:
"""Temporarily change a configuration setting.
Args:
config: Configuration dictionary
key: Setting key to change
value: Temporary value
Yields:
None
"""
original_value = config.get(key)
config[key] = value
try:
yield
finally:
config[key] = original_value
# Usage
with temporary_setting(app_config, 'debug', True):
run_tests()Dataclass
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
"""Represent a user in the system."""
username: str
email: str
created_at: datetime = field(default_factory=datetime.now)
is_admin: bool = False
tags: list[str] = field(default_factory=list)
def __post_init__(self):
"""Validate after initialization."""
if '@' not in self.email:
raise ValueError("Invalid email format")
# Usage
user = User(username="alice", email="alice@example.com")Property Decorator
class Temperature:
"""Temperature with Celsius/Fahrenheit conversion."""
def __init__(self, celsius: float):
self._celsius = celsius
@property
def celsius(self) -> float:
"""Get temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
"""Set temperature in Celsius."""
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
@property
def fahrenheit(self) -> float:
"""Get temperature in Fahrenheit."""
return self._celsius * 9/5 + 32
# Usage
temp = Temperature(25)
print(temp.fahrenheit) # 77.0Enum
from enum import Enum, auto
class Status(Enum):
"""Order status enumeration."""
PENDING = auto()
PROCESSING = auto()
SHIPPED = auto()
DELIVERED = auto()
CANCELLED = auto()
# Usage
order_status = Status.PENDING
if order_status == Status.PENDING:
process_order()Error Handling Patterns
Custom Exceptions
class ApplicationError(Exception):
"""Base exception for application errors."""
pass
class ValidationError(ApplicationError):
"""Raised when validation fails."""
pass
class ResourceNotFoundError(ApplicationError):
"""Raised when requested resource doesn't exist."""
pass
# Usage
def get_user(user_id: int) -> dict:
"""Retrieve user by ID."""
user = database.find(user_id)
if user is None:
raise ResourceNotFoundError(f"User {user_id} not found")
return userTry-Except Patterns
import logging
logger = logging.getLogger(__name__)
def safe_divide(a: float, b: float) -> float | None:
"""Safely divide two numbers."""
try:
result = a / b
except ZeroDivisionError:
logger.warning(f"Attempted division by zero: {a} / {b}")
return None
except TypeError as e:
logger.error(f"Type error in division: {e}")
raise
else:
logger.debug(f"Division successful: {a} / {b} = {result}")
return result
finally:
logger.debug("Division operation completed")Testing Patterns
Basic Test Structure
import pytest
from mymodule import Calculator
class TestCalculator:
"""Test suite for Calculator class."""
@pytest.fixture
def calc(self):
"""Provide calculator instance for tests."""
return Calculator()
def test_addition(self, calc):
"""Test addition operation."""
assert calc.add(2, 3) == 5
def test_division_by_zero(self, calc):
"""Test that division by zero raises exception."""
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_add_parameterized(self, calc, a, b, expected):
"""Test addition with multiple inputs."""
assert calc.add(a, b) == expectedMocking
from unittest.mock import Mock, patch
import pytest
def test_api_call():
"""Test function that makes API call."""
with patch('requests.get') as mock_get:
# Setup mock response
mock_get.return_value.json.return_value = {'status': 'ok'}
mock_get.return_value.status_code = 200
# Call function under test
result = fetch_data('https://api.example.com/data')
# Assertions
assert result == {'status': 'ok'}
mock_get.assert_called_once()Logging Configuration
import logging
from pathlib import Path
def setup_logging(
level: str = "INFO",
log_file: Path | None = None,
format_string: str | None = None
) -> None:
"""Configure application logging.
Args:
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
log_file: Optional file path for log output
format_string: Custom format string
"""
if format_string is None:
format_string = (
'%(asctime)s - %(name)s - %(levelname)s - '
'%(filename)s:%(lineno)d - %(message)s'
)
handlers: list[logging.Handler] = [logging.StreamHandler()]
if log_file:
handlers.append(logging.FileHandler(log_file))
logging.basicConfig(
level=getattr(logging, level.upper()),
format=format_string,
handlers=handlers
)
# Usage
setup_logging(level="DEBUG", log_file=Path("app.log"))
logger = logging.getLogger(__name__)
logger.info("Application started")Configuration Files
pyproject.toml Template
[project]
name = "my-project"
version = "0.1.0"
description = "Project description"
authors = [{name = "Your Name", email = "you@example.com"}]
requires-python = ">=3.11"
dependencies = [
"requests>=2.28.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 99
target-version = "py311"
select = ["E", "F", "I", "N", "W", "B", "C4", "UP"]
ignore = ["E501"]
[tool.ruff.per-file-ignores]
"__init__.py" = ["F401"] # Allow unused imports in __init__.py
[tool.black]
line-length = 99
target-version = ['py311']
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_functions = "test_*"
addopts = "--cov=src --cov-report=html --cov-report=term"Pre-commit Hooks
.pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.0
hooks:
- id: mypy
additional_dependencies: [types-requests]Install pre-commit
uv pip install pre-commit
pre-commit install
pre-commit run --all-filesChecklist for Code Quality
- [ ] All functions have type hints
- [ ] All public functions have docstrings
- [ ] Code passes
ruff checkwith no errors - [ ] Code is formatted with
ruff formatorblack - [ ] Types check with
mypy --strict - [ ] All tests pass (
pytest) - [ ] Test coverage > 80% (
pytest --cov) - [ ] No hardcoded values (use constants)
- [ ] No code duplication (DRY)
- [ ] Error handling is appropriate
- [ ] Logging is configured
- [ ] Dependencies are in pyproject.toml
- [ ] README.md is up to date
Python Best Practices Skill
A comprehensive Claude Code skill for writing professional, maintainable Python code following industry best practices.
Overview
This skill provides expert guidance on:
- PEP 8 Compliance: Official Python style guide adherence
- Code Quality: Readability, clarity, and self-documenting code
- Architecture: Modularity, reusability, and the DRY principle
- Testing: Unit tests, TDD methodology, and test automation
- Error Handling: Proper exception handling and resource management
- Virtual Environments: Modern dependency management with uv
- Tooling: Ruff, Black, Mypy, and other quality assurance tools
- Type Hints: Static type checking for improved code safety
When to Use This Skill
The skill is automatically invoked when you:
- Write new Python functions, classes, or modules
- Refactor existing Python code
- Set up a new Python project
- Need to implement tests or follow TDD
- Want to add type hints to your code
- Configure linting, formatting, or type checking
- Need guidance on Python best practices
Features
1. Code Style and Formatting
- Complete PEP 8 guidelines with examples
- Naming conventions for all Python constructs
- Import organization and formatting
- Line length and code layout best practices
2. Modern Python Tooling
Ruff: Fast, Rust-based linter and formatter
ruff check --fix . # Lint and auto-fix
ruff format . # Format codeBlack: Opinionated code formatter
black . # Format all filesMypy: Static type checker
mypy src/ # Check typesuv: Modern, fast package manager
uv venv # Create virtual environment
uv pip install pkg # Install packages3. Testing and TDD
- Unit testing with pytest
- Test-driven development methodology
- Fixtures and parametrized tests
- Mocking and patching
- Code coverage reporting
4. Type Hints
- Basic and advanced type annotations
- Generic types and protocols
- Callable and Iterator types
- Type checking configuration
5. Best Practice Patterns
- Configuration management
- Logging setup
- CLI argument parsing
- Context managers
- Dataclasses
- Custom exceptions
File Structure
python-best-practices/
├── SKILL.md # Main skill instructions for Claude
├── QUICK_REFERENCE.md # Quick lookup guide
├── README.md # This file
└── examples/
├── project_structure.py # Well-structured module
├── testing_examples.py # Test examples
├── type_hints_demo.py # Type hints showcase
└── error_handling.py # Exception handlingInstallation
For Personal Use
# Using skillz CLI
skillz install python-best-practices
# Or manually
mkdir -p ~/.claude/skills/python-best-practices
cp -r . ~/.claude/skills/python-best-practices/For Project Use
# Using skillz CLI
skillz install python-best-practices --target project
# Or manually
mkdir -p .claude/skills/python-best-practices
cp -r . .claude/skills/python-best-practices/Quick Start
Setting Up a New Python Project
1. Create project structure:
mkdir my_project
cd my_project2. Initialize virtual environment:
uv venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows3. Create pyproject.toml:
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
[tool.ruff]
line-length = 99
select = ["E", "F", "I", "N", "W", "B"]
[tool.mypy]
strict = true4. Install development tools:
uv pip install --dev pytest ruff mypy5. Create source structure:
mkdir -p src/my_project tests
touch src/my_project/__init__.py
touch tests/__init__.pyDaily Development Workflow
1. Write or modify code with proper type hints and docstrings 2. Run linter: ruff check --fix . 3. Format code: ruff format . 4. Check types: mypy src/ 5. Run tests: pytest 6. Check coverage: pytest --cov=src
Examples
Well-Structured Function
from pathlib import Path
def process_data_file(
input_path: Path,
output_path: Path,
*,
validate: bool = True,
encoding: str = "utf-8"
) -> dict[str, int]:
"""Process data file and generate statistics.
Args:
input_path: Path to input data file
output_path: Path for processed output
validate: Whether to validate data before processing
encoding: File encoding (default: utf-8)
Returns:
Dictionary with processing statistics:
- 'lines_processed': Number of lines processed
- 'errors': Number of errors encountered
Raises:
FileNotFoundError: If input_path doesn't exist
ValueError: If validation fails
Example:
>>> stats = process_data_file(
... Path("data.txt"),
... Path("output.txt"),
... validate=True
... )
>>> print(stats['lines_processed'])
100
"""
if not input_path.exists():
raise FileNotFoundError(f"Input file not found: {input_path}")
# Implementation...
return {'lines_processed': 100, 'errors': 0}Test-Driven Development
# 1. Write the test first
def test_parse_config():
"""Test configuration parsing."""
config = parse_config({"timeout": "30", "debug": "true"})
assert config.timeout == 30
assert config.debug is True
# 2. Implement minimal code to pass
from dataclasses import dataclass
@dataclass
class Config:
timeout: int
debug: bool
def parse_config(data: dict) -> Config:
return Config(
timeout=int(data['timeout']),
debug=data['debug'].lower() == 'true'
)
# 3. Refactor while keeping tests greenConfiguration
Recommended pyproject.toml
See QUICK_REFERENCE.md for a complete pyproject.toml template with all tool configurations.
Pre-commit Hooks
Install pre-commit hooks to automatically check code quality:
uv pip install pre-commit
pre-commit installCreate .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatIntegration with Claude Code
When Claude Code uses this skill, it will:
1. Enforce PEP 8 in all code suggestions 2. Add type hints to function signatures automatically 3. Include docstrings for all public functions 4. Suggest appropriate tests when implementing new features 5. Recommend proper error handling patterns 6. Format code according to Black/Ruff standards 7. Structure projects following best practices 8. Use modern Python features (3.11+)
Best Practices Checklist
When reviewing code, this skill ensures:
- ✅ PEP 8 compliant (naming, spacing, imports)
- ✅ All functions have type hints
- ✅ Public functions have comprehensive docstrings
- ✅ No code duplication (DRY principle)
- ✅ Single responsibility per function/class
- ✅ Appropriate error handling
- ✅ Tests exist for new functionality
- ✅ Code is formatted consistently
- ✅ Type checking passes
- ✅ Dependencies properly managed
Resources
Official Documentation
Tools
- Ruff - Fast Python linter
- Black - Code formatter
- Mypy - Type checker
- uv - Fast package manager
- Pytest - Testing framework
Learning Resources
Contributing
To improve this skill:
1. Test with various Python projects 2. Update examples with new patterns 3. Add edge cases and common pitfalls 4. Keep tool versions current 5. Incorporate community feedback
License
This skill is part of the skillz repository and follows the same license.
Support
For issues or suggestions:
- Report bugs in the main skillz repository
- Submit pull requests with improvements
- Share your experience using this skill
Version History
- 0.1.0 (Initial Release)
- Complete PEP 8 guidelines
- Modern tooling (Ruff, Black, Mypy, uv)
- Comprehensive type hints examples
- Testing patterns and TDD guidance
- Error handling best practices
- Quick reference guide
Acknowledgments
Based on:
- Official Python Enhancement Proposals (PEPs)
- Community best practices
- Modern Python tooling ecosystem
- Real-world project experience