Python Style Guide
Table of Contents
- Naming
- Python Style Rules
- Python Formatting Rules
- Modern Python Features
- Object-Oriented Programming
- Functional Programming
- Async Programming
- Error Handling
- Testing
- Performance
- Packaging and Distribution
- Security
Naming
Variable and Function Names
Variable and Function Names: Use snake_case
(all lowercase letters, with underscores separating words).
# Wrong
UserName = "Zepa"
TotalCount = 100
def ProcessData(data):
return [item.strip() for item in data]
# Right
user_name = "zepa"
total_count = 100
def process_data(data):
return [item.strip() for item in data]
Class Names: Use CamelCase
.
# Wrong
class company_info:
def __init__(self, name, city):
self.name = name
self.city = city
# Right
class CompanyInfo:
def __init__(self, name, city):
self.name = name
self.city = city
Constant Names
Use all uppercase letters with words separated by underscores.
# Wrong
maxCount = 5
apiKey = "pywndqod1"
# Right
MAX_COUNT = 5
API_KEY = "pywndqod1"
Temporary Variables
Use short, meaningful names.
for i in range(10):
print(i)
temp = company_name
idx = 0
Python Style Rules
Indentation
Use 4 spaces for indentation. Do not use tabs.
# Wrong
def example_function():
if True:
print("Success")
# Right
def example_function():
if True:
print("Success")
Line Length
Limit all lines to 80 characters.
# Wrong
def long_function_name_with_many_parameters(param1, param2, param3, param4, param5, param6):
return param1 + param2 + param3 + param4 + param5 + param6
# Right
def long_function_name_with_many_parameters(param1, param2, param3, param4):
return param1 + param2 + param3 + param4
Whitespace
Surround binary operators (e.g., +
, -
, *
, etc.) with a single space on both sides.
total_price = price + tax
a = b * (c + d)
discount = current - temp
Do not put spaces immediately inside parentheses in function definitions or slices.
# Wrong
def my_function( x ):
return x[ 0 : 3 ]
# Right
def my_function(x):
return x[0:3]
Put a space after for
, if
, while
, and other control flow statements.
# Wrong
if(request_success):
print("SUCCESS:" + status_code)
# Right
if request_success:
print("SUCCESS:" + status_code)
3. Python Formatting Rules
Comments
Inline Comments: Use inline comments to explain complex or non-obvious code. Place comments above the code with two spaces before the #
.
# Wrong
if request_success: # This is a bad comment style
print("SUCCESS:" + status_code)
# Right
# Success Request Check
if request_success:
print("SUCCESS:" + status_code)
Docstrings: Every function and class should have a docstring to describe what it does, its parameters, and its return values.
# Wrong
def add_numbers(a, b):
return a + b # Missing docstring
# Right
def add_numbers(a, b):
"""
Adds two numbers together.
Args:
a (int or float): The first number.
b (int or float): The second number.
Returns:
int or float: The sum of the two numbers.
"""
return a + b
Type Hints
Type hints should be used for function parameters and return values.
def add(a: int, b: int) -> int:
return a + b
Use Optional
and Union
when appropriate to define flexible types.
from typing import Optional, Union, List, Dict, Any
def get_value(x: Optional[str] = None) -> Union[int, str]:
if x:
return int(x)
return "No value"
# Modern Python 3.10+ syntax
def process_data(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# Generic types
from typing import TypeVar, Generic
T = TypeVar('T')
class Container(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
def get(self) -> T:
return self.value
Imports
Import standard libraries first, followed by third-party libraries, and then local imports. Each group of imports should be separated by a blank line.
import os
import sys
import requests
from mymodule import my_function
Patterns
Exception Handling
Handle exceptions by specifying specific exceptions and minimizing the use of try...except
blocks.
# Wrong
try:
# ...
except:
print("Something went wrong") # Too generic, not recommended
# Right
try:
# ...
except ValueError as e:
print(f"ValueError occurred: {e}")
except FileNotFoundError as e:
print(f"File not found: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
Function and Class Definitions
Functions should perform one clear task. A function should be as small and simple as possible.
def calculate_area(radius: float) -> float:
return 3.14159 * radius * radius
Classes should have a single responsibility and contain related methods.
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
Modern Python Features
Data Classes
Use data classes for simple data containers.
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class User:
name: str
email: str
age: int
tags: List[str] = field(default_factory=list)
is_active: bool = True
@dataclass(frozen=True)
class Point:
x: float
y: float
def distance_from_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
# Usage
user = User("John Doe", "john@example.com", 30, ["developer", "python"])
point = Point(3.0, 4.0)
print(point.distance_from_origin()) # 5.0
Enums
Use enums for representing fixed sets of values.
from enum import Enum, auto, IntEnum
class Status(Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
CRITICAL = 4
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
# Usage
status = Status.PENDING
if status == Status.APPROVED:
print("Request approved")
# Enum comparison
if Priority.HIGH > Priority.MEDIUM:
print("High priority is greater than medium")
Pathlib
Use pathlib for modern file path handling.
from pathlib import Path
# Create paths
config_file = Path("config") / "settings.json"
backup_dir = Path.home() / "backups"
# Path operations
if config_file.exists():
content = config_file.read_text()
config_file.write_text(content.upper())
# Iterate over files
for py_file in Path(".").glob("*.py"):
print(f"Python file: {py_file}")
# Create directories
backup_dir.mkdir(parents=True, exist_ok=True)
Context Managers
Use context managers for resource management.
from contextlib import contextmanager, suppress
import tempfile
import os
# Built-in context managers
with open("file.txt", "r") as f:
content = f.read()
# Custom context manager
@contextmanager
def temporary_file():
temp_file = tempfile.NamedTemporaryFile(delete=False)
try:
yield temp_file
finally:
temp_file.close()
os.unlink(temp_file.name)
# Usage
with temporary_file() as temp:
temp.write(b"Hello, World!")
# Suppress exceptions
with suppress(FileNotFoundError):
os.remove("nonexistent_file.txt")
Walrus Operator
Use the walrus operator (:=) for assignment expressions.
# Traditional approach
data = get_data()
if data:
process_data(data)
# With walrus operator
if data := get_data():
process_data(data)
# In loops
while (line := file.readline()) != "":
process_line(line)
# In list comprehensions
results = [y for x in data if (y := process(x)) is not None]
Pattern Matching (Python 3.10+)
Use pattern matching for complex conditional logic.
def handle_response(response):
match response:
case {"status": "success", "data": data}:
return f"Success: {data}"
case {"status": "error", "message": msg}:
return f"Error: {msg}"
case {"status": "pending"}:
return "Request is pending"
case _:
return "Unknown response format"
# Pattern matching with types
def process_value(value):
match value:
case int(n) if n > 0:
return f"Positive integer: {n}"
case int(n) if n < 0:
return f"Negative integer: {n}"
case str(s) if len(s) > 10:
return f"Long string: {s[:10]}..."
case str(s):
return f"Short string: {s}"
case list(items):
return f"List with {len(items)} items"
case _:
return "Unknown type"
Object-Oriented Programming
Class Design Principles
Follow SOLID principles for better class design.
from abc import ABC, abstractmethod
from typing import Protocol
# Single Responsibility Principle
class UserValidator:
def validate_email(self, email: str) -> bool:
return "@" in email and "." in email.split("@")[1]
class UserRepository:
def save(self, user: dict) -> None:
# Save user to database
pass
# Open/Closed Principle
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
# Liskov Substitution Principle
class Bird(ABC):
@abstractmethod
def move(self) -> None:
pass
class FlyingBird(Bird):
def move(self) -> None:
print("Flying")
class WalkingBird(Bird):
def move(self) -> None:
print("Walking")
# Interface Segregation Principle
class Readable(Protocol):
def read(self) -> str: ...
class Writable(Protocol):
def write(self, data: str) -> None: ...
class FileReader:
def read(self) -> str:
return "file content"
class FileWriter:
def write(self, data: str) -> None:
print(f"Writing: {data}")
# Dependency Inversion Principle
class Database(ABC):
@abstractmethod
def save(self, data: dict) -> None: ...
class PostgreSQLDatabase(Database):
def save(self, data: dict) -> None:
print(f"Saving to PostgreSQL: {data}")
class UserService:
def __init__(self, database: Database):
self.database = database
def create_user(self, user_data: dict) -> None:
self.database.save(user_data)
Properties and Descriptors
Use properties for controlled attribute access.
class Temperature:
def __init__(self, celsius: float = 0):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value: float) -> None:
self.celsius = (value - 32) * 5/9
# Custom descriptor
class ValidatedString:
def __init__(self, min_length: int = 0, max_length: int = 100):
self.min_length = min_length
self.max_length = max_length
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name, "")
def __set__(self, obj, value):
if not isinstance(value, str):
raise TypeError(f"{self.name} must be a string")
if len(value) < self.min_length:
raise ValueError(f"{self.name} must be at least {self.min_length} characters")
if len(value) > self.max_length:
raise ValueError(f"{self.name} must be at most {self.max_length} characters")
obj.__dict__[self.name] = value
class User:
name = ValidatedString(min_length=2, max_length=50)
email = ValidatedString(min_length=5, max_length=100)
def __init__(self, name: str, email: str):
self.name = name
self.email = email
Metaclasses
Use metaclasses for advanced class customization.
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
def __init__(self):
self.connected = True
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True
# Metaclass for automatic registration
class PluginRegistry(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if hasattr(cls, 'plugin_name'):
PluginRegistry._plugins[cls.plugin_name] = cls
return cls
_plugins = {}
class Plugin(metaclass=PluginRegistry):
pass
class EmailPlugin(Plugin):
plugin_name = "email"
def send(self, message: str) -> None:
print(f"Sending email: {message}")
class SMSPlugin(Plugin):
plugin_name = "sms"
def send(self, message: str) -> None:
print(f"Sending SMS: {message}")
# Usage
email_plugin = PluginRegistry._plugins["email"]()
email_plugin.send("Hello!")
Functional Programming
Higher-Order Functions
Use higher-order functions for functional programming patterns.
from functools import reduce, partial
from operator import add, mul
# Map, filter, reduce
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Map
squares = list(map(lambda x: x ** 2, numbers))
doubles = list(map(lambda x: x * 2, numbers))
# Filter
evens = list(filter(lambda x: x % 2 == 0, numbers))
odds = list(filter(lambda x: x % 2 != 0, numbers))
# Reduce
sum_all = reduce(add, numbers)
product_all = reduce(mul, numbers)
# Partial functions
def multiply(x: int, y: int) -> int:
return x * y
double = partial(multiply, 2)
triple = partial(multiply, 3)
print(double(5)) # 10
print(triple(5)) # 15
Decorators
Use decorators for cross-cutting concerns.
from functools import wraps
import time
from typing import Callable, Any
# Basic decorator
def timer(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
# Decorator with parameters
def retry(max_attempts: int = 3, delay: float = 1.0):
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise e
time.sleep(delay)
return None
return wrapper
return decorator
# Class decorator
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
# Usage
@timer
@retry(max_attempts=3, delay=0.5)
def fetch_data(url: str) -> str:
# Simulate API call
if url == "error":
raise ConnectionError("Connection failed")
return f"Data from {url}"
@singleton
class Config:
def __init__(self):
self.settings = {"debug": True}
Generators and Iterators
Use generators for memory-efficient iteration.
# Generator function
def fibonacci(n: int):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
# Generator expression
squares = (x ** 2 for x in range(10))
# Custom iterator
class CountDown:
def __init__(self, start: int):
self.start = start
def __iter__(self):
return self
def __next__(self):
if self.start <= 0:
raise StopIteration
self.start -= 1
return self.start + 1
# Usage
for num in fibonacci(10):
print(num, end=" ")
print()
for square in squares:
print(square, end=" ")
print()
for num in CountDown(5):
print(num, end=" ")
print()
Async Programming
Async/Await
Use async/await for asynchronous programming.
import asyncio
import aiohttp
from typing import List, Dict, Any
async def fetch_url(session: aiohttp.ClientSession, url: str) -> Dict[str, Any]:
try:
async with session.get(url) as response:
return {
"url": url,
"status": response.status,
"content": await response.text()
}
except Exception as e:
return {
"url": url,
"error": str(e)
}
async def fetch_multiple_urls(urls: List[str]) -> List[Dict[str, Any]]:
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
return await asyncio.gather(*tasks)
# Usage
async def main():
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3"
]
results = await fetch_multiple_urls(urls)
for result in results:
print(f"URL: {result['url']}, Status: {result.get('status', 'Error')}")
# Run the async function
asyncio.run(main())
Async Context Managers
Use async context managers for resource management.
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def async_file_reader(filename: str):
try:
with open(filename, 'r') as f:
yield f
except FileNotFoundError:
print(f"File {filename} not found")
yield None
# Async generator
async def async_range(start: int, stop: int, step: int = 1):
current = start
while current < stop:
yield current
current += step
await asyncio.sleep(0.1) # Simulate async work
# Usage
async def process_file():
async with async_file_reader("data.txt") as file:
if file:
content = file.read()
print(f"File content: {content}")
async def count_async():
async for num in async_range(0, 10):
print(f"Count: {num}")
# Run both
asyncio.run(asyncio.gather(process_file(), count_async()))
Error Handling
Custom Exceptions
Create custom exceptions for better error handling.
class ValidationError(Exception):
"""Raised when validation fails"""
def __init__(self, message: str, field: str = None):
super().__init__(message)
self.field = field
class DatabaseError(Exception):
"""Raised when database operations fail"""
def __init__(self, message: str, error_code: int = None):
super().__init__(message)
self.error_code = error_code
class APIError(Exception):
"""Raised when API calls fail"""
def __init__(self, message: str, status_code: int = None):
super().__init__(message)
self.status_code = status_code
# Exception chaining
def process_data(data: dict) -> None:
try:
validate_data(data)
save_to_database(data)
except ValidationError as e:
raise DatabaseError("Failed to process data") from e
def validate_data(data: dict) -> None:
if not data.get("name"):
raise ValidationError("Name is required", "name")
if not data.get("email"):
raise ValidationError("Email is required", "email")
Error Recovery
Implement error recovery strategies.
import logging
from typing import Optional, Callable, Any
def retry_on_failure(
max_attempts: int = 3,
delay: float = 1.0,
backoff: float = 2.0,
exceptions: tuple = (Exception,)
):
def decorator(func: Callable) -> Callable:
def wrapper(*args, **kwargs) -> Any:
current_delay = delay
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts - 1:
logging.error(f"Function {func.__name__} failed after {max_attempts} attempts: {e}")
raise e
logging.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {current_delay}s...")
time.sleep(current_delay)
current_delay *= backoff
return None
return wrapper
return decorator
@retry_on_failure(max_attempts=3, delay=1.0, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url: str) -> str:
# Simulate API call that might fail
import random
if random.random() < 0.7: # 70% chance of failure
raise ConnectionError("Connection failed")
return f"Data from {url}"
Testing
Unit Testing with pytest
Write comprehensive unit tests using pytest.
import pytest
from unittest.mock import Mock, patch
from typing import List
# Test functions
def add_numbers(a: int, b: int) -> int:
return a + b
def divide_numbers(a: float, b: float) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def process_items(items: List[str]) -> List[str]:
return [item.upper() for item in items if item]
# Test classes
class Calculator:
def __init__(self):
self.history = []
def add(self, a: int, b: int) -> int:
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def get_history(self) -> List[str]:
return self.history.copy()
# Test file
class TestCalculator:
def test_add_positive_numbers(self):
calc = Calculator()
result = calc.add(2, 3)
assert result == 5
assert "2 + 3 = 5" in calc.get_history()
def test_add_negative_numbers(self):
calc = Calculator()
result = calc.add(-2, -3)
assert result == -5
def test_add_zero(self):
calc = Calculator()
result = calc.add(5, 0)
assert result == 5
def test_add_numbers():
assert add_numbers(2, 3) == 5
assert add_numbers(-1, 1) == 0
assert add_numbers(0, 0) == 0
def test_divide_numbers():
assert divide_numbers(10, 2) == 5.0
assert divide_numbers(7, 3) == pytest.approx(2.333, rel=1e-2)
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide_numbers(10, 0)
def test_process_items():
items = ["hello", "", "world", None, "python"]
result = process_items(items)
assert result == ["HELLO", "WORLD", "PYTHON"]
# Fixtures
@pytest.fixture
def sample_data():
return [1, 2, 3, 4, 5]
@pytest.fixture
def calculator():
return Calculator()
def test_calculator_with_fixture(calculator, sample_data):
result = calculator.add(sample_data[0], sample_data[1])
assert result == 3
# Parametrized tests
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(10, -5, 5)
])
def test_add_parametrized(a, b, expected):
assert add_numbers(a, b) == expected
# Mocking
def test_with_mock():
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {"status": "success"}
# Your function that uses requests.get
response = mock_get("https://api.example.com")
data = response.json()
assert data["status"] == "success"
mock_get.assert_called_once_with("https://api.example.com")
Performance
Profiling and Optimization
Use profiling tools to identify performance bottlenecks.
import cProfile
import pstats
from functools import lru_cache
import time
# Profiling decorator
def profile(func):
def wrapper(*args, **kwargs):
profiler = cProfile.Profile()
profiler.enable()
result = func(*args, **kwargs)
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(10) # Print top 10 functions
return result
return wrapper
# Caching
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Memory profiling
import tracemalloc
def memory_profile(func):
def wrapper(*args, **kwargs):
tracemalloc.start()
result = func(*args, **kwargs)
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")
tracemalloc.stop()
return result
return wrapper
@profile
@memory_profile
def process_large_data():
data = [i ** 2 for i in range(10000)]
return sum(data)
# Performance comparison
def slow_function(n: int) -> int:
result = 0
for i in range(n):
result += i ** 2
return result
def fast_function(n: int) -> int:
return sum(i ** 2 for i in range(n))
# Time comparison
start = time.time()
slow_function(10000)
slow_time = time.time() - start
start = time.time()
fast_function(10000)
fast_time = time.time() - start
print(f"Slow function: {slow_time:.4f}s")
print(f"Fast function: {fast_time:.4f}s")
Memory Optimization
Optimize memory usage for large datasets.
import sys
from typing import Iterator, Generator
# Generator for memory-efficient processing
def read_large_file(filename: str) -> Generator[str, None, None]:
with open(filename, 'r') as file:
for line in file:
yield line.strip()
# Memory-efficient data processing
def process_large_dataset(data: Iterator[str]) -> Generator[dict, None, None]:
for line in data:
if line: # Skip empty lines
parts = line.split(',')
if len(parts) >= 3:
yield {
'id': int(parts[0]),
'name': parts[1],
'value': float(parts[2])
}
# Usage
def analyze_large_file(filename: str):
data = read_large_file(filename)
processed = process_large_dataset(data)
total = 0
count = 0
for record in processed:
total += record['value']
count += 1
return total / count if count > 0 else 0
# Memory-efficient data structures
from collections import deque
import heapq
# Use deque for efficient append/pop operations
queue = deque()
queue.append(1)
queue.append(2)
queue.popleft() # Efficient left pop
# Use heapq for efficient priority queues
priority_queue = []
heapq.heappush(priority_queue, (1, 'task1'))
heapq.heappush(priority_queue, (3, 'task3'))
heapq.heappush(priority_queue, (2, 'task2'))
while priority_queue:
priority, task = heapq.heappop(priority_queue)
print(f"Processing {task} with priority {priority}")
Packaging and Distribution
Project Structure
Organize your project with proper structure.
my_project/
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── src/
│ └── my_project/
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ └── services.py
│ ├── utils/
│ │ ├── __init__.py
│ │ └── helpers.py
│ └── cli/
│ ├── __init__.py
│ └── main.py
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ └── test_services.py
├── docs/
│ └── index.md
└── examples/
└── basic_usage.py
pyproject.toml Configuration
Configure your project with pyproject.toml.
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-project"
version = "0.1.0"
description = "A sample Python project"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
keywords = ["python", "example", "package"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
dependencies = [
"requests>=2.25.0",
"click>=8.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=6.0",
"pytest-cov>=2.0",
"black>=21.0",
"flake8>=3.8",
"mypy>=0.800",
]
docs = [
"sphinx>=4.0",
"sphinx-rtd-theme>=0.5",
]
[project.scripts]
my-project = "my_project.cli.main:main"
[project.urls]
Homepage = "https://github.com/yourusername/my-project"
Repository = "https://github.com/yourusername/my-project"
Documentation = "https://my-project.readthedocs.io"
"Bug Tracker" = "https://github.com/yourusername/my-project/issues"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-dir]
"" = "src"
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| build
| dist
)/
'''
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--strict-config",
"--cov=my_project",
"--cov-report=term-missing",
"--cov-report=html",
]
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
Security
Input Validation and Sanitization
Implement proper input validation and sanitization.
import re
import html
from typing import Any, Dict, List
from dataclasses import dataclass
@dataclass
class ValidationError(Exception):
field: str
message: str
class InputValidator:
@staticmethod
def validate_email(email: str) -> str:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, email):
raise ValidationError("email", "Invalid email format")
return email.lower()
@staticmethod
def validate_password(password: str) -> str:
if len(password) < 8:
raise ValidationError("password", "Password must be at least 8 characters")
if not re.search(r'[A-Z]', password):
raise ValidationError("password", "Password must contain uppercase letter")
if not re.search(r'[a-z]', password):
raise ValidationError("password", "Password must contain lowercase letter")
if not re.search(r'\d', password):
raise ValidationError("password", "Password must contain digit")
return password
@staticmethod
def sanitize_html(text: str) -> str:
return html.escape(text)
@staticmethod
def validate_sql_input(input_str: str) -> str:
# Remove potential SQL injection patterns
dangerous_patterns = [
r'(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION)\b)',
r'(\b(OR|AND)\s+\d+\s*=\s*\d+)',
r'(\'|\"|;|--|\/\*|\*\/)'
]
for pattern in dangerous_patterns:
if re.search(pattern, input_str, re.IGNORECASE):
raise ValidationError("input", "Potentially dangerous input detected")
return input_str
# Usage
def validate_user_input(data: Dict[str, Any]) -> Dict[str, str]:
validator = InputValidator()
validated_data = {}
try:
validated_data['email'] = validator.validate_email(data['email'])
validated_data['password'] = validator.validate_password(data['password'])
validated_data['name'] = validator.sanitize_html(data['name'])
validated_data['comment'] = validator.sanitize_html(data.get('comment', ''))
except ValidationError as e:
raise e
return validated_data
Secure Configuration
Implement secure configuration management.
import os
from typing import Optional
from dataclasses import dataclass
from pathlib import Path
@dataclass
class SecurityConfig:
secret_key: str
database_url: str
redis_url: str
debug: bool = False
@classmethod
def from_env(cls) -> 'SecurityConfig':
secret_key = os.getenv('SECRET_KEY')
if not secret_key:
raise ValueError("SECRET_KEY environment variable is required")
database_url = os.getenv('DATABASE_URL')
if not database_url:
raise ValueError("DATABASE_URL environment variable is required")
return cls(
secret_key=secret_key,
database_url=database_url,
redis_url=os.getenv('REDIS_URL', 'redis://localhost:6379'),
debug=os.getenv('DEBUG', 'False').lower() == 'true'
)
def validate(self) -> None:
if len(self.secret_key) < 32:
raise ValueError("SECRET_KEY must be at least 32 characters")
if self.debug and os.getenv('ENVIRONMENT') == 'production':
raise ValueError("Debug mode cannot be enabled in production")
# Environment-specific configuration
class Config:
def __init__(self):
self.environment = os.getenv('ENVIRONMENT', 'development')
self.config = self._load_config()
def _load_config(self) -> SecurityConfig:
if self.environment == 'production':
return self._load_production_config()
elif self.environment == 'testing':
return self._load_testing_config()
else:
return self._load_development_config()
def _load_production_config(self) -> SecurityConfig:
config = SecurityConfig.from_env()
config.validate()
return config
def _load_testing_config(self) -> SecurityConfig:
return SecurityConfig(
secret_key='test-secret-key-for-testing-only',
database_url='sqlite:///:memory:',
redis_url='redis://localhost:6379/1',
debug=True
)
def _load_development_config(self) -> SecurityConfig:
return SecurityConfig(
secret_key=os.getenv('SECRET_KEY', 'dev-secret-key-not-for-production'),
database_url=os.getenv('DATABASE_URL', 'sqlite:///dev.db'),
redis_url=os.getenv('REDIS_URL', 'redis://localhost:6379/0'),
debug=True
)
Documentation
Document your code effectively.
"""
User management module.
This module provides functionality for managing users in the system.
It includes user creation, validation, and retrieval operations.
Example:
>>> from my_project import UserService
>>> service = UserService()
>>> user = service.create_user({"name": "John", "email": "john@example.com"})
>>> print(user.name)
John
"""
from typing import List, Optional, Dict, Any
from dataclasses import dataclass
from datetime import datetime
@dataclass
class User:
"""
Represents a user in the system.
Attributes:
id: Unique identifier for the user
name: User's full name
email: User's email address
created_at: Timestamp when user was created
is_active: Whether the user account is active
Example:
>>> user = User(1, "John Doe", "john@example.com")
>>> print(user.name)
John Doe
"""
id: int
name: str
email: str
created_at: datetime = None
is_active: bool = True
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now()
def deactivate(self) -> None:
"""
Deactivate the user account.
This method sets the is_active flag to False, effectively
disabling the user's access to the system.
Returns:
None
Example:
>>> user = User(1, "John", "john@example.com")
>>> user.deactivate()
>>> print(user.is_active)
False
"""
self.is_active = False
class UserService:
"""
Service class for user management operations.
This class provides methods for creating, retrieving, and managing
users in the system. It handles validation and business logic
related to user operations.
Attributes:
database: Database interface for data persistence
Example:
>>> service = UserService()
>>> user = service.create_user({
... "name": "Jane Doe",
... "email": "jane@example.com"
... })
>>> users = service.get_active_users()
"""
def __init__(self, database: DatabaseInterface):
"""
Initialize the UserService.
Args:
database: Database interface for data persistence
Raises:
ValueError: If database is None
"""
if database is None:
raise ValueError("Database cannot be None")
self.database = database
def create_user(self, user_data: Dict[str, Any]) -> User:
"""
Create a new user.
This method validates the user data and creates a new user
in the system. It performs email validation and ensures
the user data is complete.
Args:
user_data: Dictionary containing user information
- name (str): User's full name
- email (str): User's email address
Returns:
User: The created user object
Raises:
ValueError: If user data is invalid
ValidationError: If email format is invalid
Example:
>>> service = UserService(database)
>>> user = service.create_user({
... "name": "John Doe",
... "email": "john@example.com"
... })
>>> print(user.name)
John Doe
"""
# Validation logic here
user = User(
id=self._generate_id(),
name=user_data["name"],
email=user_data["email"]
)
self.database.save(user.__dict__)
return user
def get_active_users(self) -> List[User]:
"""
Retrieve all active users.
Returns:
List[User]: List of active user objects
Example:
>>> service = UserService(database)
>>> active_users = service.get_active_users()
>>> print(f"Found {len(active_users)} active users")
"""
users_data = self.database.find({"is_active": True})
return [User(**data) for data in users_data]
def _generate_id(self) -> int:
"""Generate a unique user ID."""
# Implementation here
return 1