Python Full Tutorial -- Part 12: Virtual Environments & Real Project Setup

By Suraj Ahir 2026-01-14 11 min read

← Part 11Python Tutorial · Part 12 of 12
Python Full Tutorial -- Part 12: Virtual Environments & Real Project Setup

This final part combines everything from the series into a professional Python project. We set up the project properly, structure the code, write tests, add logging, and build a real server monitoring automation tool. This is how Python projects look in production teams -- not toy examples, but maintainable, testable, deployable code.

Project Setup from Scratch

Initialise a professional project
mkdir server-monitor && cd server-monitor

python3 -m venv venv
source venv/bin/activate

mkdir -p src tests
touch src/__init__.py
touch src/monitor.py
touch src/alerts.py
touch tests/__init__.py
touch tests/test_monitor.py
touch .env.example .gitignore README.md

pip install psutil requests python-dotenv pytest
pip freeze > requirements.txt

The Application Code

src/monitor.py
import psutil
import logging
from dataclasses import dataclass
from datetime import datetime

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

@dataclass
class Metrics:
    cpu: float
    memory: float
    disk: float
    timestamp: datetime
    
    def is_critical(self, cpu_thresh=90, mem_thresh=85):
        return self.cpu > cpu_thresh or self.memory > mem_thresh

def collect():
    return Metrics(
        cpu=psutil.cpu_percent(interval=1),
        memory=psutil.virtual_memory().percent,
        disk=psutil.disk_usage("/").percent,
        timestamp=datetime.now()
    )

def check_and_report():
    m = collect()
    logger.info(f"CPU: {m.cpu}% | Memory: {m.memory}% | Disk: {m.disk}%")
    if m.is_critical():
        logger.warning("ALERT: Critical resource usage!")
        return False
    return True

Tests with pytest

tests/test_monitor.py
import pytest
from datetime import datetime
from src.monitor import Metrics

def test_not_critical():
    m = Metrics(50.0, 60.0, 40.0, datetime.now())
    assert not m.is_critical()

def test_critical_cpu():
    m = Metrics(95.0, 50.0, 50.0, datetime.now())
    assert m.is_critical()

def test_custom_threshold():
    m = Metrics(75.0, 70.0, 65.0, datetime.now())
    assert m.is_critical(cpu_thresh=70)   # 75 > 70
    assert not m.is_critical(cpu_thresh=80)
Run tests
pytest tests/ -v
# tests/test_monitor.py::test_not_critical PASSED
# tests/test_monitor.py::test_critical_cpu PASSED
# tests/test_monitor.py::test_custom_threshold PASSED

pytest tests/ -v --tb=short  # Shorter error output
pytest tests/ -x             # Stop on first failure

Environment Configuration

.env and .env.example
# .env.example (commit this -- template for others)
CPU_THRESHOLD=90
MEMORY_THRESHOLD=85
ALERT_WEBHOOK=https://hooks.slack.com/services/YOUR/HOOK

# .gitignore (never commit .env)
.env
venv/
__pycache__/
*.pyc
.pytest_cache/
Using env vars in code
from dotenv import load_dotenv
import os

load_dotenv()  # Load from .env file

CPU_THRESH = int(os.getenv("CPU_THRESHOLD", "90"))
MEM_THRESH = int(os.getenv("MEMORY_THRESHOLD", "85"))
WEBHOOK    = os.getenv("ALERT_WEBHOOK")  # None if not set

Standard .gitignore for Python

.gitignore
venv/
.env
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/
.DS_Store

Frequently Asked Questions

Why use virtual environments?

Isolates each project's dependencies. Prevents version conflicts between projects. Always create a venv per project.

What is the standard Python project structure?

src/ or package-name/ for code, tests/ for tests, requirements.txt for deps, .env.example for config, README.md, .gitignore. Keep it simple until you need more.

How do I write tests with pytest?

Create test_*.py files in tests/. Write def test_*() functions with assert statements. Run with pytest. Use -v for verbose, -x to stop on first failure.

How should I handle secrets and config?

Use environment variables loaded from a .env file with python-dotenv. Never commit .env to Git. Commit .env.example as a template. Use os.getenv() with default values.

How do I share my Python project?

pip freeze > requirements.txt. Git commit everything except .env and venv/. Add README.md with setup steps. Others: git clone, python -m venv venv, pip install -r requirements.txt, then run.

You have completed the full Python series. From first print() to a tested, configurable production tool. The DevOps Roadmap shows how Python fits into a complete cloud engineering career. The Docker series shows how to containerise and deploy everything you build.

Key takeaways

Continue reading
Back to — Programming Track
Pick your next language or specialization.
Suraj Ahir — author of SRJahir Tech

Written by

Suraj Ahir

Cloud & DevOps engineer running four live production services on my own AWS infrastructure. I write everything on this site myself — no ghostwriters, no AI filler.

← Part 11Python Tutorial · Part 12 of 12
← Back to Blog
Disclaimer: Educational content only. No guarantees of outcome.

Complete CI/CD for Python Projects

.github/workflows/python-ci.yml
name: Python CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python 3.11
        uses: actions/setup-python@v4
        with:
          python-version: "3.11"
          cache: "pip"
      
      - name: Install dependencies
        run: pip install -r requirements.txt -r requirements-dev.txt
      
      - name: Lint with flake8
        run: flake8 src/ tests/
      
      - name: Type check with mypy
        run: mypy src/
      
      - name: Security scan
        run: bandit -r src/ -ll    # -ll only medium+ severity
      
      - name: Run tests with coverage
        run: pytest tests/ -v --cov=src --cov-report=xml --cov-fail-under=80
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

  deploy:
    needs: quality
    runs-on: ubuntu-latest
    if: github.ref == "refs/heads/main"
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to server
        run: |
          ssh -i ${{ secrets.SSH_KEY }} ubuntu@${{ secrets.SERVER_IP }} "
            cd /opt/server-monitor && 
            git pull && 
            pip install -r requirements.txt &&
            systemctl restart server-monitor
          "

Type Hints for Professional Python

Type-safe Python with mypy
from typing import Optional, List, Dict, Union, Callable
from dataclasses import dataclass

@dataclass
class MetricPoint:
    timestamp: float
    value: float
    labels: Dict[str, str]

def collect_metrics(
    sources: List[str],
    callback: Optional[Callable[[MetricPoint], None]] = None,
    timeout: float = 30.0
) -> Dict[str, List[MetricPoint]]:
    results: Dict[str, List[MetricPoint]] = {}
    for source in sources:
        # ... fetch metrics ...
        pass
    return results

# Run mypy: mypy src/ --strict
# Catches type errors before runtime

Packaging Python Projects for Distribution

pyproject.toml for installable packages
# pyproject.toml
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "server-monitor"
version = "1.0.0"
description = "Monitor server health metrics"
authors = [{name = "Suraj Ahir", email = "suraj@srjahir.in"}]
license = {text = "MIT"}
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "psutil>=5.9",
    "requests>=2.31",
    "python-dotenv>=1.0",
]

[project.scripts]
server-monitor = "server_monitor.cli:main"  # Creates CLI command

[project.optional-dependencies]
dev = ["pytest", "black", "mypy", "bandit"]
Build and publish to PyPI
pip install build twine

python -m build               # Creates dist/*.whl and dist/*.tar.gz
twine check dist/*            # Validate before publishing
twine upload dist/*           # Upload to PyPI

# Or publish to TestPyPI first
twine upload --repository testpypi dist/*
pip install -i https://test.pypi.org/simple/ server-monitor