# Import required libraries
import argparse
import sys
from typing import Optional, List
print("β
Setup complete! Ready to build professional Python applications.")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:
- Create professional CLI applications using Pythonβs
argparselibrary - Design intuitive command-line interfaces with positional arguments, flags, and subcommands
- Structure Python projects as proper packages with
__init__.pyand clean imports - Understand the
__name__ == "__main__"pattern for dual-purpose scripts - Make your code installable using modern
pyproject.tomlconfiguration - 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.
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 implementationAdvanced 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 structureImport 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:
- 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 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:
- Restructure your project with proper package layout
- Create package directory with
__init__.py - Separate concerns into modules (api, cli, utils, models)
- Add
tests/directory
- Create package directory with
- Add CLI interface using argparse
- Design intuitive commands and arguments
- Add help text for all options
- Include error handling and validation
- Create
pyproject.tomlfor your project- Define project metadata
- List dependencies
- Create CLI entry point
- Test installation
- Run
pip install -e .in your project directory - Test your CLI command
- Verify imports work from other directories
- Run
- Update README.md
- Installation instructions
- Usage examples with your CLI
- Project structure explanation
Resources
- argparse documentation
- Python Packages tutorial
- pyproject.toml guide
- Click library - Advanced CLI framework
- Typer library - Modern CLI with type hints
π‘ Tips for Success
- Start simple: Get basic structure working first, then refine
- Test frequently: Run your CLI after each change
- Use AI assistance: Great for generating boilerplate and structure
- Study examples: Look at popular Python packages on GitHub
- Ask for help: Use office hours if you get stuck on structure
Good luck building your professional Python application! π