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.