"""simple_greeter.py - A friendly CLI greeter."""
import argparse
def main():
# Create the parser
parser = argparse.ArgumentParser(
description="A simple greeting tool",
epilog="Thanks for using the greeter!"
)
# Add positional argument (required)
parser.add_argument(
"name",
help="The name of the person to greet"
)
# Add optional flag
parser.add_argument(
"--loud",
action="store_true",
help="Greet in ALL CAPS"
)
# Parse the arguments
args = parser.parse_args()
# Use the arguments
greeting = f"Hello, {args.name}!"
if args.loud:
greeting = greeting.upper()
print(greeting)
if __name__ == "__main__":
main()IS 4010: Application Development with Artificial Intelligence
Week 08: Building professional Python applications
Brandon M. Greenwell
Building professional Python applications
From scripts to maintainable tools
- Application development adds a clear interface, structure, tests, and documentation to a script
- Command-line interfaces (CLIs): A composable interface for automation and developer tools
- Package structure: Organizing code for reusability, testing, and distribution
- Final project connection: Every project needs a usable CLI, tests, documentation, and a repeatable environment
Part 1: command-line interfaces
Why CLIs matter: automation
The problem: manual, repetitive tasks
- Scenario: You need to fetch data from an API multiple times per day
- Manual approach: Open browser, navigate to site, copy data, paste into file
- Repeated work: Manual steps take time and can be difficult to reproduce
- Error-prone: Manual copy-paste leads to mistakes and inconsistencies
- Solution: A CLI tool that makes the steps repeatable with one command
CLI tools in the wild
- Version control:
git-git add,git commit,git push - Python project management:
uv-uv sync,uv add,uv run - Web frameworks:
django-admin-startproject,migrate,runserver - AWS CLI:
aws- Manage cloud infrastructure from the terminal - Data science:
jupyter-jupyter notebook,jupyter lab - Your final project: Provide the working CLI required by the rubric
Introducing argparse: Python’s built-in CLI library
argparseis Python’s standard library for creating command-line interfaces- Built-in: No installation required, part of Python’s standard library
- Capabilities: Handles arguments, flags, subcommands, help text, and validation
- Automatic help: Generates
--helpoutput from the parser definition - Established tool: Used by many Python command-line applications
- Alternatives: Click and Typer offer different APIs when a project needs them
CLI design patterns: arguments and flags
Three types of CLI inputs
- Positional arguments: Required, order matters -
git commit message.txt - Optional arguments (flags): Named, order flexible -
git commit --amend -m "Fix bug" - Subcommands: Different actions in one tool -
git addvsgit commit
Design principles:
- Intuitive: Users should guess the correct syntax
- Consistent: Follow conventions from popular tools
- Helpful: Provide clear error messages and help text
- Forgiving: Accept common variations and abbreviations
Basic argparse: your first CLI
Running the CLI: usage examples
Getting help:
$ uv run python simple_greeter.py --help
usage: simple_greeter.py [-h] [--loud] name
A simple greeting tool
positional arguments:
name The name of the person to greet
options:
-h, --help show this help message and exit
--loud Greet in ALL CAPS
Thanks for using the greeter!Using the tool:
$ uv run python simple_greeter.py Alice
Hello, Alice!
$ uv run python simple_greeter.py Bob --loud
HELLO, BOB!
$ uv run python simple_greeter.py
usage: simple_greeter.py [-h] [--loud] name
simple_greeter.py: error: the following arguments are required: nameYour turn: practice
Create a CLI calculator that: 1. Takes two numbers as positional arguments 2. Has a --operation flag with choices: add, subtract, multiply, divide 3. Prints the result
Example usage:
uv run python calculator.py 10 5 --operation add # Should print 15
uv run python calculator.py 10 5 --operation multiply # Should print 50
# Your code here
def create_calculator_parser():
"""Create a parser for a calculator CLI."""
parser = argparse.ArgumentParser(description="Simple calculator")
# TODO: Add positional arguments for two numbers
# TODO: Add --operation flag with choices
return parser
# Test your calculator
# parser = create_calculator_parser()
# args = parser.parse_args(["10", "5", "--operation", "add"])
# print(f"Result: {args.num1 + args.num2}") # Adjust based on your implementationAdvanced argparse: multiple arguments and types
"""api_fetcher.py - Fetch data from APIs with configurable options."""
import argparse
def fetch_data(api_name: str, resource: str, limit: int, output_file: str = None):
"""Fetch data from an API (implementation details omitted)."""
print(f"Fetching {limit} {resource} from {api_name}...")
if output_file:
print(f"Saving to {output_file}")
# Actual API call would go here
return {"status": "success", "count": limit}
def main():
parser = argparse.ArgumentParser(
description="Fetch data from various APIs"
)
# Positional arguments
parser.add_argument("api", help="API name (e.g., 'pokemon', 'jokes', 'weather')")
parser.add_argument("resource", help="Resource to fetch (e.g., 'pikachu', 'random')")
# Optional arguments with types
parser.add_argument(
"-l", "--limit",
type=int,
default=10,
help="Number of items to fetch (default: 10)"
)
parser.add_argument(
"-o", "--output",
help="Output file path (if not specified, prints to console)"
)
parser.add_argument(
"--format",
choices=["json", "csv", "txt"],
default="json",
help="Output format (default: json)"
)
args = parser.parse_args()
# Call the function with parsed arguments
result = fetch_data(args.api, args.resource, args.limit, args.output)
print(f"Result: {result}")
if __name__ == "__main__":
main()Your turn: practice
Create a file processor CLI that: 1. Takes a filename as positional argument 2. Has --lines flag (integer) for number of lines to process (default: 10) 3. Has --mode flag with choices: read, count, search 4. Has --pattern flag (string) for search mode
Print out what the tool would do based on the arguments.
# Your code here
def create_file_processor_parser():
"""Create a parser for a file processing tool."""
# TODO: Implement the parser
pass
# Test your parser
# parser = create_file_processor_parser()
# args = parser.parse_args(["data.txt", "--lines", "20", "--mode", "search", "--pattern", "error"])
# print(f"Processing {args.filename}: {args.mode} mode, {args.lines} lines")Subcommands: building multi-function tools
"""project_manager.py - Manage your project with subcommands."""
import argparse
def cmd_init(args):
"""Initialize a new project."""
print(f"Initializing project: {args.name}")
print(f"Template: {args.template}")
def cmd_build(args):
"""Build the project."""
print(f"Building project...")
if args.verbose:
print("Verbose output enabled")
def cmd_deploy(args):
"""Deploy the project."""
print(f"Deploying to {args.environment}")
def main():
parser = argparse.ArgumentParser(description="Project management tool")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# 'init' subcommand
init_parser = subparsers.add_parser("init", help="Initialize a new project")
init_parser.add_argument("name", help="Project name")
init_parser.add_argument("--template", default="basic", help="Project template")
init_parser.set_defaults(func=cmd_init)
# 'build' subcommand
build_parser = subparsers.add_parser("build", help="Build the project")
build_parser.add_argument("-v", "--verbose", action="store_true")
build_parser.set_defaults(func=cmd_build)
# 'deploy' subcommand
deploy_parser = subparsers.add_parser("deploy", help="Deploy the project")
deploy_parser.add_argument("environment", choices=["dev", "staging", "prod"])
deploy_parser.set_defaults(func=cmd_deploy)
args = parser.parse_args()
# Call the appropriate function
if hasattr(args, "func"):
args.func(args)
else:
parser.print_help()
if __name__ == "__main__":
main()Your turn: practice
Create a data management CLI with three subcommands: 1. fetch - Fetch data from an API (takes source as argument) 2. clean - Clean a dataset (takes filename and optional --method) 3. export - Export data (takes filename and --format with choices: json, csv, excel)
Each subcommand should print what action it would take.
# Your code here
def create_data_manager_parser():
"""Create a parser for a data management tool with subcommands."""
# TODO: Implement parser with subcommands
pass
# Test your data manager
# parser = create_data_manager_parser()
# args = parser.parse_args(["export", "data.json", "--format", "csv"])
# if hasattr(args, "func"):
# args.func(args)CLI usability practices
- Clear error messages: “Error: API key not found. Set POKEAPI_KEY environment variable.”
- Progress indicators: Show activity for long-running operations
- Colorized output: Use libraries like
richorcoloramafor visual hierarchy - Input validation: Fail fast with helpful messages, not cryptic stack traces
- Exit codes: Return 0 for success, non-zero for failures (enables shell scripting)
- Confirmation prompts: Ask before destructive operations
- Verbose mode:
--verboseor-vflag for debugging - Version flag:
--versionshows tool version
Practical example: API CLI
"""Command-line interface for an API project."""
import argparse
import sys
from typing import Optional
def fetch_resource(resource_type: str, resource_id: Optional[str], limit: int):
"""Fetch data from your API."""
# Your API integration code here
print(f"Fetching {resource_type}...")
if resource_id:
print(f"ID: {resource_id}")
print(f"Limit: {limit}")
# Return the data
def main():
parser = argparse.ArgumentParser(
prog="my_api_tool",
description="Interact with [Your API Name] from the command line",
epilog="Example: uv run python my_api_tool.py pokemon pikachu --format json"
)
parser.add_argument("resource", help="Resource type to fetch")
parser.add_argument("id", nargs="?", help="Optional resource ID")
parser.add_argument("-l", "--limit", type=int, default=10, help="Number of items")
parser.add_argument("--format", choices=["json", "pretty"], default="pretty")
parser.add_argument("-o", "--output", help="Save to file instead of printing")
try:
args = parser.parse_args()
result = fetch_resource(args.resource, args.id, args.limit)
print("Success!")
return 0 # Success exit code
except (OSError, ValueError) as error:
print(f"Error: {error}", file=sys.stderr)
return 1 # Error exit code
if __name__ == "__main__":
sys.exit(main())AI-assisted CLI development
Prompt strategies for building CLIs:
- Starter prompt: “Create a Python argparse CLI for fetching data from the [API Name] API with options for resource type, limit, and output format”
- Enhancement prompt: “Add subcommands for ‘fetch’, ‘list’, and ‘search’ operations”
- Refinement prompt: “Improve error handling and add input validation for API parameters”
- Testing prompt: “Generate test cases for this CLI including error scenarios”
CLI tasks an assistant can draft:
- Generating boilerplate argparse setup
- Drafting help text from the specified arguments
- Adding input validation logic
- Designing argument structures
Part 2: Python packages and project structure
From scripts to packages
One possible progression
- Stage 1: Single
.pyfile -my_script.pydoes everything - Stage 2: Multiple files -
utils.py,api.py,cli.pyin one folder - Stage 3: Package structure - Organized directories with
__init__.pyfiles - Stage 4: Installable package - Can run your own code through
uv - Design choice: Add structure when it improves imports, tests, or maintenance
Why package structure matters
- Code organization: Logical grouping of related functionality
- Reusability: Import code across multiple projects
- Collaboration: Team members work on different modules without conflicts
- Testing: Easier to test individual components in isolation
- Distribution: Share your code with others via PyPI
- Reviewability: Makes module responsibilities and imports visible
Examples
- Django: Models, views, templates organized in packages
- Requests:
requests/,requests/auth/,requests/models/ - Your final project: Use modules or a package when that structure improves the chosen project
What is a Python package?
- A regular package is a directory containing an
__init__.pyfile - Module: A single
.pyfile - Package: A directory of modules with
__init__.py - Subpackage: A package inside another package
__init__.py: Marks directory as package, can contain initialization code- Namespace packages: Can omit
__init__.py; this course uses regular packages unless a namespace is intentionally required
Basic package structure: a practical example
my_api_project/
│
├── my_api/ # Main package directory
│ ├── __init__.py # Marks this as a package
│ ├── api.py # API client code
│ ├── cli.py # CLI interface
│ └── utils.py # Utility functions
│
├── tests/ # Test directory
│ ├── __init__.py
│ ├── test_api.py
│ └── test_cli.py
│
├── main.py # Entry point script
├── pyproject.toml # Project metadata and dependencies
├── uv.lock # Exact resolved environment
└── README.md # Documentation
Using this structure:
# In main.py
from my_api.cli import main
from my_api.api import fetch_data
from my_api.utils import format_output
if __name__ == "__main__":
main()The __init__.py file: package initialization
Empty __init__.py (most common):
# my_api/__init__.py
# Empty file - marks directory as packageWith initialization code:
# my_api/__init__.py
"""Public interface for the API client package."""
# Import key functions for convenient access
from my_api.api import fetch_data, APIClient
from my_api.utils import format_output
# Package metadata
__version__ = "1.0.0"
__author__ = "Your Name"
# Define public API - controls "from my_api import *"
__all__ = ["fetch_data", "APIClient", "format_output"]Usage becomes cleaner:
# Instead of: from my_api.api import fetch_data
# You can do: from my_api import fetch_data
import my_api
print(my_api.__version__) # 1.0.0Your turn: practice
Design a package structure for your final project. Include: 1. A main package directory name 2. At least 3 module files (.py files) with their purposes 3. What you would put in __init__.py
Write your design as comments or markdown in the cell below.
# Your package structure design here
# Example:
# my_final_project/
# ├── pokemon_api/ # Main package
# │ ├── __init__.py # Package initialization
# │ ├── client.py # API client logic
# │ ├── cli.py # Command-line interface
# │ ├── models.py # Data models/classes
# │ └── utils.py # Helper functions
# ├── tests/ # Test directory
# ├── main.py # Entry point
# └── README.md # Documentation
# TODO: Design your own structureImport systems: absolute and relative
Absolute imports (preferred):
# my_api/cli.py
from my_api.api import fetch_data
from my_api.utils import format_output
import my_apiRelative imports (within a package):
# my_api/cli.py
from .api import fetch_data # Same directory
from .utils import format_output # Same directory
from ..other_package import helper # Parent directoryWhen to use each:
- Absolute imports: Often clearer about the package a name comes from
- Relative imports: Useful for large packages, makes refactoring easier
- PEP 8 recommendation: Prefer absolute imports unless path becomes excessively long
The __name__ == "__main__" pattern
Understanding Python execution:
- When Python runs a file directly:
__name__is set to"__main__" - When Python imports a file:
__name__is the module name - This allows code to be both a script (runnable) and a module (importable)
The pattern:
# my_api/api.py
def fetch_data(api_name: str):
"""Fetch data from an API."""
print(f"Fetching from {api_name}")
return {"data": "example"}
def main():
"""Entry point for running as a script."""
result = fetch_data("pokemon")
print(result)
# Script mode: runs when file is executed directly
if __name__ == "__main__":
main()Usage:
# Run as script
$ uv run python my_api/api.py
Fetching from pokemon
{'data': 'example'}# Import as module (main() doesn't run)
from my_api.api import fetch_dataThe src/ project layout
Example structure
my_api_project/
│
├── src/ # Optional source-layout directory
│ └── my_api/
│ ├── __init__.py
│ ├── api.py
│ ├── cli.py
│ └── utils.py
│
├── tests/ # Tests outside source tree
│ ├── __init__.py
│ ├── test_api.py
│ └── test_cli.py
│
├── docs/ # Documentation
│ └── index.md
│
├── pyproject.toml # Modern Python project metadata
├── README.md
└── .gitignore
Why src/ layout?
- Import discipline: Ensures you test installed version, not local files
- Namespace clarity: Prevents accidental relative imports
- Documented option: PyPA compares source and flat layouts
- Build isolation: Cleaner separation of source and build artifacts
Making code installable: pyproject.toml
Modern Python packaging with pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-api-tool"
version = "0.1.0"
description = "An API client for [Your API]"
authors = [{name = "Your Name", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["requests"]
[project.optional-dependencies]
dev = ["pytest", "ruff"]
[project.scripts]
my-api = "my_api.cli:main" # Creates a 'my-api' commandInstalling in editable mode:
# Install and lock the project for development
uv sync
# Now you can import it anywhere
uv run python -c "from my_api import fetch_data"
# And use the CLI command
uv run my-api pokemon pikachuOne project workflow with uv
uvmanages Python versions, environments, dependencies, commands, and lockfiles- Reproducible:
pyproject.tomldeclares dependencies anduv.lockrecords exact resolutions - No activation step:
uv runexecutes inside the project environment - One local/CI interface: The same commands work on a laptop and in GitHub Actions
- Written in Rust: A useful bridge to the next half of the course
Basic usage:
# Reproduce a committed project
uv sync --locked
# Add dependencies in your own project
uv add requests
uv add --dev pytest
# Run inside the project environment
uv run python -m pytest
uv run my-api --helpAI-assisted package scaffolding
Prompt strategies for project structure:
- Initial scaffold: “Propose the smallest Python project tree for this API client, CLI, and tests. Explain the purpose of every file”
- Alternative: “Compare a flat layout with a
src/layout for this project and name the configuration each requires” - Migration: “Plan a behavior-preserving split of this script into modules; do not edit until the current tests pass”
- Documentation: “Generate a README with installation and usage instructions for my package”
Package tasks an assistant can draft:
- Generating boilerplate directory structures
- Creating
__init__.pyfiles with proper imports - Writing
pyproject.tomlconfigurations - Setting up testing frameworks
- Drafting README sections from verified commands and behavior
Writing your own AGENTS.md
Week 02 introduced the format; your project needs one
# AGENTS.md: weather CLI
A Python 3.12 CLI that reports forecasts from the Open-Meteo API.
Environment managed with `uv`. Source lives in `src/weather_cli/`.
## Commands
- Install: `uv sync`
- Test: `uv run python -m pytest tests/ -v`
- Run: `uv run weather --city Cincinnati`
## Conventions
- NumPy-style docstrings on every public function
- HTTP calls belong in `api_client.py` and are mocked in tests
- Read the API key from the `WEATHER_API_KEY` environment variable; never commit it
## Before you finish
Run the tests. If one fails, fix the implementation, not the test.This is not your README
- The README tells a person what the project is and how to use it
AGENTS.mdtells an agent how to work in the repository without breaking it- Commands, conventions, and constraints that would clutter a README belong here
- Overlap is fine; link between the two instead of maintaining both
Practical example: final-project structure
One possible structure for the final project
my_final_project/
│
├── src/
│ └── my_api_tool/
│ ├── __init__.py # Package initialization
│ ├── api_client.py # API interaction logic
│ ├── cli.py # argparse CLI interface
│ ├── models.py # Data models/classes
│ └── utils.py # Helper functions
│
├── tests/
│ ├── __init__.py
│ ├── test_api_client.py # API tests
│ ├── test_cli.py # CLI tests
│ └── test_models.py # Model tests
│
├── .github/
│ └── workflows/
│ └── tests.yml # GitHub Actions CI/CD
│
├── pyproject.toml # Project configuration
├── README.md # Your documentation
├── AGENTS.md # Briefing for coding agents
├── .gitignore # Git exclusions
└── uv.lock # Exact resolved dependencies
Putting it all together: CLI and package structure
Entry point (main.py or src/my_api_tool/__main__.py):
"""Entry point for the my_api_tool CLI."""
from my_api_tool.cli import main
if __name__ == "__main__":
main()CLI module (src/my_api_tool/cli.py):
"""Command-line interface for API tool."""
import argparse
import sys
from my_api_tool.api_client import APIClient
from my_api_tool.utils import format_output
def main():
parser = argparse.ArgumentParser(description="API client tool")
parser.add_argument("resource", help="Resource to fetch")
parser.add_argument("--format", choices=["json", "pretty"], default="pretty")
args = parser.parse_args()
try:
client = APIClient()
data = client.fetch(args.resource)
print(format_output(data, args.format))
return 0
except (OSError, ValueError) as error:
print(f"Error: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())Usage after install:
uv sync
uv run my-api-tool pokemon --format jsonYour turn: final exercise
Design a complete CLI application for your final project:
- Create a class for your API client (like
APIClientabove) - Create a CLI parser with appropriate arguments for your project
- Create a main function that integrates them together
- Use the
__name__ == "__main__"pattern
Test it with simulated arguments.
# Your complete final project CLI design here
# TODO: Create your API client class
class YourAPIClient:
pass
# TODO: Create your CLI parser
def create_your_cli_parser():
pass
# TODO: Create main function
def your_main():
pass
# TODO: Add __name__ == "__main__" pattern
if __name__ == "__main__":
your_main()Bridge to Rust: a preview of next week
Python packaging challenges we’ve seen:
- Manual structure: Create directories,
__init__.pyfiles by hand - Import complexity: Absolute vs relative,
__name__ == "__main__"patterns - Dependency management: Multiple tools (
pip,venv,pyproject.toml) - Build system: Choose from many options (setuptools, hatch, poetry, PDM)
Next week in Rust:
- Cargo provides one interface for common Rust project tasks
- One command:
cargo new my_projectcreates a conventional starting structure - Integrated commands: Dependency management, building, testing, and documentation
- Conventions:
cargo newsupplies defaults, while larger projects still require design decisions - Comparison point: Compare Cargo’s defaults with Python’s explicit project configuration
Resources and further learning
Official documentation
- argparse - Python standard library CLI tool
- Python Packages - Official modules and packages guide
- Packaging Projects - Python Packaging Authority tutorial
- Click - Alternative CLI framework (more advanced)
- Typer - Modern CLI with type hints
- Rich - Beautiful terminal output
- uv documentation - Fast Python package installer
Advanced topics
- PEP 8 - Python style guide
- PEP 621 - Project metadata in
pyproject.toml - Entry points - Creating console scripts
- Namespace packages - Splitting packages across distributions
Applying this to the final project
This week’s work session
- Choose a clear structure; a
src/package is useful for larger Python projects but is not required - Add a CLI interface with argparse for easy testing and demonstration
- Create clean imports between your modules
- Consider an entry point in
pyproject.tomlwhen it improves installation and use - Document usage in your README with examples
- Add an
AGENTS.mdwith your real commands, conventions, and constraints - Test your CLI with different arguments and edge cases
Success criteria
- Can run
uv syncand launch the project withuv run - CLI works with intuitive arguments and helpful error messages
- Code is organized in logical modules with clean imports
--helpflag provides clear usage instructions- README explains installation and usage
AGENTS.mdwould orient an agent that has never seen the repository
Summary: key takeaways
- CLI with argparse: Create command-line tools with generated help and validation
- Package structure: Organize code with
__init__.py, proper imports, and logical modules __name__ == "__main__": Make code both runnable and importable- Modern tooling:
pyproject.tomldescribes a project anduvreproduces its environment - Integration: A CLI plus clear module boundaries can improve testing and maintenance
- Final project application: Apply the patterns that make your chosen project easier to run and test
- Next week: Compare this workflow with Cargo’s defaults and commands
Lab 08 checkpoint: weather CLI
Complete the supplied three-module contract:
favorites.py:FavoritesManagerstores case-insensitive favorites in JSON and recovers from missing or corrupted filesweather_api.py:WeatherAPIcalls the specified current and forecast endpoints withparamsandtimeout=10;format_current_weather()andformat_forecast()produce the required textweather.py:load_api_key(),build_parser(), andmain(argv=None)implement the documented commands, favorite lookup, standard-error messages, and exit status- Automated tests use fake responses; manual API use requires an ignored
config.pyor theWEATHER_API_KEYenvironment variable - Do not modify the supplied tests, workflow,
config.example.py, or root.gitignore
uv run python -m pytest week08/tests/ -v
git add week08/favorites.py week08/weather_api.py week08/weather.py
git commit -m "Complete Lab 08"
git push origin mainConfirm the Week 08 README badge is green; that badge is the complete 10-point grade.
Instructions: week08/lab08.md
Work session: build a maintainable Python application
Today’s goals
- Choose a structure that fits the size of your final project
- Implement a CLI interface with argparse
- Create clean module organization
- Test your CLI and fix any issues
- Update your README with installation and usage instructions
Need help?
- Review the slide examples
- Use AI assistants for structure scaffolding
- Ask questions as you work
- Reference the official documentation
- Test frequently as you refactor
Build one small, testable improvement before adding more structure.