Python Guide
Applies to: Python 3.9+, Django, FastAPI, Flask, Data Science
Core Principles
- Readability Counts: "Code is read more often than written" (PEP 20)
- Type Hints: Use type annotations for better IDE support and validation
- Explicit is Better: Avoid magic, prefer clarity over cleverness
- Batteries Included: Use standard library when possible
- Virtual Environments: Always isolate dependencies
Language-Specific Guardrails
Python Version & Setup
✓ Use Python 3.9+ (3.11+ recommended for performance)
✓ Use virtual environments (venv, poetry, or conda)
✓ Pin dependencies in requirements.txt or pyproject.toml
✓ Include python version in README or .python-version file
Type Hints (PEP 484)
✓ All function signatures have type hints
✓ Use mypy for static type checking
✓ Use Optional[T] or T | None for nullable types
✓ Complex types via typing module: List, Dict, Union, Callable
✓ Type aliases for complex types: UserId = int
Code Style (PEP 8)
✓ Follow PEP 8 style guide (enforced by Black)
✓ Line length: 88 characters (Black default) or 100 max
✓ Use snake_case for functions and variables
✓ Use PascalCase for classes
✓ Use SCREAMING_SNAKE_CASE for constants
✓ 2 blank lines between top-level definitions
✓ Import order: stdlib → third-party → local (isort handles this)
Data Validation
✓ Use Pydantic for data models (FastAPI, APIs)
✓ Use dataclasses for simple data structures
✓ Validate user inputs before processing
✓ Use attrs or pydantic over manual __init__
Recommended Libraries
| Library | Description |
| Pydantic | Data validation with type hints (recommended) |
| Marshmallow | Serialization/deserialization (older projects) |
| attrs | Better classes with validation |
Pattern
from pydantic import BaseModel, EmailStr, validator
class UserCreate(BaseModel):
email: EmailStr
age: int
role: str
@validator('age')
def age_must_be_positive(cls, v):
if v <= 0:
raise ValueError('Age must be positive')
return v
@validator('role')
def role_must_be_valid(cls, v):
if v not in ['admin', 'user', 'guest']:
raise ValueError('Invalid role')
return v
# Usage
def create_user(data: dict) -> User:
validated = UserCreate(**data) # Raises ValidationError if invalid
return User.objects.create(**validated.dict())
Testing
Frameworks
| Framework | Use Case |
| pytest | Industry standard (recommended) |
| unittest | Built-in, verbose but works |
| hypothesis | Property-based testing |
Guardrails
✓ Test files: test_*.py or *_test.py (pytest convention)
✓ Test functions: test_function_name_should_do_something
✓ Use fixtures for setup: @pytest.fixture
✓ Mock external dependencies: unittest.mock or pytest-mock
✓ Test both success and error paths
✓ Coverage target: >80% for business logic
✓ Use pytest-cov for coverage reports
Example
import pytest
from myapp.services import UserService
from myapp.exceptions import InvalidEmailError
@pytest.fixture
def user_service():
return UserService()
def test_create_user_with_valid_data(user_service):
user = user_service.create(
email='test@example.com',
age=25
)
assert user.id is not None
assert user.email == 'test@example.com'
def test_create_user_with_invalid_email_raises_error(user_service):
with pytest.raises(InvalidEmailError):
user_service.create(
email='invalid',
age=25
)
@pytest.mark.parametrize('age', [0, -1, -100])
def test_create_user_with_invalid_age(user_service, age):
with pytest.raises(ValueError, match='Age must be positive'):
user_service.create(email='test@example.com', age=age)
| Tool | Purpose |
| Black | Code formatter (opinionated, consistent) |
| isort | Import sorting |
| mypy | Static type checker |
| ruff | Fast linter (replaces flake8, pylint) |
| poetry/pip-tools | Dependency management |
Configuration
# pyproject.toml
[tool.black]
line-length = 88
target-version = ['py311']
[tool.isort]
profile = "black"
line_length = 88
[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", "*_test.py"]
addopts = "--cov=myapp --cov-report=html --cov-report=term"
Pre-Commit Commands
# Format
black .
# Sort imports
isort .
# Type check
mypy .
# Lint
ruff check .
# Test
pytest
Common Pitfalls
Don't Do This
# ❌ No type hints
def calculate(a, b):
return a + b
# ❌ Mutable default arguments
def add_item(item, items=[]):
items.append(item)
return items
# ❌ Bare except
try:
risky_operation()
except:
pass
# ❌ Using == for None
if value == None:
pass
# ❌ Not using context managers
f = open('file.txt')
data = f.read()
f.close()
Do This Instead
# ✅ Proper type hints
def calculate(a: int, b: int) -> int:
return a + b
# ✅ Immutable defaults
def add_item(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = []
items.append(item)
return items
# ✅ Specific exception handling
try:
risky_operation()
except ValueError as e:
logger.error(f'Operation failed: {e}')
raise
# ✅ Use is for None
if value is None:
pass
# ✅ Context managers
with open('file.txt') as f:
data = f.read()
Framework-Specific Patterns
Django
# models.py
from django.db import models
class User(models.Model):
email = models.EmailField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
indexes = [models.Index(fields=['email'])]
# views.py
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
@login_required
def user_detail(request, user_id: int):
user = User.objects.select_related('profile').get(id=user_id)
return JsonResponse({
'id': user.id,
'email': user.email,
})
FastAPI
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
email: str
age: int
class UserResponse(BaseModel):
id: int
email: str
class Config:
orm_mode = True
@app.post('/users', response_model=UserResponse)
async def create_user(user: UserCreate):
# user is automatically validated
db_user = await create_user_in_db(user)
return db_user
@app.get('/users/{user_id}', response_model=UserResponse)
async def get_user(user_id: int):
user = await get_user_from_db(user_id)
if not user:
raise HTTPException(status_code=404, detail='User not found')
return user
Optimization Guardrails
✓ Use async/await for I/O-bound operations
✓ Database: Use select_related, prefetch_related (Django)
✓ Pagination for large datasets (not all())
✓ Cache expensive computations: functools.lru_cache
✓ Profile before optimizing: cProfile, line_profiler
✓ Use generators for large data processing
Example
from functools import lru_cache
from typing import Iterator
# Caching
@lru_cache(maxsize=128)
def expensive_computation(n: int) -> int:
# Complex calculation
return result
# Generators for memory efficiency
def process_large_file(filename: str) -> Iterator[dict]:
with open(filename) as f:
for line in f:
yield process_line(line)
# Async I/O
async def fetch_users() -> list[User]:
async with aiohttp.ClientSession() as session:
async with session.get('/api/users') as response:
return await response.json()
Security Best Practices
Guardrails
✓ Never use eval() or exec() on user input
✓ Use parameterized queries (ORM prevents SQL injection)
✓ Hash passwords: bcrypt or argon2
✓ Validate all user inputs with Pydantic/Marshmallow
✓ Use environment variables for secrets (python-decouple, django-environ)
✓ CSRF protection enabled (Django middleware)
Example
# Password hashing
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
# Environment variables
from decouple import config
DATABASE_URL = config('DATABASE_URL')
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
References