Teqvault.study — Python Series

Phase 3: The Ecosystem

Libraries are half of Python's power. Learn the tools that appear in every real project — virtual environments, HTTP, CLIs, validation, data, and professional project structure.

📦 🌐 🛠️ 📊 🏗️
Phase Progress
0%
LESSON 01

Virtual Environments — Your Project's Bubble

Every Python project needs its own isolated environment. Without this, you'll spend hours debugging dependency conflicts that have nothing to do with your code.

🤔 The Problem — Why Global Installs Break Everything

Imagine installing requests 2.28 globally for Project A. Then Project B needs requests 2.20 because it depends on an older library. You can't have both installed globally at the same time. One project breaks.

This is the dependency conflict problem. It happens constantly in real development. Virtual environments solve it by giving each project its own isolated Python installation and package directory — completely separate from everything else on your system.

⚙️ What a virtual environment actually is
A virtual environment is just a folder (usually called venv/ or .venv/) containing a copy of the Python interpreter, a pip executable, and an empty site-packages/ directory where packages get installed. When you "activate" it, your terminal temporarily redirects python and pip commands to point inside that folder instead of the global Python. Deactivate it and you're back to global. No magic — just path manipulation.
🛠️ Creating and Using a Virtual Environment
Terminal — venv workflow
# Step 1 — Create the virtual environment
# Run this ONCE per project, in the project root
python -m venv .venv
# Creates a .venv/ folder. The dot keeps it hidden in file explorers.

# Step 2 — Activate it
# macOS / Linux:
source .venv/bin/activate

# Windows (Command Prompt):
.venv\Scripts\activate.bat

# Windows (PowerShell):
.venv\Scripts\Activate.ps1

# Your prompt changes to show the active env:
(.venv) PS C:\Users\jayka\projects\my-app>

# Step 3 — Install packages (now go into .venv ONLY)
pip install requests rich typer

# Step 4 — Work on your project normally
python main.py

# Step 5 — Deactivate when done
deactivate
# Prompt returns to normal. Global Python unaffected.
💡
Always add .venv to .gitignoreThe virtual environment folder can be hundreds of MB and is machine-specific. Never commit it. Add .venv/ to your .gitignore. Your requirements.txt or pyproject.toml is what other developers use to recreate the environment.
⚠️
VS Code tipOpen your project folder in VS Code, then press Ctrl+Shift+P → "Python: Select Interpreter" → choose the one inside .venv/. VS Code will then activate the environment automatically in its integrated terminal, and your IntelliSense will see all installed packages.
🧠 Quick Check
Why should you never commit your .venv/ folder to Git?
LESSON 02

pip & Dependency Management

pip is Python's package installer. But knowing pip commands is only half the job — the other half is properly recording and sharing your project's dependencies so anyone can reproduce your environment exactly.

📦 Essential pip Commands
Terminal — pip commands you'll use daily
# Install a package
pip install requests

# Install a specific version
pip install requests==2.31.0

# Install minimum version (common in requirements files)
pip install "requests>=2.28"

# Install multiple packages at once
pip install requests rich typer pydantic

# Upgrade a package to latest
pip install --upgrade requests

# Uninstall
pip uninstall requests

# List everything installed in the current environment
pip list

# See details about one package (version, location, dependencies)
pip show requests

# Search PyPI (the Python Package Index) for packages
pip index versions requests

# Save current environment to requirements.txt
pip freeze > requirements.txt

# Install everything from requirements.txt
# (run this after cloning a project to restore the environment)
pip install -r requirements.txt
📄 requirements.txt vs pyproject.toml

There are two ways to record dependencies. Both are valid — understand the difference so you use the right one.

FormatBest ForCreated By
requirements.txtSimple scripts, quick projects, exact reproducibilitypip freeze > requirements.txt
pyproject.tomlProper packages, libraries, modern projectsManually or with poetry init
requirements.txt
# Exact pinned versions
requests==2.31.0
rich==13.7.0
typer==0.9.0
pydantic==2.5.0

# Recreate with:
# pip install -r requirements.txt
pyproject.toml
[project]
name = "my-app"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "requests>=2.28",
  "rich>=13.0",
  "typer>=0.9",
  "pydantic>=2.0",
]
🔍 pip freeze — what it actually does

Why pinned versions matter

pip freeze dumps every installed package with its exact version number. This creates a "snapshot" of your working environment. When another developer runs pip install -r requirements.txt, they get exactly the same versions you tested with — not whatever the latest version happens to be that day. This prevents the "works on my machine" problem.

The trade-off: pinned versions go stale. Security patches and bug fixes in newer versions won't reach you unless you explicitly update. Make a habit of running pip install --upgrade -r requirements.txt and re-testing periodically.

🔍 Finding Good Packages on PyPI

pypi.org is the Python Package Index — over 500,000 packages. Not all are good. Here's how to evaluate one quickly:

  1. Weekly downloads — millions = trusted. Hundreds = niche but may be fine. Tens = experimental.
  2. Last release date — abandoned for 3+ years is a yellow flag for security. Abandoned for 5+ is a red flag.
  3. GitHub stars & open issues — click the GitHub link. Are issues being responded to?
  4. Dependencies — a package that pulls in 50 sub-dependencies is a liability. Check the dependency count.
  5. License — MIT, Apache 2.0, BSD = use freely. GPL = check if it affects your project's license requirements.
LESSON 03

Requests — HTTP for Humans

requests is the most downloaded Python package in history. It turns HTTP — normally a tedious low-level protocol — into something you can do in one readable line.

🌐 What HTTP Requests Are

Every time your browser loads a page, fetches data, or submits a form, it sends an HTTP request. Your Python code can do the same — call weather APIs, GitHub, OpenAI, your own backend, anything with a URL.

HTTP MethodWhat It DoesExample
GETRetrieve dataFetch a user's profile
POSTSend data, create resourceSubmit a form, create a task
PUTReplace a resourceUpdate entire user record
PATCHPartially updateChange just the user's name
DELETERemove a resourceDelete a task
🚀 Installation & First Request
Terminal
pip install requests
Python — your first API call
import requests

# GET request — fetch data from a public API
response = requests.get("https://api.github.com/users/torvalds")

# The response object contains everything
print(response.status_code)    # 200 = success, 404 = not found, etc.
print(response.headers)        # dict of response headers
print(response.text)           # raw response body as string

# .json() parses JSON response into a Python dict — use this constantly
data = response.json()
print(data["name"])            # "Linus Torvalds"
print(data["public_repos"])   # number of public repos
print(data["followers"])      # follower count
🔧 The Full Requests Toolkit
Python — requests patterns for real projects
import requests

# ── Query parameters — appended to URL as ?key=value ──
params = {"q": "python", "sort": "stars", "per_page": 5}
r = requests.get("https://api.github.com/search/repositories", params=params)
# Requests builds: ?q=python&sort=stars&per_page=5 — no manual string building
repos = r.json()["items"]
for repo in repos:
    print(f{repo['full_name']:40} ⭐ {repo['stargazers_count']:,}")


# ── Headers — authentication, content type ──
headers = {
    "Authorization": "Bearer YOUR_TOKEN_HERE",
    "Accept":        "application/vnd.github+json",
}
r = requests.get("https://api.github.com/user", headers=headers)


# ── POST — send JSON data ──
payload = {"title": "Learn requests", "done": False}
r = requests.post(
    "https://jsonplaceholder.typicode.com/todos",
    json=payload   # automatically serializes dict to JSON + sets Content-Type
)
print(r.status_code)   # 201 Created
print(r.json())        # {'title': 'Learn requests', 'done': False, 'id': 201}


# ── Timeout — always set this in production ──
# Without timeout, a hanging server will hang your program forever
try:
    r = requests.get("https://api.example.com/data", timeout=5)
    # timeout=5 means: 5 seconds to connect AND 5 seconds to read
    r.raise_for_status()  # raises HTTPError if status is 4xx or 5xx
    data = r.json()
except requests.Timeout:
    print("Request timed out")
except requests.HTTPError as e:
    print(f"HTTP error: {e.response.status_code}")
except requests.ConnectionError:
    print("No internet / DNS failure")


# ── Sessions — reuse connections, share headers ──
# More efficient when making many calls to the same API
with requests.Session() as session:
    session.headers.update({"Authorization": "Bearer TOKEN"})
    users  = session.get("https://api.example.com/users").json()
    tasks  = session.get("https://api.example.com/tasks").json()
    # Auth header sent automatically on every request in this session
⚠️
Always check the status codeA request that gets a response isn't necessarily successful. A 404 or 500 still returns a response object — requests won't raise an exception unless you call raise_for_status() or check response.status_code yourself. Never assume success just because you got a response.
🔨 Try It — Call a Real API

Use the free Open-Meteo weather API (no API key needed)

  • URL: https://api.open-meteo.com/v1/forecast
  • Params: latitude=32.52, longitude=-93.75 (Bossier City), current=temperature_2m,wind_speed_10m
  • Parse the response and print the current temperature and wind speed
  • Add proper timeout and error handling
  • Extend it: take latitude/longitude as function arguments so it works for any city
LESSON 04

Rich — Beautiful Terminal Output

Rich turns your terminal output from plain text into something that looks professional — colored syntax, tables, progress bars, panels, and live-updating displays. One import changes everything.

🎨 Installation & First Steps
Terminal
pip install rich
Python — drop-in print replacement
from rich import print   # shadows built-in print — same API

# Markup syntax for colors and styles
print("[bold green]✓ Task completed![/bold green]")
print("[red]✗ Error occurred[/red]")
print("[bold yellow]⚠ Warning:[/bold yellow] check your config")
print("[cyan]https://teqvault.study[/cyan]")

# Rich auto-highlights Python objects
data = {"name": "Jude", "skills": ["python", "unity"], "level": 5}
print(data)   # dict with colored keys, strings, numbers — automatic
📊 Tables, Panels, and the Console Object
Python — Rich components
from rich.console import Console
from rich.table   import Table
from rich.panel   import Panel
from rich.syntax  import Syntax

console = Console()   # main Rich object — use instead of print()

# ── Styled panels ──
console.print(Panel(
    "[bold]Welcome to Teqvault[/bold]\nYour Python learning platform.",
    title="[green]Phase 3[/green]",
    border_style="green"
))

# ── Tables ──
table = Table(title="Task Manager", border_style="bright_black")
table.add_column("#",        style="dim",         width=4)
table.add_column("Title",    style="bold white")
table.add_column("Priority", justify="center")
table.add_column("Status",   justify="center")

table.add_row("1", "Learn Requests",   "[yellow]●[/yellow] High",   "[green]✓ Done[/green]")
table.add_row("2", "Ship Death Road",   "[red]● Critical[/red]", "[yellow]○ Pending[/yellow]")
table.add_row("3", "Read Pragmatic Prog","[blue]● Normal[/blue]",   "[yellow]○ Pending[/yellow]")
console.print(table)

# ── Syntax highlighting for code output ──
code = """
def fibonacci(n):
    if n < 2: return n
    return fibonacci(n-1) + fibonacci(n-2)
"""
console.print(Syntax(code, "python", theme="monokai", line_numbers=True))
⏳ Progress Bars & Spinners
Python — Rich progress tracking
from rich.progress import track, Progress, SpinnerColumn, TimeElapsedColumn
import time

# ── Simple — wrap any iterable with track() ──
for item in track(range(20), description="Processing..."):
    time.sleep(0.05)
# Shows: Processing... ━━━━━━━━━━━━━━━━ 20/20 0:00:01

# ── Advanced — multiple tasks, custom columns ──
with Progress(
    SpinnerColumn(),
    "[progress.description]{task.description}",
    TimeElapsedColumn(),
) as progress:
    task1 = progress.add_task("[green]Fetching data...",  total=100)
    task2 = progress.add_task("[yellow]Processing...",    total=100)

    while not progress.finished:
        progress.update(task1, advance=2)
        progress.update(task2, advance=1)
        time.sleep(0.05)
LESSON 05

Typer — Real CLI Tools in Minutes

Typer turns your Python functions into command-line interfaces automatically — argument parsing, help text, type checking, and tab completion, all from your existing type hints.

⌨️ Why Typer Instead of argparse

Python has a built-in argparse module for CLIs. Typer is better for almost everything because it uses type hints you're already writing — no duplicate declarations, much less code, automatic help text, and it integrates perfectly with Rich for colored output.

Terminal
pip install "typer[all]"   # [all] includes rich + shell completion
🚀 From Function to CLI — Instantly
Python — cli.py
import typer
from rich import print
from typing import Optional

app = typer.Typer(help="Task Manager CLI — manage your tasks from the terminal.")


@app.command()
def add(
    title:    str             = typer.Argument(..., help="Task title"),
    priority: int             = typer.Option(2,  help="Priority 1-5"),
    due:      Optional[str]  = typer.Option(None, help="Due date YYYY-MM-DD"),
):
    """Add a new task."""
    print(f"[green]✓[/green] Added: [bold]{title}[/bold] (priority {priority})")


@app.command()
def list(
    show_done: bool = typer.Option(False, "--done", help="Include completed tasks"),
):
    """List all tasks."""
    print("[bold]Your tasks:[/bold]")
    print("  [yellow]○[/yellow] Ship Death Road prototype")
    print("  [yellow]○[/yellow] Learn Phase 4")
    if show_done:
        print("  [green]✓[/green] Complete Phase 3")


@app.command()
def complete(title: str = typer.Argument(..., help="Task title to mark done")):
    """Mark a task as complete."""
    print(f"[green]✓ Completed:[/green] {title}")


if __name__ == "__main__":
    app()
python cli.py --help
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
Task Manager CLI — manage your tasks from the terminal.
╭─ Commands ───────────────────────────────────────╮ │ add Add a new task. │ │ list List all tasks. │ │ complete Mark a task as complete. │ ╰──────────────────────────────────────────────────╯
python cli.py add "Ship Death Road" --priority 5 --due 2026-07-01 ✓ Added: Ship Death Road (priority 5)
python cli.py list --done Your tasks: ○ Ship Death Road prototype ✓ Complete Phase 3
🔍 Argument vs Option

Two kinds of CLI inputs

Arguments are positional — the user types them without a flag name: python cli.py add "My Task". Use typer.Argument(). Required by default.

Options are named with --flag: python cli.py add "My Task" --priority 3. Use typer.Option(). Optional by default, with a default value. Boolean options become --flag / --no-flag pairs automatically.

LESSON 06

Pydantic — Data Validation That Actually Works

Pydantic validates data at the boundary of your application — when data comes in from APIs, files, or users — and gives you clear errors when it's wrong. It's the backbone of FastAPI and essential for any serious Python project.

✅ The Problem Pydantic Solves

Data coming from outside your program (API responses, JSON files, form submissions) is untrustworthy. Fields might be missing, wrong types, or invalid values. Without validation, you get cryptic KeyError or TypeError crashes deep in your code — far from where the bad data entered. Pydantic catches it at the door.

Terminal
pip install pydantic   # v2 — current version
Python — Pydantic BaseModel
from pydantic import BaseModel, Field, EmailStr, validator
from typing   import Optional
from datetime import date

class Task(BaseModel):
    title:    str           = Field(..., min_length=1, max_length=200)
    priority: int           = Field(default=2, ge=1, le=5)  # ge=≥, le=≤
    done:     bool          = False
    due:      Optional[date] = None
    tags:     list[str]     = []

# ── Valid data — works perfectly ──
task = Task(title="Learn Pydantic", priority=4, tags=["python", "learning"])
print(task)
# title='Learn Pydantic' priority=4 done=False due=None tags=['python', 'learning']

# ── Pydantic coerces compatible types automatically ──
task2 = Task(title="Another task", priority="3")  # "3" → 3, auto-coerced

# ── Invalid data — immediate, clear error ──
try:
    bad = Task(title="", priority=10)   # empty title, priority > 5
except Exception as e:
    print(e)
# 2 validation errors for Task
# title: String should have at least 1 character [min_length=1]
# priority: Input should be less than or equal to 5 [le=5]
🌐 Parsing API Responses with Pydantic

The most common real-world use: you get a JSON blob from an API and want structured, validated, typed Python objects instead of raw dicts.

Python — API response → Pydantic model
import requests
from pydantic import BaseModel, Field
from typing   import Optional

# Define the shape of what the API returns
class GitHubUser(BaseModel):
    login:        str
    name:         Optional[str]   = None
    public_repos: int
    followers:    int
    bio:          Optional[str]   = None
    html_url:     str

    # model_config lets you configure behavior
    model_config = {"extra": "ignore"}   # ignore fields not in the model

def get_github_user(username: str) -> GitHubUser:
    r = requests.get(
        f"https://api.github.com/users/{username}",
        timeout=5
    )
    r.raise_for_status()
    # model_validate parses the dict and validates every field
    return GitHubUser.model_validate(r.json())

user = get_github_user("torvalds")
print(f"{user.name} — {user.public_repos} repos — {user.followers:,} followers")
# Linus Torvalds — 7 repos — 236,174 followers
# user is now a proper GitHubUser object, not a raw dict
# user.login, user.followers, etc. all have the right types
💡
Pydantic is FastAPI's foundationWhen you reach Phase 5 (APIs & Backend), everything you define in FastAPI uses Pydantic models — request bodies, response schemas, query parameters. Learning it now means Phase 5 is just connecting the pieces you already know.
🧠 Quick Check
You define a Pydantic model with priority: int = Field(ge=1, le=5) and pass priority="3" (a string). What happens?
LESSON 07

Pandas — Working With Tabular Data

You don't need to become a data scientist. But every real project eventually touches CSV files, spreadsheets, or structured data. Pandas makes that work feel like nothing.

📊 The Two Core Objects
Terminal
pip install pandas
Python — Series and DataFrame
import pandas as pd

# Series — a single column with an index
scores = pd.Series([85, 92, 78, 96], index=["Jude", "Ana", "Max", "Zoe"])
print(scores["Jude"])     # 85
print(scores.mean())     # 87.75
print(scores[scores > 85]) # Ana:92, Zoe:96 — boolean indexing

# DataFrame — a table: multiple columns, each is a Series
df = pd.DataFrame({
    "name":     ["Jude", "Ana", "Max", "Zoe"],
    "score":    [85, 92, 78, 96],
    "level":    [5, 7, 3, 9],
    "passing":  [True, True, False, True],
})
print(df)
print(df.dtypes)       # column types
print(df.describe())   # count, mean, std, min, max for numeric columns
🔧 The Operations You'll Use 90% of the Time
Python — Pandas essentials
import pandas as pd

# ── Reading files — the most common entry point ──
df = pd.read_csv("tasks.csv")          # CSV
df = pd.read_excel("data.xlsx")        # Excel
df = pd.read_json("data.json")         # JSON

# ── Quick inspection — always do this first ──
print(df.head())              # first 5 rows
print(df.tail(3))            # last 3 rows
print(df.shape)              # (rows, columns)
print(df.columns.tolist())  # list of column names
print(df.isnull().sum())    # count missing values per column

# ── Selecting data ──
df["score"]                  # single column → Series
df[["name", "score"]]       # multiple columns → DataFrame
df.iloc[0]                  # first row by integer index
df.loc[df["name"] == "Jude"] # rows where name is "Jude"

# ── Filtering — boolean indexing ──
passing = df[df["score"] >= 80]
high_level = df[(df["level"] > 5) & (df["passing"] == True)]

# ── Sorting ──
df.sort_values("score", ascending=False)       # by score descending
df.sort_values(["level", "score"])              # multi-column sort

# ── Aggregation ──
print(df["score"].mean())    # average score
print(df["score"].max())     # highest score
print(df.groupby("passing")["score"].mean())  # mean score per group

# ── Adding/modifying columns ──
df["grade"] = df["score"].apply(lambda s: "A" if s >= 90 else "B")
df["bonus_score"] = df["score"] * 1.1   # vectorized — no loop needed

# ── Saving results ──
df.to_csv("results.csv", index=False)     # index=False avoids extra column
df.to_json("results.json", orient="records")
df.to_excel("results.xlsx", index=False)
📌
For Phase 3 you only need this muchPandas goes very deep — entire careers are built on it. For a developer (not a data scientist), what you've seen here covers 90% of what you'll actually encounter: read a file, inspect it, filter it, aggregate it, write it back. Don't go deeper until you hit a problem that requires it.
LESSON 08

Pathlib — The Modern File System

Python's old os.path module works but it's clunky. pathlib — built into Python since 3.4 — treats file paths as objects with methods. It's cleaner, cross-platform, and the right way to work with files in 2026.

📁 Path as an Object
Python — pathlib basics
from pathlib import Path

# Create path objects — note the / operator for joining!
home    = Path.home()                         # /Users/jayka  or  C:/Users/jayka
project = Path(".")                            # current directory
data    = project / "data" / "tasks.json"    # join with /
# No os.path.join() needed. The / works on all platforms.

# ── Inspect paths ──
p = Path("/home/jayka/projects/app/main.py")
print(p.name)       # "main.py"       — filename with extension
print(p.stem)       # "main"          — filename without extension
print(p.suffix)     # ".py"           — extension
print(p.parent)     # /home/jayka/projects/app
print(p.parts)      # ('/', 'home', 'jayka', 'projects', 'app', 'main.py')

# ── Check existence ──
print(p.exists())    # True / False
print(p.is_file())   # True if it's a file
print(p.is_dir())    # True if it's a directory

# ── Read and write — no open() needed for simple cases ──
data_file = Path("tasks.json")
content   = data_file.read_text(encoding="utf-8")   # read whole file as string
data_file.write_text('{"tasks": []}', encoding="utf-8") # write string to file

# ── Create directories ──
output = Path("output/reports/2026")
output.mkdir(parents=True, exist_ok=True)
# parents=True creates all intermediate dirs
# exist_ok=True won't error if it already exists

# ── Find files — glob ──
project = Path(".")

# All Python files in current directory (not recursive)
py_files = list(project.glob("*.py"))

# All Python files recursively (** = any depth)
all_py = list(project.glob("**/*.py"))

# All JSON files in data/ subdirectory
json_files = list((project / "data").glob("*.json"))

# Iterate a directory
for item in project.iterdir():
    if item.is_file():
        print(f"{item.name:30} {item.stat().st_size:>8} bytes")
🔧 Common Pathlib Patterns
Python — practical pathlib recipes
from pathlib import Path
import shutil, json

# ── Config file in user's home directory ──
config_path = Path.home() / ".myapp" / "config.json"
config_path.parent.mkdir(parents=True, exist_ok=True)

if not config_path.exists():
    config_path.write_text(json.dumps({"theme": "dark"}, indent=2))

config = json.loads(config_path.read_text())

# ── Rename / move a file ──
old_path = Path("report.txt")
new_path = old_path.with_name("final_report.txt")  # same dir, new name
new_ext  = old_path.with_suffix(".md")              # same name, new extension

# ── Copy file ──
shutil.copy2(Path("source.txt"), Path("backup.txt"))

# ── Delete ──
Path("temp.txt").unlink(missing_ok=True)  # delete file, no error if missing
shutil.rmtree(Path("old_folder"))          # delete entire directory tree

# ── Get file stats ──
p = Path("tasks.json")
stat = p.stat()
print(f"Size: {stat.st_size} bytes")
print(f"Modified: {stat.st_mtime}")
LESSON 09

Professional Project Structure

How you organize your project is architecture. A bad structure creates friction every day. A good one makes the codebase feel obvious to anyone who opens it — including future you.

🏗️ The Standard Python Project Layout
my-project/ ├── .env # secret keys and env vars — NEVER commit ├── .env.example # template showing which env vars are needed ├── .gitignore # excludes .venv/, .env, __pycache__/, etc. ├── README.md # what this is, how to set it up, how to run it ├── pyproject.toml # project metadata and dependencies ├── requirements.txt # pinned deps for quick setup (or use pyproject.toml) │ ├── src/ # or just the package name directly │ └── myapp/ │ ├── __init__.py # makes this a package; often empty │ ├── main.py # entry point / CLI │ ├── config.py # settings, env var loading │ ├── models.py # Pydantic models / dataclasses │ ├── services.py # business logic │ ├── api.py # external API calls │ └── utils.py # shared helpers │ ├── tests/ │ ├── __init__.py │ ├── test_models.py │ └── test_services.py │ └── .venv/ # virtual environment — in .gitignore
⚙️ Configuration — Loading .env Files

Never hardcode API keys, database passwords, or secrets in your code. Load them from environment variables at runtime using python-dotenv.

Terminal
pip install python-dotenv
.env (never commit)
# Real secrets go here
OPENAI_API_KEY=sk-proj-abc123
GITHUB_TOKEN=ghp_xyz789
DATABASE_URL=postgresql://user:pass@localhost/db
DEBUG=true
APP_PORT=8000
.env.example (commit this)
# Copy to .env and fill in values
OPENAI_API_KEY=
GITHUB_TOKEN=
DATABASE_URL=
DEBUG=false
APP_PORT=8000
Python — config.py
from dotenv  import load_dotenv
from pydantic import BaseSettings   # pydantic-settings package
import os

load_dotenv()   # loads .env into os.environ

# Simple approach — os.environ
API_KEY = os.getenv("OPENAI_API_KEY")
if not API_KEY:
    raise RuntimeError("OPENAI_API_KEY is not set — check your .env file")

# Better approach — Pydantic Settings (validates env vars)
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    openai_api_key: str
    github_token:   str
    debug:          bool  = False
    app_port:       int   = 8000

    class Config:
        env_file = ".env"   # auto-loads from .env

settings = Settings()     # raises error if required vars missing
print(settings.app_port)  # 8000 (from .env or default)
📌 The Golden .gitignore for Python
.gitignore
# Virtual environment
.venv/
venv/
env/

# Secrets
.env

# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
.Python

# Testing
.pytest_cache/
.coverage
htmlcov/

# Editor
.vscode/
.idea/
*.swp

# Distribution
dist/
build/
*.egg-info/
PHASE PROJECT 🏁

Command Center v3 — Live API Integration

Extend the Phase 2 task manager into a real CLI tool that pulls live data from external APIs, uses Rich for beautiful output, and follows professional project structure.

🎯 What to Build
📋
Requirements
Virtual environment — project runs in its own .venv/
pyproject.toml or requirements.txt — all deps recorded
Typer CLI — commands: add, list, complete, weather, github
Requestsweather command fetches real weather for your city
Pydantic — validate API responses and task input
Rich — all output uses tables, panels, and color
Pathlib — tasks saved to ~/.commandcenter/tasks.json
.env — GitHub token loaded from environment (for github command)
Project structure — proper folders, .gitignore, README.md
💡 Reference Architecture
command-center/ ├── .env ├── .env.example ├── .gitignore ├── README.md ├── requirements.txt # requests rich typer pydantic python-dotenv └── commandcenter/ ├── __init__.py ├── main.py # Typer app, all commands ├── models.py # Task, WeatherData, GitHubUser (Pydantic) ├── storage.py # load/save tasks via pathlib ├── weather.py # fetch_weather() using requests ├── github.py # get_user(), list_repos() using requests └── display.py # Rich tables, panels, formatting helpers
Python — main.py skeleton
import typer
from rich.console import Console
from commandcenter import storage, weather, github, display
from commandcenter.models import Task

app     = typer.Typer(help="Command Center — your personal productivity CLI.")
console = Console()


@app.command()
def add(title: str, priority: int = 2, due: str = None):
    """Add a new task."""
    tasks = storage.load()
    task  = Task(title=title, _priority=priority, due=due)
    tasks.append(task)
    storage.save(tasks)
    console.print(display.task_added(task))


@app.command(name="list")
def list_tasks(done: bool = False):
    """List all tasks."""
    tasks = storage.load()
    console.print(display.task_table(tasks, show_done=done))


@app.command()
def weather_cmd(city: str = "Bossier City"):
    """Show current weather for a city."""
    data = weather.fetch(city)
    console.print(display.weather_panel(data))


@app.command()
def gh(username: str):
    """Show a GitHub user's profile."""
    user = github.get_user(username)
    console.print(display.github_panel(user))


if __name__ == "__main__":
    app()
🎯 Extend the Challenge

After the baseline works, push further:

  • Add a stats command that shows a Rich table: total tasks, pending, overdue, completion rate
  • Add a export command that saves tasks to CSV using pandas
  • Add a --json flag to the list command that outputs raw JSON (useful for piping to other tools)
  • Wrap all API calls in the @retry decorator from Phase 2
  • Add a search command that filters tasks by keyword and uses Rich to highlight matches
🚀
Phase 3 Complete. You now have a working CLI tool that calls real APIs, validates data with Pydantic, displays beautiful output with Rich, reads and writes files with Pathlib, and follows professional project structure. Phase 4 is Files, Data & Automation — where you'll build things that run on a schedule and process the real world automatically.
Roadmap