Python Final Project — Building a Complete Application
This tutorial guides you through building a real-world Python project from scratch, applying everything you have learned. You'll build a complete Task Manager API with testing, documentation, Docker, and CI/CD.
Learning Objectives
- Plan and structure a Python project
- Implement core functionality with best practices
- Write tests and documentation
- Set up CI/CD pipelines
- Deploy to production
- Conduct code reviews
Project Planning
Before writing code, answer these questions:
- What problem are you solving? Be specific.
- Who is the user? Understand their needs.
- What are the core features? Start with MVP (Minimum Viable Product).
- What tech stack? Choose tools you know (or want to learn).
Architecture Diagram
Example Project: Task Manager API
---------------------------------
Problem: Teams need a simple way to track tasks
User: Small teams (2-10 people)
Core Features:
- Create, read, update, delete tasks
- Assign tasks to team members
- Mark tasks as complete
- Filter by status/assignee
- User authentication
Tech Stack:
- FastAPI (web framework)
- PostgreSQL (database)
- SQLAlchemy (ORM)
- pytest (testing)
- Docker (deployment)
- GitHub Actions (CI/CD)
Project Structure
Architecture Diagram
task-manager/
+-- src/
| +-- task_manager/
| +-- __init__.py
| +-- main.py # Application entry point
| +-- config.py # Configuration
| +-- models.py # Database models
| +-- schemas.py # Pydantic schemas
| +-- routes/
| | +-- __init__.py
| | +-- tasks.py # Task endpoints
| | +-- auth.py # Auth endpoints
| +-- services/
| | +-- __init__.py
| | +-- task_service.py # Business logic
| | +-- auth_service.py # Auth logic
| +-- database.py # Database connection
| +-- dependencies.py # FastAPI dependencies
+-- tests/
| +-- __init__.py
| +-- conftest.py # Shared fixtures
| +-- test_models.py # Model tests
| +-- test_routes.py # API tests
| +-- test_services.py # Service tests
+-- docs/
| +-- api.md # API documentation
| +-- architecture.md # Architecture decisions
| +-- deployment.md # Deployment guide
+-- Dockerfile
+-- docker-compose.yml
+-- pyproject.toml
+-- requirements.txt
+-- README.md
+-- .env.example
+-- .gitignore
+-- .github/
+-- workflows/
+-- ci.yml # CI pipeline
+-- deploy.yml # CD pipeline
Implementation
Configuration (src/task_manager/config.py)
import os
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
# Required
database_url: str
secret_key: str
# Optional with defaults
debug: bool = False
host: str = "0.0.0.0"
port: int = 8000
environment: str = "development"
allowed_origins: list[str] = ["http://localhost:3000"]
# Database
db_pool_size: int = 10
db_max_overflow: int = 20
# Auth
access_token_expire_minutes: int = 30
refresh_token_expire_days: int = 7
@property
def is_production(self) -> bool:
return self.environment == "production"
settings = Settings()
Database Models (src/task_manager/models.py)
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum
from sqlalchemy.orm import relationship, declarative_base
from datetime import datetime
import enum
Base = declarative_base()
class TaskStatus(str, enum.Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(100), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
tasks = relationship("Task", back_populates="assignee")
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
description = Column(String(1000))
status = Column(Enum(TaskStatus), default=TaskStatus.PENDING)
assignee_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
assignee = relationship("User", back_populates="tasks")
Pydantic Schemas (src/task_manager/schemas.py)
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
order: 55
description: Optional[str] = Field(None, max_length=1000)
assignee_id: Optional[int] = None
class TaskUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=200)
order: 55
description: Optional[str] = Field(None, max_length=1000)
status: Optional[str] = None
assignee_id: Optional[int] = None
class TaskResponse(BaseModel):
id: int
title: str
order: 55
description: Optional[str]
status: str
assignee_id: Optional[int]
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
password: str = Field(..., min_length=8)
class UserResponse(BaseModel):
id: int
username: str
email: str
created_at: datetime
model_config = {"from_attributes": True}
class TokenResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
Task Routes (src/task_manager/routes/tasks.py)
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
from ..schemas import TaskCreate, TaskUpdate, TaskResponse
from ..services.task_service import TaskService
from ..dependencies import get_db, get_current_user
router = APIRouter(prefix="/tasks", tags=["tasks"])
@router.get("/", response_model=List[TaskResponse])
def list_tasks(
status: Optional[str] = None,
assignee_id: Optional[int] = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
):
service = TaskService(db)
return service.list_tasks(status=status, assignee_id=assignee_id, skip=skip, limit=limit)
@router.post("/", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
def create_task(
task: TaskCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
service = TaskService(db)
return service.create_task(task, assignee_id=current_user["user_id"])
@router.get("/{task_id}", response_model=TaskResponse)
def get_task(task_id: int, db: Session = Depends(get_db)):
service = TaskService(db)
task = service.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
@router.put("/{task_id}", response_model=TaskResponse)
def update_task(
task_id: int,
task: TaskUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
service = TaskService(db)
updated = service.update_task(task_id, task)
if not updated:
raise HTTPException(status_code=404, detail="Task not found")
return updated
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_task(
task_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
service = TaskService(db)
if not service.delete_task(task_id):
raise HTTPException(status_code=404, detail="Task not found")
Tests (tests/test_routes.py)
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.task_manager.main import app
from src.task_manager.database import Base
from src.task_manager.dependencies import get_db
# Test database
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
TestSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def override_get_db():
db = TestSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
@pytest.fixture(autouse=True)
def setup_db():
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
def test_create_task():
response = client.post("/tasks", json={"title": "Buy groceries"})
assert response.status_code == 201
data = response.json()
assert data["title"] == "Buy groceries"
assert data["status"] == "pending"
def test_list_tasks():
client.post("/tasks", json={"title": "Task 1"})
client.post("/tasks", json={"title": "Task 2"})
response = client.get("/tasks")
assert response.status_code == 200
assert len(response.json()) == 2
def test_get_task():
response = client.post("/tasks", json={"title": "Test task"})
task_id = response.json()["id"]
response = client.get(f"/tasks/{task_id}")
assert response.status_code == 200
assert response.json()["title"] == "Test task"
def test_update_task():
response = client.post("/tasks", json={"title": "Original"})
task_id = response.json()["id"]
response = client.put(f"/tasks/{task_id}", json={"title": "Updated"})
assert response.status_code == 200
assert response.json()["title"] == "Updated"
def test_complete_task():
response = client.post("/tasks", json={"title": "Test task"})
task_id = response.json()["id"]
response = client.put(f"/tasks/{task_id}", json={"status": "completed"})
assert response.status_code == 200
assert response.json()["status"] == "completed"
def test_delete_task():
response = client.post("/tasks", json={"title": "To delete"})
task_id = response.json()["id"]
response = client.delete(f"/tasks/{task_id}")
assert response.status_code == 204
response = client.get(f"/tasks/{task_id}")
assert response.status_code == 404
def test_task_not_found():
response = client.get("/tasks/999")
assert response.status_code == 404
Docker Configuration
Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Run the application
CMD ["uvicorn", "src.task_manager.main:app", "--host", "0.0.0.0", "--port", "8000"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/taskmanager
- SECRET_KEY=your-secret-key
depends_on:
- db
volumes:
- .:/app
db:
image: postgres:15
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=taskmanager
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7
ports:
- "6379:6379"
volumes:
postgres_data:
CI/CD Pipeline
.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run linter
run: ruff check src/ tests/
- name: Run type checker
run: mypy src/
- name: Run tests
run: pytest --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Code Review Checklist
## Code Quality
- [ ] No unused imports
- [ ] No commented-out code
- [ ] Consistent naming conventions
- [ ] Type hints on all functions
- [ ] Docstrings on public functions
## Security
- [ ] No hardcoded secrets
- [ ] Input validation on all endpoints
- [ ] Authentication on protected routes
- [ ] SQL injection prevention (using ORM)
- [ ] Rate limiting implemented
## Testing
- [ ] Unit tests for services
- [ ] Integration tests for routes
- [ ] Edge cases covered
- [ ] Error scenarios tested
- [ ] > 80% code coverage
## Performance
- [ ] Database queries optimized
- [ ] Pagination implemented
- [ ] Caching where appropriate
- [ ] N+1 query prevention
## Documentation
- [ ] README with setup instructions
- [ ] API documentation
- [ ] Environment variables documented
- [ ] Deployment guide
Deployment Checklist
## Pre-Deployment
- [ ] All tests passing
- [ ] Linter and type checker clean
- [ ] Environment variables configured
- [ ] Database migrations run
- [ ] Secrets stored in secret manager
## Infrastructure
- [ ] Docker image built and tested
- [ ] Database backups configured
- [ ] Monitoring and alerting set up
- [ ] Log aggregation configured
- [ ] SSL/TLS certificates installed
## Post-Deployment
- [ ] Smoke tests passing
- [ ] Health check endpoints responding
- [ ] Error rates within acceptable limits
- [ ] Performance metrics baseline established
- [ ] Rollback plan documented
Common Mistakes
| Mistake | Problem | Solution |
|---|---|---|
| No tests | Bugs reach production | Write tests first (TDD) |
| Hardcoded config | Can't deploy to different environments | Use config classes |
| No error handling | Crashes on invalid input | Add proper error handling |
| No logging | Can't debug issues | Add structured logging |
| No documentation | Others can't understand code | Write README and docstrings |
| No version control | Can't track changes | Use git from day one |
| Big bang deployment | Risky releases | Deploy small, often |
Key Takeaways
- Start with MVP — add features iteratively
- Write tests as you develop, not after
- Document your code and README
- Use version control (git) from day one
- Deploy early and often
- Get feedback from users early
- Refactor regularly — clean code is maintainable code
- Celebrate small wins — every feature is progress
- Follow the code review checklist
- Always have a rollback plan