🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
Search courses…
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Python Final Project — Building a Complete Application

Python ProjectsFinal Project🟢 Free Lesson

Advertisement

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:

  1. What problem are you solving? Be specific.
  2. Who is the user? Understand their needs.
  3. What are the core features? Start with MVP (Minimum Viable Product).
  4. 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

MistakeProblemSolution
No testsBugs reach productionWrite tests first (TDD)
Hardcoded configCan't deploy to different environmentsUse config classes
No error handlingCrashes on invalid inputAdd proper error handling
No loggingCan't debug issuesAdd structured logging
No documentationOthers can't understand codeWrite README and docstrings
No version controlCan't track changesUse git from day one
Big bang deploymentRisky releasesDeploy small, often

Key Takeaways

  1. Start with MVP — add features iteratively
  2. Write tests as you develop, not after
  3. Document your code and README
  4. Use version control (git) from day one
  5. Deploy early and often
  6. Get feedback from users early
  7. Refactor regularly — clean code is maintainable code
  8. Celebrate small wins — every feature is progress
  9. Follow the code review checklist
  10. Always have a rollback plan

Premium Content

Python Final Project — Building a Complete Application

Unlock this lesson and 900+ advanced tutorials with a Premium plan.

🎯End-to-end Projects
💼Interview Prep
📜Certificates
🤝Community Access

Already a member? Log in

Need Expert Python Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement