IS4010: AI-Enhanced Application Development

Week 8: Building Professional Python Applications

Instructor: Brandon M. Greenwell
Focus: Command-Line Interfaces & Python Package Structure


πŸ“š Learning Objectives

By the end of this notebook, you will be able to:

  1. Create professional CLI applications using Python’s argparse library
  2. Design intuitive command-line interfaces with positional arguments, flags, and subcommands
  3. Structure Python projects as proper packages with __init__.py and clean imports
  4. Understand the __name__ == "__main__" pattern for dual-purpose scripts
  5. Make your code installable using modern pyproject.toml configuration
  6. Apply these patterns to your midterm project for professional-quality code

πŸ› οΈ Setup

This notebook contains executable examples. Run each cell in order to follow along.

# Import required libraries
import argparse
import sys
from typing import Optional, List

print("βœ… Setup complete! Ready to build professional Python applications.")

Part 1: Command-Line Interfaces with argparse

Why CLIs Matter

Command-line interfaces (CLIs) are essential for: - Automation: Run tasks without manual interaction - Scripting: Chain multiple commands together - Professional tools: Git, pip, docker - all CLIs - Career skills: Every Python job uses CLI tools

You’ve already used many CLIs: git, pip, python, jupyter


Basic argparse: Your First CLI

# Example 1: Simple greeter CLI

def create_greeter_parser():
    """Create a parser for a simple greeting tool."""
    parser = argparse.ArgumentParser(
        description="A simple greeting tool",
        epilog="Thanks for using the greeter!"
    )
    
    # Positional argument (required)
    parser.add_argument(
        "name",
        help="The name of the person to greet"
    )
    
    # Optional flag
    parser.add_argument(
        "--loud",
        action="store_true",
        help="Greet in ALL CAPS"
    )
    
    return parser

# Test it out (simulating command-line arguments)
parser = create_greeter_parser()

# Simulate: python greeter.py Alice
args = parser.parse_args(["Alice"])
greeting = f"Hello, {args.name}!"
if args.loud:
    greeting = greeting.upper()
print(greeting)

# Simulate: python greeter.py Bob --loud
args = parser.parse_args(["Bob", "--loud"])
greeting = f"Hello, {args.name}!"
if args.loud:
    greeting = greeting.upper()
print(greeting)

🎯 Your Turn: Exercise 1

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:

python calculator.py 10 5 --operation add     # Should print 15
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 implementation

Advanced argparse: Types, Defaults, and Validation

# Example 2: API fetcher with multiple argument types

def create_api_fetcher_parser():
    """Create a parser for an API data fetcher."""
    parser = argparse.ArgumentParser(
        description="Fetch data from various APIs"
    )
    
    # Positional arguments
    parser.add_argument("api", help="API name (e.g., 'pokemon', 'weather')")
    parser.add_argument("resource", help="Resource to fetch")
    
    # Optional argument with type conversion
    parser.add_argument(
        "-l", "--limit",
        type=int,
        default=10,
        help="Number of items to fetch (default: 10)"
    )
    
    # Choices restriction
    parser.add_argument(
        "--format",
        choices=["json", "csv", "txt"],
        default="json",
        help="Output format (default: json)"
    )
    
    # Optional output file
    parser.add_argument(
        "-o", "--output",
        help="Output file path (prints to console if not specified)"
    )
    
    return parser

# Test the parser
parser = create_api_fetcher_parser()

# Simulate: python api_fetcher.py pokemon pikachu -l 5 --format json
args = parser.parse_args(["pokemon", "pikachu", "-l", "5", "--format", "json"])
print(f"Fetching {args.limit} {args.resource} from {args.api} API")
print(f"Format: {args.format}")
print(f"Output: {args.output if args.output else 'console'}")

🎯 Your Turn: Exercise 2

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: Git-Style CLIs

# Example 3: Project manager with subcommands

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("Building project...")
    if args.verbose:
        print("Verbose output enabled")

def cmd_deploy(args):
    """Deploy the project."""
    print(f"Deploying to {args.environment}")

def create_project_manager_parser():
    """Create a parser with subcommands."""
    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)
    
    return parser

# Test subcommands
parser = create_project_manager_parser()

# Simulate: python project_manager.py init my_app --template django
args = parser.parse_args(["init", "my_app", "--template", "django"])
if hasattr(args, "func"):
    args.func(args)

print("\n---\n")

# Simulate: python project_manager.py deploy prod
args = parser.parse_args(["deploy", "prod"])
if hasattr(args, "func"):
    args.func(args)

🎯 Your Turn: Exercise 3

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)

Part 2: Python Packages & Project Structure

Understanding Python Packages

A package is a way to organize related Python modules (files) into a directory structure.

Key concepts: - Module: A single .py file - Package: A directory containing an __init__.py file - Subpackage: A package inside another package


The __name__ == "__main__" Pattern

# Example 4: Understanding __name__

# When Python runs a file directly, __name__ is "__main__"
# When Python imports a file, __name__ is the module name

def greet(name: str) -> str:
    """Return a greeting message."""
    return f"Hello, {name}!"

def main():
    """Entry point when run as a script."""
    print("This is the main function")
    print(greet("World"))

# This code only runs when the file is executed directly
# It does NOT run when the file is imported
if __name__ == "__main__":
    print(f"__name__ is: {__name__}")
    main()
else:
    print(f"This module was imported. __name__ is: {__name__}")

Why This Matters

The __name__ == "__main__" pattern allows your code to be: 1. Runnable as a script: python my_module.py 2. Importable as a library: from my_module import greet

This is essential for: - Testing individual modules - Creating reusable code - Professional project structure


Package Structure Example

# Example 5: Simulating package structure

# Imagine this file structure:
# my_api_project/
#   β”œβ”€β”€ my_api/
#   β”‚   β”œβ”€β”€ __init__.py
#   β”‚   β”œβ”€β”€ api.py
#   β”‚   β”œβ”€β”€ cli.py
#   β”‚   └── utils.py
#   └── main.py

# In api.py:
def fetch_data(api_name: str) -> dict:
    """Fetch data from an API."""
    return {"api": api_name, "data": "example_data"}

# In utils.py:
def format_output(data: dict, format_type: str = "json") -> str:
    """Format data for output."""
    if format_type == "json":
        return str(data)
    elif format_type == "pretty":
        return "\n".join(f"{k}: {v}" for k, v in data.items())
    return str(data)

# In main.py, you would import like this:
# from my_api.api import fetch_data
# from my_api.utils import format_output

# Demo the functions
data = fetch_data("pokemon")
print("JSON format:")
print(format_output(data, "json"))
print("\nPretty format:")
print(format_output(data, "pretty"))

The __init__.py File

# Example 6: What goes in __init__.py

# Option 1: Empty __init__.py (most common)
# Just marks the directory as a package

# Option 2: With convenience imports
# Imagine this is my_api/__init__.py:

# """My API Client Package - A professional API wrapper."""
#
# # Import key functions for convenient access
# from my_api.api import fetch_data
# from my_api.utils import format_output
#
# # Package metadata
# __version__ = "1.0.0"
# __author__ = "Your Name"
#
# # Define public API
# __all__ = ["fetch_data", "format_output"]

# With this __init__.py, users can do:
# from my_api import fetch_data  # Instead of: from my_api.api import fetch_data

print("βœ… __init__.py makes imports cleaner and more convenient")

🎯 Your Turn: Exercise 4

Design a package structure for your midterm 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_midterm_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 structure

Import Patterns: Absolute vs Relative

# Example 7: Import strategies

# ABSOLUTE IMPORTS (preferred)
# from my_api.api import fetch_data
# from my_api.utils import format_output

# RELATIVE IMPORTS (within a package)
# In my_api/cli.py:
# from .api import fetch_data        # Same directory
# from .utils import format_output   # Same directory
# from ..parent import something     # Parent directory

# Best practice: Use absolute imports unless:
# 1. You're within a large package
# 2. Absolute paths become very long
# 3. You're following team conventions

print("βœ… Absolute imports are clearer and easier to understand")
print("βœ… Relative imports survive package renaming")
print("Choose based on your project needs!")

Modern Python: pyproject.toml

# Example 8: Understanding pyproject.toml

pyproject_toml_example = """
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-api-tool"
version = "0.1.0"
description = "A professional API client"
authors = [{name = "Your Name", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "requests>=2.31.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black>=23.0.0",
]

[project.scripts]
my-api = "my_api.cli:main"  # Creates a 'my-api' command
"""

print("Example pyproject.toml configuration:")
print(pyproject_toml_example)

print("\nβœ… After 'pip install -e .', you can:")
print("   - Import your package from anywhere")
print("   - Run your CLI with the command name (my-api)")
print("   - Share your package with others")

Putting It All Together: Complete Example

Here’s how CLI + package structure work together in a real project.

# Example 9: Complete integration

# ===== FILE: my_api/api.py =====
class APIClient:
    """Simple API client."""
    
    def __init__(self, base_url: str = "https://api.example.com"):
        self.base_url = base_url
    
    def fetch(self, resource: str) -> dict:
        """Fetch a resource from the API."""
        # In a real implementation, this would make an HTTP request
        return {
            "resource": resource,
            "data": f"Mock data for {resource}",
            "status": "success"
        }

# ===== FILE: my_api/utils.py =====
def format_output(data: dict, format_type: str = "pretty") -> str:
    """Format API response for display."""
    if format_type == "json":
        return str(data)
    elif format_type == "pretty":
        return "\n".join(f"{k}: {v}" for k, v in data.items())
    return str(data)

# ===== FILE: my_api/cli.py =====
def create_cli_parser():
    """Create the CLI parser."""
    parser = argparse.ArgumentParser(description="My API Client")
    parser.add_argument("resource", help="Resource to fetch")
    parser.add_argument(
        "--format",
        choices=["json", "pretty"],
        default="pretty",
        help="Output format"
    )
    return parser

def cli_main():
    """Main entry point for CLI."""
    parser = create_cli_parser()
    args = parser.parse_args(["pokemon", "--format", "pretty"])  # Simulated args
    
    # Use the API client
    client = APIClient()
    data = client.fetch(args.resource)
    
    # Format and display output
    output = format_output(data, args.format)
    print(output)
    return 0

# Run the integrated example
print("=== Running integrated CLI + Package example ===")
cli_main()

🎯 Your Turn: Final Exercise

Design a complete CLI application for your midterm project:

  1. Create a class for your API client (like APIClient above)
  2. Create a CLI parser with appropriate arguments for your project
  3. Create a main function that integrates them together
  4. Use the __name__ == "__main__" pattern

Test it with simulated arguments.

# Your complete midterm 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()

πŸš€ Next Steps: Applying to Your Midterm

Action Items for This Week:

  1. Restructure your project with proper package layout
    • Create package directory with __init__.py
    • Separate concerns into modules (api, cli, utils, models)
    • Add tests/ directory
  2. Add CLI interface using argparse
    • Design intuitive commands and arguments
    • Add help text for all options
    • Include error handling and validation
  3. Create pyproject.toml for your project
    • Define project metadata
    • List dependencies
    • Create CLI entry point
  4. Test installation
    • Run pip install -e . in your project directory
    • Test your CLI command
    • Verify imports work from other directories
  5. Update README.md
    • Installation instructions
    • Usage examples with your CLI
    • Project structure explanation

Resources


πŸ’‘ Tips for Success

  1. Start simple: Get basic structure working first, then refine
  2. Test frequently: Run your CLI after each change
  3. Use AI assistance: Great for generating boilerplate and structure
  4. Study examples: Look at popular Python packages on GitHub
  5. Ask for help: Use office hours if you get stuck on structure

Good luck building your professional Python application! πŸŽ‰