Python Programming

Testing, Packages and a Real Project

45 min

Testing with pytest

pip install pytest

shop/pricing.py:

def apply_discount(price, percent):
    if not 0 <= percent <= 100:
        raise ValueError("percent must be 0-100")
    return round(price * (100 - percent) / 100, 2)

tests/test_pricing.py:

import pytest
from shop.pricing import apply_discount

@pytest.mark.parametrize("price, pct, expected", [(100, 10, 90.0), (19.99, 0, 19.99), (50, 100, 0.0)])
def test_apply_discount(price, pct, expected):
    assert apply_discount(price, pct) == expected

def test_rejects_bad_percent():
    with pytest.raises(ValueError):
        apply_discount(10, 150)

Run pytest -q. The Software Testing & QA track goes much deeper.

Using third-party packages

pip install requests rich
import requests
resp = requests.get("https://api.github.com/repos/python/cpython", timeout=10)
resp.raise_for_status()
print(resp.json()["stargazers_count"])

Before adding a dependency, check it is maintained and widely used. Pin versions in requirements.txt, and audit them: pip install pip-audit && pip-audit.

pyproject.toml

Modern projects describe themselves in pyproject.toml:

[project]
name = "expense-tracker"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["rich>=13"]

[project.scripts]
expenses = "expenses.cli:main"

pip install -e . installs your project in editable mode and creates the expenses command.

Capstone: an expense tracker CLI

Requirements:

  1. expenses add 12.50 food "lunch" stores an expense in ~/.expenses.json.
  2. expenses list --month 2026-10 prints a table (use rich).
  3. expenses summary shows totals per category and the month's total.
  4. Invalid input gives a friendly message, never a traceback.
  5. Data is written atomically: write to a temp file, then Path.replace() it, so a crash never corrupts your data.
  6. Tests cover parsing, totals and the storage layer (use tmp_path).

Skeleton:

import argparse, json
from dataclasses import dataclass, asdict
from datetime import date
from pathlib import Path

DB = Path.home() / ".expenses.json"

@dataclass
class Expense:
    amount: float
    category: str
    note: str
    day: str

def load():
    return [Expense(**e) for e in json.loads(DB.read_text())] if DB.exists() else []

def save(expenses):
    tmp = DB.with_suffix(".tmp")
    tmp.write_text(json.dumps([asdict(e) for e in expenses], indent=2))
    tmp.replace(DB)

def main():
    p = argparse.ArgumentParser(prog="expenses")
    sub = p.add_subparsers(dest="cmd", required=True)
    a = sub.add_parser("add"); a.add_argument("amount", type=float)
    a.add_argument("category"); a.add_argument("note", nargs="?", default="")
    sub.add_parser("summary")
    args = p.parse_args()
    expenses = load()
    if args.cmd == "add":
        expenses.append(Expense(args.amount, args.category, args.note, date.today().isoformat()))
        save(expenses)
    elif args.cmd == "summary":
        totals = {}
        for e in expenses:
            totals[e.category] = totals.get(e.category, 0) + e.amount
        for cat, amt in sorted(totals.items()):
            print(f"{cat:<12}{amt:>10.2f}")

Where to go next

  • Flask Full-Stack to build web apps.
  • Automation & Scripting for scheduled jobs and APIs.
  • Software Testing & QA for professional test suites.

Check yourself

  • What does pip install -e . do?
  • Why write to a temp file before replacing the real one?
  • How would you test save() without touching your real home folder?

I tuoi progressi sono salvati solo in questo browser.

Invia per la revisione (10 punti)