Lesson 35 • Advanced
Building Command-Line Tools with argparse & Typer
Build real command-line tools the same way professionals build developer utilities, automated scripts, data pipelines, AI model runners, and deployment tools. Master both argparse (standard, built-in) and Typer (modern, FastAPI-style) for creating production-grade CLI applications.
🧰 What You'll Learn
This lesson teaches how to build real command-line tools, the same way professionals build:
✔ Developer utilities
✔ Automated scripts
✔ Data pipelines
✔ AI model runners
✔ Server management tools
✔ Deployment scripts
✔ Productivity tools
You will learn two approaches:
1. argparse (standard, low-level, built into Python) — Perfect for: small scripts, simple tools, full control.
2. Typer (modern, high-level, super-fast development) — Perfect for: professional apps, developer tools, banners, sub-commands, autocomplete.
Part 1: CLI Fundamentals — argparse Basics & Introduction to Typer
🔥 1. Why Command-Line Tools Matter
Before GUIs, everything ran in the terminal. But even today:
- developers use CLI tools daily (git, pip, docker)
- servers run scripts using CLI arguments
- automations depend on commands and flags
- real engineers build internal CLI tools to improve workflows
- cloud deployment (AWS/GCP/Azure) relies heavily on CLI automation
| CLI Tool | What It Does | Why It's Fast |
|---|---|---|
git push | Upload code to server | 1 command vs. clicking through UI |
pip install | Add a library | Instant, no browser needed |
docker run | Start a container | Scriptable, automatable |
python script.py --help | Show tool options | Self-documenting |
Having CLI-building skills makes you 10× more valuable as a developer.
⚙️ 2. argparse Basics — The Standard Python CLI Option
argparse ships with Python. No installation needed.
argparse Basics
Basic CLI tool with argparse
import argparse
parser = argparse.ArgumentParser(description="Simple greeting tool")
parser.add_argument("name", help="The name of the person to greet")
# For demo purposes, we'll parse known args
# In real CLI: args = parser.parse_args()
args = parser.parse_args(["Boopie"])
print(f"Hello, {args.name}!")
# Run in terminal: python app.py Boopie
# Output: Hello, Boopie!🧠 3. Positional vs Optional Arguments
| Type | Syntax | Example | Required? |
|---|---|---|---|
| Positional | filename | python tool.py data.csv | Yes |
| Optional | --verbose or -v | python tool.py --verbose | No |
Positional — Required inputs:
Positional Arguments
Required input arguments
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("path", help="File path")
# Demo: parse with sample argument
args = parser.parse_args(["myfile.txt"])
print(f"Path: {args.path}")Optional — Flags that start with - or --:
Optional Arguments
Flags with - or --
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", "-v", action="store_true")
# Demo: parse with verbose flag
args = parser.parse_args(["--verbose"])
print(f"Verbose mode: {args.verbose}")Usage: python tool.py file.txt --verbose
Optional flags improve usability and mirror real CLI tools like pip, git, and docker.
📝 4. Typed Arguments: int, float, file paths
argparse automatically converts types:
Typed Arguments
Automatic type conversion
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--count", type=int, default=1)
parser.add_argument("--rate", type=float, default=1.0)
# Demo: parse with typed arguments
args = parser.parse_args(["--count", "5", "--rate", "2.5"])
print(f"Count: {args.count}, Rate: {args.rate}")
# Or accept file objects:
# parser.add_argument("--file", type=argparse.FileType("r"))This is extremely useful for real tools.
🧩 5. Sub-Commands (Like Git)
Many tools behave like: git add, git push, git commit. You can create the same pattern:
Sub-Commands
Git-style subcommands
import argparse
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command")
add_cmd = sub.add_parser("add")
add_cmd.add_argument("numbers", nargs="+", type=int)
show_cmd = sub.add_parser("show")
# Demo: parse "add" subcommand
args = parser.parse_args(["add", "1", "2", "3"])
if args.command == "add":
print(f"Sum: {sum(args.numbers)}")Example usage:
python tool.py add 1 2 3
python tool.py showThis transforms your script into a multi-command CLI application.
⚡ 6. Real Project Example — File Organizer
File Organizer CLI
Real project example
import argparse
import os
import shutil
def main():
parser = argparse.ArgumentParser(description="Organize files by extension")
parser.add_argument("folder")
# Demo: simulate with current directory info
print("File Organizer Tool")
print("Usage: python organize.py <folder>")
print("This tool organizes files into subfolders by extension.")
# Example of what the tool would do:
sample_files = ["report.pdf", "image.jpg", "script.py"]
for file in sample_file
...Run: python organize.py downloads/
Automatically sorts files into subfolders.
🚀 7. Introduction to Typer (The Modern CLI Framework)
Typer is built by the creator of FastAPI. It offers:
| Feature | argparse | Typer |
|---|---|---|
| Setup Code | 10+ lines | 2-3 lines |
| Type Validation | Manual | Automatic |
| Help Generation | Basic | Beautiful |
| Colors/Formatting | Manual | Built-in |
| Autocompletion | Complex | One command |
Install: pip install typer[all]
🧠 8. Basic Typer Example
Basic Typer Example
Modern CLI framework
# Note: Typer requires installation: pip install typer[all]
# This is a conceptual example
# import typer
# app = typer.Typer()
# @app.command()
# def greet(name: str):
# typer.echo(f"Hello {name}!")
# if __name__ == "__main__":
# app()
# Simulated output:
print("Typer CLI Example")
print("Command: greet Boopie")
print("Output: Hello Boopie!")
print()
print("Typer generates:")
print(" ✔ automatic help screen")
print(" ✔ validation")
print(" ✔ color output")
print(" ✔ autocomplete
...Run: python app.py greet Boopie
🎛 9. Optional Arguments in Typer
It's cleaner than argparse:
Optional Arguments in Typer
Cleaner than argparse
# Typer optional arguments example
# @app.command()
# def download(url: str, retries: int = typer.Option(3), verbose: bool = False):
# typer.echo(f"Downloading {url} with {retries} retries, verbose={verbose}")
# Simulated output:
url = "https://example.com"
retries = 5
verbose = True
print(f"Downloading {url} with {retries} retries, verbose={verbose}")
# Usage: python tool.py download https://example.com --retries 5 --verboseUsage: python tool.py download https://example.com --retries 5 --verbose
🧩 10. Typed Options & Defaults
Typed Options
Automatic type validation
# Typer typed options example
# @app.command()
# def math(x: int, y: int = 10):
# typer.echo(x + y)
# Simulated:
x = 5
y = 10
print(f"x + y = {x + y}")
# Typer automatically:
# ✔ validates integers
# ✔ uses type hints
# ✔ generates help textThis makes code cleaner and more maintainable.
🏗 11. Subcommands (Typer's strongest feature)
Typer Subcommands
Multi-command CLI apps
# Typer subcommands example
# import typer
# app = typer.Typer()
# files = typer.Typer()
# users = typer.Typer()
# app.add_typer(files, name="files")
# app.add_typer(users, name="users")
# @files.command()
# def clean():
# typer.echo("Cleaning files...")
# @users.command()
# def add(name: str):
# typer.echo(f"Added user: {name}")
# Simulated output:
print("Command: python tool.py files clean")
print("Output: Cleaning files...")
print()
print("Command: python tool.py users add Boopie
...Run:
python tool.py files clean
python tool.py users add BoopieYou now have a full CLI program with modules.
🧠 12. Real Project Example — AI Assistant CLI
AI Assistant CLI
Real project example
# AI Assistant CLI example
# import typer
# import openai
# app = typer.Typer()
# @app.command()
# def ask(prompt: str):
# typer.echo("Thinking...")
# response = "Pretend AI answer"
# typer.echo(response)
# Simulated:
prompt = "What is Python?"
print("Thinking...")
print("Python is a high-level programming language known for its readability.")
print()
print("Typer is fantastic for developer tools, automation scripts, and utilities.")Part 2: Advanced Patterns — Real Engineering Features
In this part, we go deeper into real engineering patterns, advanced argument features, error handling, output design, color/UI improvements, and structuring multi-file CLI apps the same way professionals build tools like pip, docker, aws-cli, git, uv, and fastapi-cli.
⚡ 1. Advanced argparse Features You MUST Know
✔ Mutually Exclusive Groups
Sometimes a user should only choose one option:
Mutually Exclusive Groups
Only one option allowed
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("--verbose", action="store_true")
group.add_argument("--quiet", action="store_true")
# Demo: can only use one
args = parser.parse_args(["--verbose"])
print(f"Verbose: {args.verbose}, Quiet: {args.quiet}")
# Using both would cause an error:
# python tool.py --verbose --quiet # ERROR!Now the user cannot do: python tool.py --verbose --quiet
This is used for logging mode, compression types, environment switches, etc.
✔ nargs — Accepting multiple values
nargs - Multiple Values
Accept multiple arguments
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--files", nargs="+")
# Demo: multiple files
args = parser.parse_args(["--files", "a.txt", "b.txt", "c.txt"])
print(f"Files: {args.files}")
# Usage: python tool.py --files a.txt b.txt c.txtUseful for batch operations.
✔ Choices — Restricting allowed values
Choices
Restrict allowed values
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--level", choices=["low", "medium", "high"])
# Demo: valid choice
args = parser.parse_args(["--level", "high"])
print(f"Level: {args.level}")
# Invalid choice would cause error automaticallyStops invalid inputs before they crash your program.
✔ Default values + required flags
Required Flags
Mandatory arguments
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--mode", required=True)
parser.add_argument("--count", type=int, default=1)
# Demo
args = parser.parse_args(["--mode", "production"])
print(f"Mode: {args.mode}, Count: {args.count}")
# argparse handles missing required flags automatically🧠 2. Using Config Files + CLI Arguments Together
Professional CLI tools combine: environment variables, config files, command-line arguments.
Example: python deploy.py --config config.yaml --env production
argparse supports this pattern through: FileType, custom loaders, layered parsing logic
🧰 3. Advanced Typer Patterns (Modern CLI Engineering)
✔ Prompting users interactively
Interactive Prompts
User input in CLI
# Typer interactive prompts
# @app.command()
# def register():
# name = typer.prompt("Enter your name")
# password = typer.prompt("Password", hide_input=True)
# typer.echo(f"Welcome, {name}!")
# Simulated:
print("Enter your name: Boopie")
print("Password: ********")
print("Welcome, Boopie!")
print()
print("This is real UX - great for account creation scripts,"
" secure flows, admin utilities.")✔ Confirmation dialogs
Confirmation Dialogs
Ask before dangerous actions
# Typer confirmation dialogs
# @app.command()
# def delete_user(name: str):
# if typer.confirm(f"Delete user {name}?"):
# typer.echo("Deleted.")
# else:
# typer.echo("Cancelled.")
# Simulated:
name = "Boopie"
print(f"Delete user {name}? [y/N]: y")
print("Deleted.")✔ Progress bars
Progress Bars
Visual feedback
# Typer progress bars
# with typer.progressbar(range(100)) as bar:
# for i in bar:
# time.sleep(0.01)
# Simulated:
print("Processing: [████████████████████████████████] 100%")
print("Done!")
print()
print("Progress bars make tools feel professional instantly.")✔ Rich (Color + Formatting)
Rich Formatting
Colors and styling
# Rich console output
# from rich.console import Console
# console = Console()
# @app.command()
# def info():
# console.print("[bold green]Server Running[/bold green]")
# Simulated:
print("\033[1;32mServer Running\033[0m") # Bold green
print()
print("Used in: FastAPI CLI, Poetry, Modern developer tools")⚙️ 4. Handling Errors Gracefully
Typer supports structured exceptions:
Error Handling
Graceful error messages
# Typer error handling
# class NotFound(Exception):
# pass
# @app.command()
# def get_user(user_id: int):
# if user_id != 1:
# raise NotFound("User not found")
# @app.error_handler(NotFound)
# def handle_not_found(e):
# typer.echo(f"Error: {e}")
# Simulated:
user_id = 99
if user_id != 1:
print(f"Error: User {user_id} not found")
print()
print("This makes CLI tools 'feel' polished.")🧩 5. Building Multi-Command Apps (Professional Structure)
Real CLIs use folder architectures. For Typer:
app/
├─ main.py
├─ users.py
├─ files.py
└─ config.pymain.py:
Main Entry Point
Multi-command structure
# main.py
# import typer
# import users, files
# app = typer.Typer()
# app.add_typer(users.app, name="users")
# app.add_typer(files.app, name="files")
# if __name__ == "__main__":
# app()
print("Multi-command CLI structure:")
print(" python tool.py users add Boopie")
print(" python tool.py files clean")
print(" python tool.py config set key value")users.py:
Users Module
Subcommand module
# users.py
# import typer
# app = typer.Typer()
# @app.command()
# def add(name: str):
# typer.echo(f"Added user: {name}")
# Simulated:
name = "Boopie"
print(f"Added user: {name}")
print()
print("This is identical to how enterprise tools structure their commands.")🧠 6. Environment-Aware Commands
Combine environment variables:
Environment Variables
Environment-aware commands
import os
# @app.command()
# def upload(file: str):
# key = os.getenv("API_KEY")
# if not key:
# typer.echo("Missing API_KEY")
# raise typer.Exit()
# Simulated:
key = os.getenv("API_KEY")
if not key:
print("Missing API_KEY")
print("Set it with: export API_KEY='your-key'")
else:
print(f"Using API key: {key[:4]}...")
print()
print("Used heavily in: cloud automation, CI/CD tools, backend deployment flows")📦 7. Creating "Plugins" for CLI Tools
Typer supports loading commands dynamically:
Plugin System
Dynamic command loading
# Plugin loading pattern
# def load_plugins(app):
# for plugin in discover_plugins():
# app.add_typer(plugin.typer_app)
# Simulated plugin discovery:
plugins = ["analytics", "backup", "export"]
for plugin in plugins:
print(f"Loaded plugin: {plugin}")
print()
print("This pattern is how tools like pytest, aws-cli, docker")
print("extend functionality through plugins.")🔍 8. Auto-generated Help Screens
Typer help output looks like:
Usage: tool.py greet [OPTIONS] NAME
Arguments:
NAME Your name.
Options:
--wave / --no-wave Whether to wave.
--help Show this message and exit.This UI alone saves hours of documentation. argparse also generates help automatically, but Typer's formatting is noticeably cleaner.
🧱 9. Testing CLI Tools
You can test argparse & Typer apps with pytest using CliRunner. Example for Typer:
Testing CLI Tools
pytest with CliRunner
# Testing CLI with pytest
# from typer.testing import CliRunner
# from main import app
# runner = CliRunner()
# def test_greet():
# result = runner.invoke(app, ["greet", "Boopie"])
# assert "Hello Boopie" in result.stdout
# Simulated test:
def test_greet():
# Simulated result
stdout = "Hello Boopie!"
assert "Hello Boopie" in stdout
print("✔ test_greet PASSED")
test_greet()
print()
print("Testing CLIs makes them reliable.")🌐 10. Packaging Your CLI as a PIP Installable Tool
You can turn a Typer/argparse script into a real installable command:
pyproject.toml:
[project.scripts]
learnfast = "app.main:app"Users can now install: pip install learnfast
And then run: learnfast
Exactly how uv, pip, and fastapi work.
Part 3: Enterprise-Grade CLI Engineering — Production Deployment
This final section focuses on enterprise-grade CLI engineering, including structured logging, configuration layers, packaging/distribution, versioning, interactive shell modes, autocomplete, performance optimisation, security best practices, and production deployment patterns.
⚡ 1. Enterprise-Style Configuration Layers
Professional CLI apps often support configuration from multiple layers:
- Default settings
- Config file (YAML/JSON/TOML)
- Environment variables
- Command-line arguments (highest priority)
Example loading order:
Configuration Layers
Enterprise config pattern
import os
# Configuration layer pattern
def load_defaults():
return {"debug": False, "port": 8000}
def load_yaml(path):
# Would use yaml.safe_load() in real code
return {"port": 3000}
def read_env():
return {"debug": os.getenv("DEBUG", "false") == "true"}
# Build config with priority
config = load_defaults()
print(f"1. Defaults: {config}")
# if os.path.exists("config.yaml"):
config.update(load_yaml("config.yaml"))
print(f"2. After YAML: {config}")
config.update(read_env())
...This allows flexible behaviour: users can override defaults, automation pipelines can use environment variables, developers can test using temporary config files. Typer-based tools commonly use pydantic or dynaconf to manage all layers cleanly.
🧠 2. CLI Autocompletion (Bash, Zsh, Fish)
Modern CLI tools include autocomplete for: commands, options, file paths, arguments.
Typer makes this extremely simple:
tool --install-completion
tool --show-completionUnder the hood, it generates shell-compatible scripts. This dramatically improves user experience and is a key part of professional developer tools.
📦 3. Packaging as a Standalone Executable (Windows, Mac, Linux)
Not every user has Python installed. You can turn your CLI into one executable file using:
PyInstaller
pyinstaller --onefile main.pyNuitka (fastest) — Compiles Python to C.
nuitka --onefile main.pyBriefcase — Creates full OS-native installers.
This is how tools become "real" apps that run anywhere.
🧩 4. Versioning & Release Flows
Every CLI should have a version:
Versioning
CLI version management
# Versioning your CLI
__version__ = "1.2.0"
# @app.command()
# def version():
# typer.echo("LearnCodingFast CLI v1.2.0")
# Or use Typer's built-in:
# app = typer.Typer(context_settings={"help_option_names": ["--help"]})
print(f"LearnCodingFast CLI v{__version__}")
print()
print("Release strategy:")
print(" 1. increment version")
print(" 2. build wheel")
print(" 3. publish to PyPI")
print(" 4. update docs")
print(" 5. tag release in GitHub")⚙️ 5. Creating Subcommands Like Professional Tools (docker, git, aws-cli)
Large CLI tools organise commands into modules:
tool
├─ users add
├─ users delete
├─ files upload
├─ files download
├─ system infoTyper subcommands:
Professional Subcommands
Docker/Git style structure
# Professional subcommand structure
# app = typer.Typer()
# users_app = typer.Typer()
# files_app = typer.Typer()
# app.add_typer(users_app, name="users")
# app.add_typer(files_app, name="files")
print("Professional CLI structure:")
print(" tool users add <name>")
print(" tool users delete <id>")
print(" tool files upload <path>")
print(" tool files download <url>")
print(" tool system info")
print()
print("Each subcommand can have its own flags,")
print("callbacks, error handlers, and he
...🧵 6. Integrating Your CLI With Other Tools (Pipes & Redirects)
CLI tools should work with Linux/Windows pipes:
echo "data" | tool process
tool list | grep "Boopie"
tool fetch > output.jsonTyper + sys.stdin:
Pipes & Redirects
Unix pipeline integration
import sys
# Reading from stdin for piped input
# data = sys.stdin.read()
# Simulated pipe input
simulated_input = "Hello from pipe!"
print(f"Received: {simulated_input}")
print()
print("Your CLI can now be used inside:")
print(" ✔ shells")
print(" ✔ automation scripts")
print(" ✔ cron jobs")
print(" ✔ CI/CD pipelines")
print()
print("This elevates it to real engineering level.")🔒 7. Authentication & Secrets Management
If your CLI interacts with APIs, databases, or cloud services, it must handle secrets safely. Best practices:
- ✔ Use environment variables:
export API_KEY="123" - ✔ Never store secrets in code
- ✔ Use a .env loader (python-dotenv)
- ✔ Hide passwords in Typer prompts
Secrets Management
Secure credential handling
# Secure password input
# password = typer.prompt("Password", hide_input=True)
import getpass
# Simulated secure input
print("Password: ", end="")
# password = getpass.getpass("") # Would hide input
print("********")
print()
print("Best practices:")
print(" ✔ Use environment variables")
print(" ✔ Never store secrets in code")
print(" ✔ Use python-dotenv for .env files")
print(" ✔ Hide passwords in prompts")
print(" ✔ Encrypt stored tokens")🧪 8. Writing Realistic Tests for CLI Behaviour
Use Typer's testing tools:
CLI Testing
Comprehensive test patterns
# Comprehensive CLI testing
# from typer.testing import CliRunner
# runner = CliRunner()
# def test_delete():
# result = runner.invoke(app, ["users", "delete", "123"])
# assert result.exit_code == 0
# assert "Deleted" in result.stdout
# Simulated tests:
def test_delete():
exit_code = 0
stdout = "Deleted user 123"
assert exit_code == 0
assert "Deleted" in stdout
print("✔ test_delete PASSED")
def test_invalid_command():
exit_code = 2
assert exit_code !=
...🌐 9. Color, Formatting & Tabular Output (Rich + Typer)
Real-world tools display: tables, panels, logs, status values, colour-coded results.
Rich Tables
Beautiful CLI output
# Rich table output
# from rich.table import Table
# from rich.console import Console
# @app.command()
# def stats():
# table = Table(title="User Stats")
# table.add_column("Name")
# table.add_column("Uploads")
# table.add_row("Boopie", "22")
# table.add_row("Josh", "5")
# Console().print(table)
# Simulated table output:
print("┌───────────────────────┐")
print("│ User Stats │")
print("├──────────┬────────────┤")
print("│ Name │ Uploads │")
print("├───
...🧱 10. Performance & Concurrency Inside CLI Tools
CLI commands often do: API requests, file scanning, JSON parsing, image processing, database queries.
Improve speed using:
- ✔ ThreadPoolExecutor (I/O tasks)
- ✔ ProcessPoolExecutor (CPU tasks)
- ✔ asyncio (many lightweight tasks)
- ✔ caching (functools.lru_cache)
- ✔ batching
- ✔ chunk streaming
Your CLI becomes fast and scalable.
🚀 11. Real Engineering Pattern — Modular "Actions" Layer
For large CLIs, never put logic inside the command functions. Instead:
actions/
├─ users.py
├─ files.py
└─ analytics.pyCommand → calls → action function
This allows: rewiring logic, sharing helpers, unit testing functions directly, keeping CLI layer clean. This is how professional tools stay maintainable.
📦 12. Distribution: PyPI, Brew, Winget, and GitHub Releases
You can distribute your tool via:
- ✔ PyPI (pip install)
- ✔ Homebrew (Mac)
- ✔ Winget (Windows)
- ✔ Snap/Apt (Linux)
- ✔ GitHub Releases (binaries)
This is the path for turning your CLI into a global developer tool.
🎉 Final Summary
After completing all 3 parts, you can build CLI tools with:
✔ Professional UX
✔ Structured commands
✔ Interactive flows
✔ Autocomplete
✔ Config layers
✔ Error handling
✔ Testing
✔ Distribution
You now have the skills to build production-grade CLI tools!
📋 Quick Reference — CLI Tools
| Syntax / Tool | What it does |
|---|---|
| argparse.ArgumentParser() | Parse CLI arguments (stdlib) |
| parser.add_argument('--name') | Add a named flag argument |
| @app.command() (Typer) | Define a Typer CLI command |
| typer.Option(..., help=) | Add option with help text |
| rich.print("[bold]text[/bold]") | Pretty terminal output |
🎉 Great work! You've completed this lesson.
You can now build polished CLI tools with argument parsing, progress bars, coloured output, and bash completions.
Up next: Virtual Environments — manage isolated Python environments for every project.
Sign up for free to track which lessons you've completed and get learning reminders.