PY
Best Practices
Python fundamentals v1.0.0
Python Best Practices
Modern Python development requires adherence to established conventions and tooling to ensure code quality, maintainability, and team collaboration. This skill covers PEP 8 style guidelines, type hints for static analysis, comprehensive documentation strategies, project structure patterns, and dependency isolation through virtual environments. Following these practices reduces bugs, improves code readability, and enables effective collaboration across teams.
Key Concepts
- PEP 8 Compliance - Official Python style guide covering naming conventions, indentation, line length, imports, and whitespace
- Type Hints (PEP 484) - Static type annotations enabling IDE support, mypy validation, and self-documenting code
- Docstrings (PEP 257) - Structured documentation using Google, NumPy, or Sphinx formats
- Project Structure - Standardized layout with src/ layout, tests/, docs/, and configuration files
- Virtual Environments - Isolated Python environments using venv, virtualenv, or Poetry for dependency management
- Dependency Management - requirements.txt, pyproject.toml, Poetry lock files, and reproducible builds
- Code Quality Tools - Black (formatting), isort (import sorting), flake8/ruff (linting), mypy (type checking)
- Import Organization - Grouping standard library, third-party, and local imports with consistent ordering
Best Practices
- Always use type hints - Annotate function signatures, class attributes, and complex variables for static analysis
- Format with Black - Use Black code formatter with default settings for consistent, zero-configuration formatting
- Organize imports with isort - Group and sort imports: standard library, third-party, local, with one blank line between groups
- Write comprehensive docstrings - Document all public modules, classes, functions with parameters, return values, and examples
- Use src/ layout - Place package code in src/package_name/ to avoid import confusion and enable proper testing
- Pin dependencies - Use lock files (Poetry.lock, requirements.txt with versions) for reproducible environments
- Validate with mypy - Run mypy with strict mode to catch type errors before runtime
- Follow naming conventions - snake_case for functions/variables, PascalCase for classes, UPPER_CASE for constants
- Limit line length - Keep lines under 88 characters (Black default) or 79 (PEP 8 strict)
- Separate concerns - Split large modules, use packages for organization, maintain single responsibility per module
Code Examples
Type Hints and Function Signatures
# ✅ GOOD: Comprehensive type hints with modern syntax
from typing import TypeAlias
from collections.abc import Sequence, Mapping
UserId: TypeAlias = int
UserData: TypeAlias = Mapping[str, str | int]
def process_users(
user_ids: Sequence[UserId],
metadata: UserData | None = None,
*,
strict: bool = True
) -> list[dict[str, UserId | str]]:
"""
Process user IDs with optional metadata.
Args:
user_ids: Sequence of user identifiers to process
metadata: Optional mapping of user attributes
strict: Enforce strict validation (keyword-only)
Returns:
List of processed user records with IDs and status
Raises:
ValueError: If user_ids is empty and strict=True
"""
if strict and not user_ids:
raise ValueError("user_ids cannot be empty in strict mode")
return [{"id": uid, "status": "processed"} for uid in user_ids]
# ❌ BAD: Missing type hints and documentation
def process_users(user_ids, metadata=None, strict=True):
if strict and not user_ids:
raise ValueError("user_ids cannot be empty")
return [{"id": uid, "status": "processed"} for uid in user_ids]
Project Structure and Import Organization
# ✅ GOOD: Proper import grouping with isort
# Standard library imports
import asyncio
import logging
from pathlib import Path
from typing import Final
# Third-party imports
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
# Local application imports
from myapp.config import settings
from myapp.services.user import UserService
from myapp.utils.validation import validate_email
# Constants
API_VERSION: Final[str] = "v1"
MAX_RETRY_ATTEMPTS: Final[int] = 3
# ❌ BAD: Mixed import groups, inconsistent ordering
from myapp.config import settings
import logging
from fastapi import FastAPI
from typing import Final
import asyncio
from myapp.services.user import UserService
from pydantic import BaseModel
import httpx
Documentation with Google-Style Docstrings
# ✅ GOOD: Comprehensive Google-style docstring
from typing import Protocol
from decimal import Decimal
class PaymentProcessor(Protocol):
"""
Protocol for payment processing implementations.
Defines the interface that all payment processors must implement
for handling transactions, refunds, and status queries.
Attributes:
provider_name: Name of the payment provider
max_retry_attempts: Maximum number of retry attempts for failed transactions
Example:
>>> processor = StripePaymentProcessor(api_key="sk_test_123")
>>> result = processor.process_payment(amount=Decimal("99.99"), currency="USD")
>>> print(result.transaction_id)
'txn_abc123'
"""
provider_name: str
max_retry_attempts: int
def process_payment(
self,
amount: Decimal,
currency: str,
*,
idempotency_key: str | None = None
) -> dict[str, str | Decimal]:
"""
Process a payment transaction.
Args:
amount: Payment amount with precision to 2 decimal places
currency: ISO 4217 currency code (e.g., 'USD', 'EUR')
idempotency_key: Optional key for idempotent requests
Returns:
Dictionary containing:
- transaction_id: Unique transaction identifier
- status: Payment status ('success', 'pending', 'failed')
- amount_charged: Actual amount charged including fees
Raises:
PaymentError: If payment processing fails
ValueError: If amount is negative or currency is invalid
Note:
This method is idempotent when idempotency_key is provided.
Retry logic uses exponential backoff up to max_retry_attempts.
"""
...
# ❌ BAD: Minimal or missing documentation
class PaymentProcessor:
def process_payment(self, amount, currency, idempotency_key=None):
# Process payment
...
Virtual Environment and Dependency Management
# ✅ GOOD: pyproject.toml with Poetry for modern dependency management
"""
[tool.poetry]
name = "myapp"
version = "1.0.0"
description = "Production application with proper dependencies"
authors = ["Team <team@example.com>"]
readme = "README.md"
python = "^3.11"
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.109.0"
uvicorn = {extras = ["standard"], version = "^0.27.0"}
pydantic = "^2.5.0"
httpx = "^0.26.0"
sqlalchemy = {extras = ["asyncio"], version = "^2.0.0"}
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-asyncio = "^0.23.0"
pytest-cov = "^4.1.0"
mypy = "^1.8.0"
ruff = "^0.1.0"
black = "^24.0.0"
[tool.poetry.group.test.dependencies]
pytest-mock = "^3.12.0"
faker = "^22.0.0"
[tool.black]
line-length = 88
target-version = ['py311']
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
[tool.ruff]
line-length = 88
target-version = "py311"
select = ["E", "F", "I", "N", "W", "B", "C4", "UP"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "--cov=myapp --cov-report=term-missing --cov-report=html"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
"""
# Project structure following src/ layout
"""
myapp/
├── .python-version # pyenv version
├── pyproject.toml # Project config and dependencies
├── poetry.lock # Locked dependency versions
├── README.md
├── .gitignore
├── src/
│ └── myapp/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── models/
│ ├── services/
│ ├── api/
│ └── utils/
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── unit/
│ └── integration/
└── docs/
└── api.md
"""
# ❌ BAD: requirements.txt without versions, flat structure
"""
fastapi
uvicorn
pydantic
httpx
myapp/
├── app.py
├── models.py
├── utils.py
└── test_app.py
"""
Configuration and Settings Management
# ✅ GOOD: Pydantic settings with environment validation
from functools import lru_cache
from typing import Literal
from pydantic import Field, HttpUrl, PostgresDsn, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""
Application settings with environment variable support.
Attributes are loaded from environment variables with APP_ prefix.
Supports .env file loading in development.
"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="APP_",
case_sensitive=False,
extra="ignore"
)
# Application
environment: Literal["development", "staging", "production"] = "development"
debug: bool = Field(default=False, description="Enable debug mode")
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
# API
api_title: str = "MyApp API"
api_version: str = "1.0.0"
api_host: str = "0.0.0.0"
api_port: int = Field(default=8000, ge=1024, le=65535)
# Database
database_url: PostgresDsn = Field(
...,
description="PostgreSQL connection URL"
)
database_pool_size: int = Field(default=10, ge=5, le=50)
# External Services
external_api_url: HttpUrl
external_api_key: str = Field(..., min_length=20)
external_api_timeout: int = Field(default=30, ge=1, le=300)
@field_validator("environment")
@classmethod
def validate_environment(cls, v: str) -> str:
"""Ensure environment is valid and warn on production."""
if v == "production":
import logging
logging.warning("Running in PRODUCTION mode")
return v
@lru_cache
def get_settings() -> Settings:
"""
Get cached settings instance.
Returns singleton settings object for dependency injection.
"""
return Settings()
# Usage in FastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
@app.get("/config")
async def get_config(settings: Settings = Depends(get_settings)) -> dict[str, str]:
"""Return non-sensitive configuration."""
return {
"environment": settings.environment,
"api_version": settings.api_version,
"log_level": settings.log_level,
}
# ❌ BAD: Hardcoded configuration, no validation
import os
DEBUG = True
DATABASE_URL = "postgresql://user:pass@localhost/db"
API_KEY = "hardcoded-key-123"
TIMEOUT = 30
Anti-Patterns
Missing Type Hints
# ❌ Avoid: Dynamic typing without hints
def process_data(data, options):
result = []
for item in data:
if options.get("transform"):
result.append(item.upper())
return result
# ✅ Fix: Add comprehensive type hints
from collections.abc import Sequence, Mapping
from typing import Any
def process_data(
data: Sequence[str],
options: Mapping[str, Any]
) -> list[str]:
result: list[str] = []
for item in data:
if options.get("transform"):
result.append(item.upper())
return result
Inconsistent Import Ordering
# ❌ Avoid: Mixed import groups
from myapp.utils import helper
import json
from fastapi import FastAPI
import os
from typing import Dict
# ✅ Fix: Grouped and sorted imports (use isort)
import json
import os
from typing import Dict
from fastapi import FastAPI
from myapp.utils import helper
Mutable Default Arguments
# ❌ Avoid: Mutable default creates shared state
def add_item(item: str, items: list[str] = []) -> list[str]:
items.append(item)
return items
# ✅ Fix: Use None and create new instance
def add_item(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = []
items.append(item)
return items
Global State Without Dependency Injection
# ❌ Avoid: Global configuration makes testing difficult
CONFIG = load_config()
def process_request(data: dict[str, Any]) -> dict[str, Any]:
timeout = CONFIG["timeout"]
return call_api(data, timeout)
# ✅ Fix: Use dependency injection
from fastapi import Depends
def process_request(
data: dict[str, Any],
config: Settings = Depends(get_settings)
) -> dict[str, Any]:
return call_api(data, config.timeout)
Testing Strategies
Type Checking with mypy
# Run mypy with strict mode
mypy src/ --strict --warn-return-any --warn-unused-configs
# Check specific module
mypy src/myapp/services/user.py --strict
Code Formatting Validation
# Format with Black
black src/ tests/
# Check without modifying
black --check src/
# Sort imports with isort
isort src/ tests/
# Validate import sorting
isort --check-only src/
Linting with Ruff
# Run Ruff linter (faster alternative to flake8, pylint)
ruff check src/ tests/
# Auto-fix issues
ruff check --fix src/
# Check specific rules
ruff check src/ --select E,F,I,N,W,B
Testing Code Quality in CI/CD
# tests/test_code_quality.py
import subprocess
from pathlib import Path
def test_black_formatting() -> None:
"""Ensure code is formatted with Black."""
result = subprocess.run(
["black", "--check", "src/"],
capture_output=True
)
assert result.returncode == 0, "Code is not formatted with Black"
def test_mypy_type_checking() -> None:
"""Ensure type hints pass mypy validation."""
result = subprocess.run(
["mypy", "src/", "--strict"],
capture_output=True
)
assert result.returncode == 0, f"Type errors found:\n{result.stdout.decode()}"
def test_import_sorting() -> None:
"""Ensure imports are sorted with isort."""
result = subprocess.run(
["isort", "--check-only", "src/"],
capture_output=True
)
assert result.returncode == 0, "Imports are not sorted"
References
- PEP 8 - Style Guide for Python Code
- PEP 484 - Type Hints
- PEP 257 - Docstring Conventions
- Black Code Formatter
- mypy Static Type Checker
- Ruff Linter
- Poetry Dependency Management
- Pydantic Settings
- Python Packaging User Guide
Related Skills
- async-python.md - Type hints for async functions and coroutines
- python-testing.md - Testing code quality and type coverage
- python-packaging.md - Package structure and distribution
- pythonic-patterns.md - Idiomatic Python patterns following PEP 8