How to Build and Deploy a Python Project: A Complete Step-by-Step Guide for Beginners

So, you’ve got an idea for a Python project—awesome! But what’s next?
Whether you're creating a data science tool, a web scraper, an automation script, or a full web app, turning your idea into a live project can feel a bit daunting, especially if you're unsure where to begin.
In this guide, I'll take you through the entire Python project lifecycle, from setting up your environment to deploying your finished product. Think of it as your personal roadmap, perfect for beginners eager to build real-world projects, and also useful for experienced developers looking to streamline their workflow.
Let’s jump in and turn that idea into a reality. We’ll build a practical example throughout this guide: a Task Management CLI Tool that showcases real-world development patterns.
Project Planning: Setting the Foundation
Define Your Goal and Scope
Before writing a single line of code, clearly define what you’re building. For our example project:
Goal: Create a command-line task management tool that allows users to add, list, complete, and delete tasks.
Scope:
Add new tasks with descriptions and priorities
List all tasks or filter by status
Mark tasks as complete
Delete tasks
Store data persistently in a JSON file
Provide a clean CLI interface
Success Criteria:
Intuitive command-line interface
Data persistence between sessions
Error handling for edge cases
Comprehensive test coverage (>90%)
Clear documentation
Select Tools and Frameworks
Choose your tech stack based on project requirements:
For our Task Manager CLI:
Core Framework: Pure Python
CLI Framework:
click
(elegant command-line interfaces)Data Storage: JSON files (simple, no database required)
Testing:
pytest
Code Quality:
black
,flake8
,mypy
Documentation:
MkDocs
with material themeDependency Management:
poetry
Decision Matrix Template:
| Tool Category | Options Considered | Chosen | Reason |
|----------------|------------------------|--------|------------------------------------|
| CLI Framework | argparse, click | click | Better UX, decorators |
| Testing | unittest, pytest | pytest | More features, fixtures |
| Packaging | pip, poetry | poetry | Better dependency resolution |
Environment Setup: Creating Isolation
Creating a Virtual Environment
Virtual environments prevent dependency conflicts between projects. Here are three common approaches:
Option 1: Using venv (built-in)
# Create virtual environment
python -m venv task-manager-env
# Activate (Linux/Mac)
source task-manager-env/bin/activate
# Activate (Windows)
task-manager-env\Scripts\activate
# Verify activation
which python # Should point to your venv
Option 2: Using conda
# Create environment with specific Python version
conda create -n task-manager python=3.11
conda activate task-manager
Option 3: Using poetry (recommended)
# Install poetry first
curl -sSL https://install.python-poetry.org | python3 -
# Create new project (automatically creates venv)
poetry new task-manager
cd task-manager
Dependency Management
With Poetry:
# Add dependencies
poetry add click colorama
poetry add --group dev pytest black flake8 mypy
# Install all dependencies
poetry install
With pip:
# Install dependencies
pip install click colorama
# Development dependencies
pip install pytest black flake8 mypy
# Generate requirements file
pip freeze > requirements.txt
Configuration Files
pyproject.toml (Poetry)
[tool.poetry]
name = "task-manager"
version = "0.1.0"
description = "A simple CLI task management tool"
authors = ["Your Name your.email@example.com"]
readme = "README.md"
packages = [{include = "task_manager"}]
[tool.poetry.dependencies]
python = "^3.9"
click = "^8.1.0"
colorama = "^0.4.6"
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
black = "^23.7.0"
flake8 = "^6.0.0"
mypy = "^1.5.0"
pytest-cov = "^4.1.0"
[tool.poetry.scripts]
task-manager = "task_manager.cli:main"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 88
target-version = ['py39']
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "--cov=task_manager --cov-report=html --cov-report=term-missing"
requirements.txt (pip)
click==8.1.7
colorama==0.4.6
# Development dependencies
pytest==7.4.3
black==23.9.1
flake8==6.1.0
mypy==1.6.1
pytest-cov==4.1.0
Project Structure: Organization Matters
Directory Structure
A well-organized project structure improves maintainability and collaboration:
task-manager/
├── task_manager/ # Main package
│ ├── __init__.py
│ ├── cli.py # CLI interface
│ ├── models.py # Data models
│ ├── storage.py # Data persistence
│ └── utils.py # Helper functions
├── tests/ # Test suite
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_storage.py
│ ├── test_cli.py
│ └── fixtures/ # Test data
├── docs/ # Documentation
│ ├── index.md
│ └── api/
├── scripts/ # Utility scripts
│ └── setup_dev.sh
├── .env.example # Environment variables template
├── .gitignore # Git ignore rules
├── README.md # Project documentation
├── pyproject.toml # Project configuration
├── Dockerfile # Container setup
└── .github/ # GitHub workflows
└── workflows/
└── ci.yml
Core Module Structure
task_manager/init.py
"""Task Manager CLI Tool.
A simple command-line task management application.
"""
__version__ = "0.1.0"
__author__ = "Your Name"
__email__ = "your.email@example.com"
from .models import Task, TaskStatus
from .storage import TaskStorage
__all__ = ["Task", "TaskStatus", "TaskStorage"]
task_manager/models.py
"""Data models for the task manager."""
from datetime import datetime
from enum import Enum
from typing import Dict, Any
from dataclasses import dataclass, asdict
import uuid
class TaskStatus(Enum):
"""Task status enumeration."""
PENDING = "pending"
COMPLETED = "completed"
class Priority(Enum):
"""Task priority enumeration."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
@dataclass
class Task:
"""Task data model."""
id: str
title: str
description: str = ""
status: TaskStatus = TaskStatus.PENDING
priority: Priority = Priority.MEDIUM
created_at: datetime = None
completed_at: datetime = None
def __post_init__(self):
"""Initialize computed fields."""
if self.created_at is None:
self.created_at = datetime.now()
@classmethod
def create(cls, title: str, description: str = "", priority: Priority = Priority.MEDIUM) -> "Task":
"""Create a new task with generated ID."""
return cls(
id=str(uuid.uuid4()),
title=title,
description=description,
priority=priority
)
def complete(self) -> None:
"""Mark task as completed."""
self.status = TaskStatus.COMPLETED
self.completed_at = datetime.now()
def to_dict(self) -> Dict[str, Any]:
"""Convert task to dictionary."""
data = asdict(self)
# Convert enums to strings
data['status'] = self.status.value
data['priority'] = self.priority.value
# Convert datetime to ISO format
data['created_at'] = self.created_at.isoformat() if self.created_at else None
data['completed_at'] = self.completed_at.isoformat() if self.completed_at else None
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Task":
"""Create task from dictionary."""
# Convert strings back to enums
data['status'] = TaskStatus(data['status'])
data['priority'] = Priority(data['priority'])
# Convert ISO strings back to datetime
if data['created_at']:
data['created_at'] = datetime.fromisoformat(data['created_at'])
if data['completed_at']:
data['completed_at'] = datetime.fromisoformat(data['completed_at'])
return cls(**data)
Configuration Files
.env.example
# Task Manager Configuration
TASK_DATA_FILE=~/.task_manager/tasks.json
LOG_LEVEL=INFO
DEBUG=false
.gitignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
env/
ENV/
task-manager-env/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# Environment variables
.env
# OS
.DS_Store
Thumbs.db
# Application specific
tasks.json
*.log
Version Control: Tracking Changes
Initializing Git Repository
# Initialize repository
git init
# Add all files
git add .
# Initial commit
git commit -m "feat: initial project setup with basic structure
- Add project structure with core modules
- Configure poetry for dependency management
- Set up development tools (black, flake8, mypy)
- Add comprehensive .gitignore
- Create basic data models for tasks"
Writing Meaningful Commit Messages
Follow the Conventional Commits specification:
# Format: ():
# Types: feat, fix, docs, style, refactor, test, chore
git commit -m "feat(models): add Task model with status and priority"
git commit -m "fix(storage): handle file not found error gracefully"
git commit -m "docs: add API documentation for TaskStorage class"
git commit -m "test: add unit tests for Task model methods"
git commit -m "refactor(cli): extract command handlers to separate functions"
Remote Repository Setup
# Create repository on GitHub/GitLab first, then:
git remote add origin https://github.com/yourusername/task-manager.git
git branch -M main
git push -u origin main
# Create development branch
git checkout -b develop
git push -u origin develop
Branching Strategy
# Feature development
git checkout -b feature/add-priority-filtering
# ... make changes ...
git add .
git commit -m "feat(cli): add priority filtering for list command"
git push origin feature/add-priority-filtering
# Create pull request, then merge and clean up
git checkout main
git pull origin main
git branch -d feature/add-priority-filtering
Development Workflow: Writing Quality Code
Storage Layer Implementation
task_manager/storage.py
"""Data storage and persistence layer."""
import json
import logging
from pathlib import Path
from typing import List, Optional, Dict, Any
from .models import Task, TaskStatus, Priority
logger = logging.getLogger(__name__)
class TaskStorageError(Exception):
"""Custom exception for storage operations."""
pass
class TaskStorage:
"""Handles task data persistence."""
def __init__(self, data_file: Path):
"""Initialize storage with data file path."""
self.data_file = Path(data_file).expanduser()
self.data_file.parent.mkdir(parents=True, exist_ok=True)
logger.info(f"TaskStorage initialized with file: {self.data_file}")
def save_tasks(self, tasks: List[Task]) -> None:
"""Save tasks to file."""
try:
data = [task.to_dict() for task in tasks]
with open(self.data_file, 'w') as f:
json.dump(data, f, indent=2)
logger.info(f"Saved {len(tasks)} tasks to {self.data_file}")
except (IOError, OSError) as e:
error_msg = f"Failed to save tasks: {e}"
logger.error(error_msg)
raise TaskStorageError(error_msg) from e
def load_tasks(self) -> List[Task]:
"""Load tasks from file."""
if not self.data_file.exists():
logger.info("Data file doesn't exist, returning empty task list")
return []
try:
with open(self.data_file, 'r') as f:
data = json.load(f)
tasks = [Task.from_dict(task_data) for task_data in data]
logger.info(f"Loaded {len(tasks)} tasks from {self.data_file}")
return tasks
except json.JSONDecodeError as e:
error_msg = f"Invalid JSON in data file: {e}"
logger.error(error_msg)
raise TaskStorageError(error_msg) from e
except (IOError, OSError) as e:
error_msg = f"Failed to load tasks: {e}"
logger.error(error_msg)
raise TaskStorageError(error_msg) from e
def backup_data(self) -> Optional[Path]:
"""Create a backup of the current data file."""
if not self.data_file.exists():
return None
backup_file = self.data_file.with_suffix('.json.backup')
try:
backup_file.write_text(self.data_file.read_text())
logger.info(f"Created backup: {backup_file}")
return backup_file
except (IOError, OSError) as e:
logger.error(f"Failed to create backup: {e}")
return None
CLI Interface
task_manager/cli.py
"""Command-line interface for task manager."""
import os
import logging
from pathlib import Path
from typing import Optional
import click
from colorama import init, Fore, Style
from .models import Task, TaskStatus, Priority
from .storage import TaskStorage, TaskStorageError
# Initialize colorama for cross-platform colored output
init()
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class TaskManager:
"""Task manager business logic."""
def __init__(self, storage: TaskStorage):
"""Initialize with storage backend."""
self.storage = storage
self._tasks = None
@property
def tasks(self) -> list[Task]:
"""Get current tasks, loading from storage if needed."""
if self._tasks is None:
self._tasks = self.storage.load_tasks()
return self._tasks
def add_task(self, title: str, description: str = "", priority: Priority = Priority.MEDIUM) -> Task:
"""Add a new task."""
task = Task.create(title=title, description=description, priority=priority)
self.tasks.append(task)
self._save_tasks()
logger.info(f"Added task: {task.title}")
return task
def complete_task(self, task_id: str) -> Optional[Task]:
"""Mark a task as completed."""
task = self._find_task(task_id)
if task:
task.complete()
self._save_tasks()
logger.info(f"Completed task: {task.title}")
return task
def delete_task(self, task_id: str) -> Optional[Task]:
"""Delete a task."""
task = self._find_task(task_id)
if task:
self.tasks.remove(task)
self._save_tasks()
logger.info(f"Deleted task: {task.title}")
return task
def list_tasks(self, status: Optional[TaskStatus] = None, priority: Optional[Priority] = None) -> list[Task]:
"""List tasks with optional filtering."""
filtered_tasks = self.tasks
if status:
filtered_tasks = [t for t in filtered_tasks if t.status == status]
if priority:
filtered_tasks = [t for t in filtered_tasks if t.priority == priority]
return filtered_tasks
def _find_task(self, task_id: str) -> Optional[Task]:
"""Find task by ID or partial ID."""
# Try exact match first
for task in self.tasks:
if task.id == task_id:
return task
# Try partial match
matching_tasks = [t for t in self.tasks if t.id.startswith(task_id)]
if len(matching_tasks) == 1:
return matching_tasks[0]
elif len(matching_tasks) > 1:
raise click.ClickException(f"Ambiguous task ID '{task_id}'. Multiple matches found.")
return None
def _save_tasks(self) -> None:
"""Save tasks to storage."""
try:
self.storage.save_tasks(self.tasks)
except TaskStorageError as e:
raise click.ClickException(f"Failed to save tasks: {e}")
# Global task manager instance
task_manager = None
def get_task_manager() -> TaskManager:
"""Get or create task manager instance."""
global task_manager
if task_manager is None:
data_file = os.getenv('TASK_DATA_FILE', '~/.task_manager/tasks.json')
storage = TaskStorage(Path(data_file))
task_manager = TaskManager(storage)
return task_manager
def format_task(task: Task, show_id: bool = False) -> str:
"""Format task for display."""
status_color = Fore.GREEN if task.status == TaskStatus.COMPLETED else Fore.YELLOW
priority_symbol = {"low": "◦", "medium": "●", "high": "◉"}[task.priority.value]
parts = []
if show_id:
parts.append(f"{Fore.CYAN}{task.id[:8]}{Style.RESET_ALL}")
parts.extend([
f"{status_color}{priority_symbol}{Style.RESET_ALL}",
f"{Fore.WHITE}{task.title}{Style.RESET_ALL}"
])
if task.description:
parts.append(f"{Fore.LIGHTBLACK_EX}- {task.description}{Style.RESET_ALL}")
return " ".join(parts)
@click.group()
@click.option('--debug', is_flag=True, help='Enable debug logging')
def cli(debug: bool):
"""Task Manager CLI - A simple task management tool."""
if debug:
logging.getLogger().setLevel(logging.DEBUG)
@cli.command()
@click.argument('title')
@click.option('--description', '-d', default='', help='Task description')
@click.option('--priority', '-p', type=click.Choice(['low', 'medium', 'high']),
default='medium', help='Task priority')
def add(title: str, description: str, priority: str):
"""Add a new task."""
try:
priority_enum = Priority(priority)
task = get_task_manager().add_task(title, description, priority_enum)
click.echo(f"{Fore.GREEN}✓{Style.RESET_ALL} Added task: {format_task(task)}")
except Exception as e:
click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True)
raise click.Abort()
@cli.command()
@click.option('--status', type=click.Choice(['pending', 'completed']), help='Filter by status')
@click.option('--priority', type=click.Choice(['low', 'medium', 'high']), help='Filter by priority')
@click.option('--show-id', is_flag=True, help='Show task IDs')
def list(status: Optional[str], priority: Optional[str], show_id: bool):
"""List tasks."""
try:
status_enum = TaskStatus(status) if status else None
priority_enum = Priority(priority) if priority else None
tasks = get_task_manager().list_tasks(status_enum, priority_enum)
if not tasks:
click.echo(f"{Fore.YELLOW}No tasks found.{Style.RESET_ALL}")
return
click.echo(f"\n{Fore.CYAN}Tasks:{Style.RESET_ALL}")
for task in tasks:
click.echo(f" {format_task(task, show_id)}")
click.echo()
except Exception as e:
click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True)
raise click.Abort()
@cli.command()
@click.argument('task_id')
def complete(task_id: str):
"""Mark a task as completed."""
try:
task = get_task_manager().complete_task(task_id)
if task:
click.echo(f"{Fore.GREEN}✓{Style.RESET_ALL} Completed: {format_task(task)}")
else:
click.echo(f"{Fore.RED}Task not found: {task_id}{Style.RESET_ALL}", err=True)
except Exception as e:
click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True)
raise click.Abort()
@cli.command()
@click.argument('task_id')
@click.confirmation_option(prompt='Are you sure you want to delete this task?')
def delete(task_id: str):
"""Delete a task."""
try:
task = get_task_manager().delete_task(task_id)
if task:
click.echo(f"{Fore.GREEN}✓{Style.RESET_ALL} Deleted: {task.title}")
else:
click.echo(f"{Fore.RED}Task not found: {task_id}{Style.RESET_ALL}", err=True)
except Exception as e:
click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True)
raise click.Abort()
def main():
"""Entry point for the CLI."""
cli()
if __name__ == '__main__':
main()
Code Quality Tools
Setup script for development tools:
#!/bin/bash
# scripts/setup_dev.sh
echo "Setting up development environment..."
# Format code
echo "Formatting code with black..."
poetry run black task_manager/ tests/
# Check code style
echo "Checking code style with flake8..."
poetry run flake8 task_manager/ tests/
# Type checking
echo "Running type checks with mypy..."
poetry run mypy task_manager/
# Run tests
echo "Running tests with pytest..."
poetry run pytest
echo "Development setup complete!"
Testing: Ensuring Reliability
Test Structure
tests/conftest.py
"""Pytest configuration and fixtures."""
import pytest
from pathlib import Path
import tempfile
import shutil
from task_manager.storage import TaskStorage
from task_manager.models import Task, Priority, TaskStatus
@pytest.fixture
def temp_dir():
"""Create temporary directory for tests."""
temp_dir = Path(tempfile.mkdtemp())
yield temp_dir
shutil.rmtree(temp_dir)
@pytest.fixture
def storage(temp_dir):
"""Create TaskStorage instance with temporary file."""
data_file = temp_dir / "test_tasks.json"
return TaskStorage(data_file)
@pytest.fixture
def sample_task():
"""Create a sample task for testing."""
return Task.create(
title="Test Task",
description="A task for testing",
priority=Priority.HIGH
)
@pytest.fixture
def sample_tasks():
"""Create multiple sample tasks."""
return [
Task.create("Task 1", "First task", Priority.HIGH),
Task.create("Task 2", "Second task", Priority.MEDIUM),
Task.create("Task 3", "Third task", Priority.LOW),
]
Unit Tests
tests/test_models.py
"""Tests for data models."""
import pytest
from datetime import datetime
from task_manager.models import Task, TaskStatus, Priority
class TestTask:
"""Test cases for Task model."""
def test_task_creation(self):
"""Test basic task creation."""
task = Task.create("Test Task", "Description", Priority.HIGH)
assert task.title == "Test Task"
assert task.description == "Description"
assert task.priority == Priority.HIGH
assert task.status == TaskStatus.PENDING
assert task.id is not None
assert isinstance(task.created_at, datetime)
assert task.completed_at is None
def test_task_completion(self):
"""Test task completion."""
task = Task.create("Test Task")
initial_time = datetime.now()
task.complete()
assert task.status == TaskStatus.COMPLETED
assert task.completed_at is not None
assert task.completed_at >= initial_time
def test_task_serialization(self):
"""Test task to/from dict conversion."""
original_task = Task.create("Test Task", "Description", Priority.HIGH)
original_task.complete()
# Convert to dict and back
task_dict = original_task.to_dict()
restored_task = Task.from_dict(task_dict)
assert restored_task.id == original_task.id
assert restored_task.title == original_task.title
assert restored_task.description == original_task.description
assert restored_task.priority == original_task.priority
assert restored_task.status == original_task.status
assert restored_task.created_at == original_task.created_at
assert restored_task.completed_at == original_task.completed_at
def test_task_dict_format(self):
"""Test task dictionary format."""
task = Task.create("Test Task", "Description", Priority.HIGH)
task_dict = task.to_dict()
assert 'id' in task_dict
assert 'title' in task_dict
assert 'description' in task_dict
assert task_dict['priority'] == 'high'
assert task_dict['status'] == 'pending'
assert 'created_at' in task_dict
assert 'completed_at' in task_dict
tests/test_storage.py
"""Tests for storage layer."""
import pytest
import json
from pathlib import Path
from task_manager.storage import TaskStorage, TaskStorageError
from task_manager.models import Task, Priority
class TestTaskStorage:
"""Test cases for TaskStorage."""
def test_save_and_load_tasks(self, storage, sample_tasks):
"""Test saving and loading tasks."""
# Save tasks
storage.save_tasks(sample_tasks)
# Load tasks
loaded_tasks = storage.load_tasks()
assert len(loaded_tasks) == len(sample_tasks)
assert loaded_tasks[0].title == sample_tasks[0].title
assert loaded_tasks[1].priority == sample_tasks[1].priority
def test_load_empty_file(self, temp_dir):
"""Test loading from non-existent file."""
storage = TaskStorage(temp_dir / "nonexistent.json")
tasks = storage.load_tasks()
assert tasks == []
def test_load_invalid_json(self, temp_dir):
"""Test loading from file with invalid JSON."""
data_file = temp_dir / "invalid.json"
data_file.write_text("invalid json content")
storage = TaskStorage(data_file)
with pytest.raises(TaskStorageError):
storage.load_tasks()
def test_save_to_read_only_location(self, temp_dir):
"""Test saving to read-only location."""
# Create read-only directory
readonly_dir = temp_dir / "readonly"
readonly_dir.mkdir()
readonly_dir.chmod(0o444)
storage = TaskStorage(readonly_dir / "tasks.json")
with pytest.raises(TaskStorageError):
storage.save_tasks([Task.create("Test")])
def test_backup_creation(self, storage, sample_tasks):
"""Test backup file creation."""
# Save tasks first
storage.save_tasks(sample_tasks)
# Create backup
backup_file = storage.backup_data()
assert backup_file is not None
assert backup_file.exists()
assert backup_file.suffix == '.backup'
# Verify backup content
with open(backup_file) as f:
backup_data = json.load(f)
assert len(backup_data) == len(sample_tasks)
Integration Tests
tests/test_cli.py
"""Tests for CLI interface."""
import pytest
from click.testing import CliRunner
from task_manager.cli import cli
import os
import tempfile
@pytest.fixture
def runner():
"""Create CLI test runner."""
return CliRunner()
@pytest.fixture
def temp_data_file():
"""Create temporary data file for CLI tests."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
temp_file = f.name
# Set environment variable for CLI
original_value = os.environ.get('TASK_DATA_FILE')
os.environ['TASK_DATA_FILE'] = temp_file
yield temp_file
# Cleanup
if original_value is not None:
os.environ['TASK_DATA_FILE'] = original_value
else:
os.environ.pop('TASK_DATA_FILE', None)
if os.path.exists(temp_file):
os.unlink(temp_file)
class TestCLI:
"""Test cases for CLI interface."""
def test_add_task(self, runner, temp_data_file):
"""Test adding a task via CLI."""
result = runner.invoke(cli, [
'add', 'Test Task',
'--description', 'Test description',
'--priority', 'high'
])
assert result.exit_code == 0
assert 'Added task' in result.output
assert 'Test Task' in result.output
def test_list_empty_tasks(self, runner, temp_data_file):
"""Test listing when no tasks exist."""
result = runner.invoke(cli, ['list'])
assert result.exit_code == 0
assert 'No tasks found' in result.output
def test_add_and_list_tasks(self, runner, temp_data_file):
"""Test adding and listing tasks."""
# Add tasks
runner.invoke(cli, ['add', 'Task 1', '--priority', 'high'])
runner.invoke(cli, ['add', 'Task 2', '--priority', 'low'])
# List tasks
result = runner.invoke(cli, ['list'])
assert result.exit_code == 0
assert 'Task 1' in result.output
assert 'Task 2' in result.output
def test_complete_task(self, runner, temp_data_file):
"""Test completing a task."""
# Add a task first
add_result = runner.invoke(cli, ['add', 'Test Task'])
assert add_result.exit_code == 0
# List with IDs to get task ID
list_result = runner.invoke(cli, ['list', '--show-id'])
task_id = list_result.output.split()[1] # Extract first task ID
# Complete the task
complete_result = runner.invoke(cli, ['complete', task_id[:8]]) # Use partial ID
assert complete_result.exit_code == 0
assert 'Completed' in complete_result.output
def test_delete_task_with_confirmation(self, runner, temp_data_file):
"""Test deleting a task with confirmation."""
# Add a task first
runner.invoke(cli, ['add', 'Task to delete'])
# Get task ID
list_result = runner.invoke(cli, ['list', '--show-id'])
task_id = list_result.output.split()[1]
# Delete with confirmation
result = runner.invoke(cli, ['delete', task_id[:8]], input='y\n')
assert result.exit_code == 0
assert 'Deleted' in result.output
def test_filter_by_status(self, runner, temp_data_file):
"""Test filtering tasks by status."""
# Add and complete some tasks
runner.invoke(cli, ['add', 'Pending Task'])
add_result = runner.invoke(cli, ['add', 'Completed Task'])
# Get task ID and complete it
list_result = runner.invoke(cli, ['list', '--show-id'])
lines = list_result.output.strip().split('\n')
task_line = [line for line in lines if 'Completed Task' in line][0]
task_id = task_line.split()[0]
runner.invoke(cli, ['complete', task_id])
# Filter by status
pending_result = runner.invoke(cli, ['list', '--status', 'pending'])
completed_result = runner.invoke(cli, ['list', '--status', 'completed'])
assert 'Pending Task' in pending_result.output
assert 'Completed Task' not in pending_result.output
assert 'Completed Task' in completed_result.output
assert 'Pending Task' not in completed_result.output
class TestTaskManagerIntegration:
"""Integration tests for TaskManager class."""
def test_task_lifecycle(self, storage):
"""Test complete task lifecycle."""
from task_manager.cli import TaskManager
from task_manager.models import Priority, TaskStatus
manager = TaskManager(storage)
# Add tasks
task1 = manager.add_task("Task 1", "Description 1", Priority.HIGH)
task2 = manager.add_task("Task 2", "Description 2", Priority.LOW)
# List tasks
all_tasks = manager.list_tasks()
assert len(all_tasks) == 2
# Filter by priority
high_priority = manager.list_tasks(priority=Priority.HIGH)
assert len(high_priority) == 1
assert high_priority[0].title == "Task 1"
# Complete task
completed = manager.complete_task(task1.id)
assert completed is not None
assert completed.status == TaskStatus.COMPLETED
# Filter by status
pending_tasks = manager.list_tasks(status=TaskStatus.PENDING)
assert len(pending_tasks) == 1
# Delete task
deleted = manager.delete_task(task2.id)
assert deleted is not None
final_tasks = manager.list_tasks()
assert len(final_tasks) == 1
Test Coverage Configuration
pytest.ini (alternative to pyproject.toml)
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--cov=task_manager
--cov-report=html
--cov-report=term-missing
--cov-report=xml
--strict-markers
--disable-warnings
markers =
slow: marks tests as slow
integration: marks tests as integration tests
unit: marks tests as unit tests
Running Tests
# Run all tests
poetry run pytest
# Run with coverage
poetry run pytest --cov=task_manager --cov-report=html
# Run specific test file
poetry run pytest tests/test_models.py
# Run with markers
poetry run pytest -m "not slow"
# Run in verbose mode
poetry run pytest -v
# Run and stop on first failure
poetry run pytest -x
Documentation: Making It Accessible
README.md
# Task Manager CLI
A simple, elegant command-line task management tool built with Python.
## Features
- ✅ Add tasks with descriptions and priorities
- 📋 List tasks with filtering options
- ✔️ Mark tasks as completed
- 🗑️ Delete tasks
- 💾 Persistent storage in JSON format
- 🎨 Colorful, intuitive CLI interface
## Installation
### From Source
```bash
git clone https://github.com/yourusername/task-manager.git
cd task-manager
poetry install
Using pip
pip install task-manager-cli
Quick Start
# Add your first task
task-manager add "Buy groceries" --description "Milk, eggs, bread" --priority high
# Add 'poetry run' in case of powershell issues
poetry run task-manager add "Buy groceries" --description "Milk, eggs, bread" --priority high
# List all tasks
task-manager list
# Complete a task (use task ID from list command)
complete abc123
# Delete a task
task-manager delete abc123
Usage
Adding Tasks
# Simple task
task-manager add "Call mom"
# Task with description and priority
task-manager add "Finish project" --description "Complete final review" --priority high
Listing Tasks
# List all tasks
task-manager list
# Filter by status
task-manager list --status pending
task-manager list --status completed
# Filter by priority
task-manager list --priority high
# Show task IDs
task-manager list --show-id
Managing Tasks
# Complete a task
task-manager complete
# Delete a task (with confirmation)
task-manager delete
Configuration
Set environment variables in .env file:
TASK_DATA_FILE=~/.task_manager/tasks.json
LOG_LEVEL=INFO
DEBUG=false
Development
Setup
git clone https://github.com/timothykimutai/task-manager.git
cd task-manager
poetry install
poetry shell
Running Tests
pytest
pytest --cov=task_manager --cov-report=html
Code Quality
# Format code
black task_manager/ tests/
# Lint code
flake8 task_manager/ tests/
# Type checking
mypy task_manager/
Contributing
Fork the repository
Create a feature branch (
git checkout -b feature/amazing-feature
)Commit your changes (
git commit -m 'Add amazing feature'
)Push to the branch (
git push origin feature/amazing-feature
)Open a Pull Request
License
This project is licensed under the MIT License — see the LICENSE file for details.
### API Documentation with MkDocs
**mkdocs.yml**
```yaml
site_name: Task Manager CLI Documentation
site_description: A simple command-line task management tool
site_author: Timothy Kimutai
site_url: https://timothykimutai.github.io/task-manager
theme:
name: material
palette:
- scheme: default
primary: blue
accent: blue
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: blue
accent: blue
toggle:
icon: material/brightness-4
name: Switch to light mode
features:
- navigation.tabs
- navigation.sections
- navigation.expand
- search.highlight
plugins:
- search
- mkdocstrings:
handlers:
python:
options:
docstring_style: google
nav:
- Home: index.md
- User Guide:
- Installation: user-guide/installation.md
- Quick Start: user-guide/quickstart.md
- Commands: user-guide/commands.md
- API Reference:
- Models: api/models.md
- Storage: api/storage.md
- CLI: api/cli.md
- Development:
- Contributing: development/contributing.md
- Testing: development/testing.md
markdown_extensions:
- admonition
- codehilite
- pymdownx.superfences
- pymdownx.tabbed
- toc:
permalink: true
docs/index.md
# Task Manager CLI
Welcome to the Task Manager CLI documentation!
Task Manager is a simple, elegant command-line tool for managing your daily tasks. Built with Python, it provides a clean interface for adding, listing, completing, and organizing your tasks.
## Why Task Manager CLI?
- **Simple**: Intuitive commands that are easy to remember
- **Fast**: Quick task management from your terminal
- **Persistent**: Your tasks are saved automatically
- **Flexible**: Filter and organize tasks by status and priority
- **Beautiful**: Colorful output that's easy to read
## Key Features
!!! tip "Core Functionality"
Add tasks with descriptions and priorities
List tasks with filtering options
Mark tasks as completed
Delete tasks you no longer need
Persistent JSON storage
Colorful CLI interface
## Quick Example
```bash
# Add a high-priority task
$ task-manager add "Review quarterly report" --priority high
# List all pending tasks
$ task-manager list --status pending
# Complete a task
$ task-manager complete abc123
Deployment & Execution
Local Development
# Install in development mode
poetry install
# Run directly
poetry run task-manager add "Test task"
# Or activate shell and run
poetry shell
task-manager add "Test task"
Building Distribution Packages
# Build wheel and source distribution
poetry build
# Output will be in dist/
ls dist/
# task-manager-0.1.0.tar.gz
# task_manager-0.1.0-py3-none-any.whl
Docker Containerization
Dockerfile
FROM python:3.11-slim
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
&& rm -rf /var/lib/apt/lists/*
# Install Poetry
RUN pip install poetry
# Copy poetry files
COPY pyproject.toml poetry.lock ./
# Configure poetry: Don't create virtual env, install dependencies
RUN poetry config virtualenvs.create false \
&& poetry install --no-dev --no-interaction --no-ansi
# Copy application
COPY task_manager/ ./task_manager/
# Create data directory
RUN mkdir -p /data
# Set data file location
ENV TASK_DATA_FILE=/data/tasks.json
# Create non-root user
RUN adduser --disabled-password --gecos '' taskuser
RUN chown -R taskuser:taskuser /app /data
USER taskuser
# Entry point
ENTRYPOINT ["python", "-m", "task_manager.cli"]
docker-compose.yml
version: '3.8'
services:
task-manager:
build: .
volumes:
- task_data:/data
environment:
- TASK_DATA_FILE=/data/tasks.json
stdin_open: true
tty: true
volumes:
task_data:
Building and Running with Docker
# Build image
docker build -t task-manager .
# Run container
docker run -it --rm \
-v $(pwd)/data:/data \
task-manager add "Dockerized task"
# Using docker-compose
docker-compose run task-manager list
GitHub Actions CI/CD
.github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.10, 3.11]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction
- name: Run code quality checks
run: |
poetry run black --check task_manager/ tests/
poetry run flake8 task_manager/ tests/
poetry run mypy task_manager/
- name: Run tests
run: |
poetry run pytest --cov=task_manager --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install Poetry
uses: snok/install-poetry@v1
- name: Build package
run: poetry build
- name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags/')
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
run: poetry publish
docker:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
yourusername/task-manager:latest
yourusername/task-manager:${{ github.sha }}
Publishing to PyPI
# Configure PyPI credentials
poetry config pypi-token.pypi your-token-here
# Build and publish
poetry build
poetry publish
# Or in one command
poetry publish --build
Creating Releases
# Tag release
git tag -a v0.1.0 -m "Release version 0.1.0"
git push origin v0.1.0
# GitHub will automatically create release via Actions
Best Practices & Tips
Code Organization
Follow PEP 8: Use consistent code style
Type Hints: Add type annotations for better code documentation
Docstrings: Document all public functions and classes
Error Handling: Use specific exceptions and provide helpful error messages
Logging: Add appropriate logging for debugging and monitoring
Common Mistakes to Avoid
- Poor Error Handling
# ❌ Bad: Generic exception handling
try:
result = risky_operation()
except Exception:
pass
# ✅ Good: Specific exception handling
try:
result = storage.load_tasks()
except FileNotFoundError:
logger.info("No existing tasks file found, starting fresh")
return []
except json.JSONDecodeError as e:
logger.error(f"Corrupted tasks file: {e}")
raise TaskStorageError(f"Cannot parse tasks file: {e}")
2. Hardcoded Values
# ❌ Bad: Hardcoded paths
data_file = "/home/user/.tasks.json"
# ✅ Good: Configurable paths
data_file = os.getenv('TASK_DATA_FILE', '~/.task_manager/tasks.json')
data_file = Path(data_file).expanduser()
3. Missing Input Validation
# ❌ Bad: No validation
def add_task(title: str) -> Task:
return Task.create(title)
# ✅ Good: Input validation
def add_task(title: str) -> Task:
if not title or not title.strip():
raise ValueError("Task title cannot be empty")
if len(title) > 200:
raise ValueError("Task title too long (max 200 characters)")
return Task.create(title.strip())
Maintaining Long-term Projects
- Dependency Management
# Regular dependency updates
poetry update
# Check for security vulnerabilities
poetry audit
# Pin major versions in pyproject.toml
click = "^8.1.0" # Allows 8.1.x but not 9.x
2. Code Quality Automation
# Pre-commit hooks
pip install pre-commit
pre-commit install
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 23.9.1
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8
3. Documentation Maintenance
Keep README updated with new features
Update API documentation when interfaces change
Maintain changelog for version history
Include migration guides for breaking changes
4. Testing Strategy
# Test pyramid: Many unit tests, fewer integration tests
# Unit tests: 70-80%
# Integration tests: 15-25%
# End-to-end tests: 5-10%
# Use property-based testing for complex logic
from hypothesis import given, strategies as st
@given(st.text(min_size=1, max_size=100))
def test_task_title_handling(title):
"""Test task creation with various title inputs."""
assume(title.strip()) # Skip empty strings
task = Task.create(title.strip())
assert task.title == title.strip()
5. Performance Considerations
# Profile your code
import cProfile
import pstats
def profile_function():
profiler = cProfile.Profile()
profiler.enable()
# Your code here
task_manager.list_tasks()
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(10)
# Use appropriate data structures
# For frequent lookups: dict over list
# For ordered data: list or OrderedDict
# For unique items: set
Security Best Practices
Input Sanitization: Validate and sanitize all user inputs
File Permissions: Set appropriate permissions on data files
Dependency Scanning: Regularly scan for vulnerable dependencies
Secrets Management: Never commit secrets to version control
# Use environment variables for sensitive data
import os
from pathlib import Path
def get_data_dir() -> Path:
"""Get data directory with secure permissions."""
data_dir = Path(os.getenv('TASK_DATA_DIR', '~/.task_manager')).expanduser()
data_dir.mkdir(mode=0o700, parents=True, exist_ok=True) # Owner only
return data_dir
Monitoring and Observability
# Add structured logging
import structlog
logger = structlog.get_logger()
def add_task(self, title: str) -> Task:
"""Add a new task with structured logging."""
logger.info("Adding new task", title=title, user_id=self.user_id)
try:
task = Task.create(title)
self.tasks.append(task)
self._save_tasks()
logger.info("Task added successfully",
task_id=task.id,
title=task.title,
total_tasks=len(self.tasks))
return task
except Exception as e:
logger.error("Failed to add task",
title=title,
error=str(e),
exc_info=True)
raise
Conclusion
Building a professional Python project involves much more than writing functional code. By following this comprehensive guide, you’ve learned to:
Plan projects effectively with clear goals and scope
Set up robust development environments with proper dependency management
Structure code for maintainability and scalability
Implement comprehensive testing strategies
Create quality documentation that serves both users and developers
Deploy applications using modern containerization and CI/CD practices
Follow best practices that keep projects maintainable over time
The key to successful Python projects lies in treating each phase with equal importance. A well-planned, properly tested, and thoroughly documented project will serve you and your team much better than hastily written code, no matter how clever.
Next Steps
Fork the Example Project: Visit our GitHub repository to see the complete implementation
Build Your Own: Apply these patterns to your next project
Contribute: Help improve this guide by contributing examples and corrections
Share: Pass these practices along to other developers in your community
Sample Project Repository
You can find the complete, working implementation of the Task Manager CLI at: https://github.com/timothykimutai/task-manager
The repository includes:
Complete source code with all features implemented
Comprehensive test suite with >95% coverage
CI/CD pipeline configuration
Docker setup for easy deployment
Complete documentation built with MkDocs
Example workflows and development scripts
Feel free to fork the repository, experiment with the code, and use it as a template for your own projects. Contributions, issues, and suggestions are always welcome!
Subscribe to my newsletter
Read articles from Timothy Kimutai directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Timothy Kimutai
Timothy Kimutai
I simplify AI and tech for developers and entrepreneurs. Freelance Data scientist at Upwork. Join 10K+ readers for actionable insights.