
How to Create a CLI with Python Fire on Ubuntu 24
Python Fire is a library that automatically generates command-line interfaces from Python objects, making it incredibly easy to turn any Python function or class into a CLI tool. For developers working on Ubuntu 24, this approach eliminates the boilerplate code typically required with argparse or click while providing powerful introspection and debugging capabilities. In this guide, you’ll learn how to install Python Fire, create various types of CLI applications, handle edge cases, and implement best practices for production-ready command-line tools.
Understanding Python Fire’s Magic
Python Fire works by using introspection to analyze your Python objects at runtime and automatically generating a command-line interface. Unlike traditional CLI frameworks that require explicit argument definitions, Fire reads your function signatures, docstrings, and type hints to create an intuitive command structure.
import fire
def greet(name, greeting="Hello"):
"""Greet someone with a custom message."""
return f"{greeting}, {name}!"
if __name__ == '__main__':
fire.Fire(greet)
When you run this script, Fire automatically creates flags for each parameter, handles type conversion, and provides help documentation based on your docstring. The magic happens through Python’s inspect module and dynamic argument parsing.
Setting Up Python Fire on Ubuntu 24
First, ensure you have Python 3.12 (the default on Ubuntu 24) and pip installed:
sudo apt update
sudo apt install python3 python3-pip python3-venv
python3 --version
Create a virtual environment to avoid dependency conflicts:
mkdir ~/cli-projects
cd ~/cli-projects
python3 -m venv fire-env
source fire-env/bin/activate
Install Python Fire and verify the installation:
pip install fire
python -c "import fire; print(fire.__version__)"
For development work, also install helpful debugging tools:
pip install ipython rich
Creating Your First CLI Application
Let’s build a file management CLI that demonstrates Fire’s core capabilities:
#!/usr/bin/env python3
"""
File Manager CLI - A simple file management tool using Python Fire
"""
import os
import shutil
import fire
from pathlib import Path
class FileManager:
"""A simple file management CLI tool."""
def list_files(self, directory=".", pattern="*", show_hidden=False):
"""List files in a directory with optional pattern matching.
Args:
directory: Directory to list (default: current directory)
pattern: File pattern to match (default: all files)
show_hidden: Include hidden files (default: False)
"""
path = Path(directory)
if not path.exists():
return f"Error: Directory {directory} does not exist"
files = []
for item in path.glob(pattern):
if not show_hidden and item.name.startswith('.'):
continue
files.append({
'name': item.name,
'size': item.stat().st_size if item.is_file() else 'DIR',
'type': 'file' if item.is_file() else 'directory'
})
return files
def copy_file(self, source, destination, overwrite=False):
"""Copy a file to a new location.
Args:
source: Source file path
destination: Destination file path
overwrite: Overwrite if destination exists (default: False)
"""
if not Path(source).exists():
return f"Error: Source file {source} does not exist"
if Path(destination).exists() and not overwrite:
return f"Error: Destination {destination} exists. Use --overwrite to replace"
try:
shutil.copy2(source, destination)
return f"Successfully copied {source} to {destination}"
except Exception as e:
return f"Error copying file: {str(e)}"
def create_backup(self, file_path, backup_dir="./backups"):
"""Create a backup of a file with timestamp.
Args:
file_path: Path to file to backup
backup_dir: Directory to store backups (default: ./backups)
"""
import datetime
source = Path(file_path)
if not source.exists():
return f"Error: File {file_path} does not exist"
backup_path = Path(backup_dir)
backup_path.mkdir(exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"{source.stem}_{timestamp}{source.suffix}"
destination = backup_path / backup_name
try:
shutil.copy2(source, destination)
return f"Backup created: {destination}"
except Exception as e:
return f"Error creating backup: {str(e)}"
if __name__ == '__main__':
fire.Fire(FileManager)
Save this as filemanager.py
and make it executable:
chmod +x filemanager.py
Now you can use your CLI in multiple ways:
# List files in current directory
./filemanager.py list_files
# List Python files in a specific directory
./filemanager.py list_files /home/user/projects "*.py" --show_hidden
# Copy a file
./filemanager.py copy_file README.md README_backup.md
# Create a timestamped backup
./filemanager.py create_backup important_file.txt
Advanced CLI Patterns and Real-World Examples
Here’s a more sophisticated example that demonstrates nested commands, configuration handling, and error management:
#!/usr/bin/env python3
"""
System Monitor CLI - Advanced Python Fire example
"""
import fire
import json
import subprocess
import psutil
from pathlib import Path
from typing import Dict, List, Optional
class SystemMonitor:
"""Advanced system monitoring CLI with nested commands."""
def __init__(self, config_file: Optional[str] = None):
self.config = self._load_config(config_file)
def _load_config(self, config_file: Optional[str]) -> Dict:
"""Load configuration from file or use defaults."""
default_config = {
"alert_thresholds": {
"cpu_percent": 80,
"memory_percent": 85,
"disk_percent": 90
},
"monitoring_interval": 5
}
if config_file and Path(config_file).exists():
try:
with open(config_file, 'r') as f:
user_config = json.load(f)
default_config.update(user_config)
except Exception as e:
print(f"Warning: Could not load config {config_file}: {e}")
return default_config
def cpu(self, interval: int = 1, per_cpu: bool = False) -> Dict:
"""Get CPU usage statistics.
Args:
interval: Sampling interval in seconds
per_cpu: Show per-CPU statistics
"""
if per_cpu:
cpu_percent = psutil.cpu_percent(interval=interval, percpu=True)
return {
"total_cpus": len(cpu_percent),
"per_cpu_usage": [f"CPU{i}: {usage:.1f}%"
for i, usage in enumerate(cpu_percent)],
"average": sum(cpu_percent) / len(cpu_percent)
}
else:
return {
"cpu_percent": psutil.cpu_percent(interval=interval),
"cpu_count": psutil.cpu_count(),
"cpu_freq": psutil.cpu_freq()._asdict() if psutil.cpu_freq() else None
}
def memory(self, format_bytes: bool = True) -> Dict:
"""Get memory usage statistics.
Args:
format_bytes: Format bytes in human-readable units
"""
mem = psutil.virtual_memory()
swap = psutil.swap_memory()
def format_size(bytes_val):
if not format_bytes:
return bytes_val
for unit in ['B', 'KB', 'MB', 'GB']:
if bytes_val < 1024.0:
return f"{bytes_val:.1f}{unit}"
bytes_val /= 1024.0
return f"{bytes_val:.1f}TB"
return {
"memory": {
"total": format_size(mem.total),
"available": format_size(mem.available),
"used": format_size(mem.used),
"percent": mem.percent
},
"swap": {
"total": format_size(swap.total),
"used": format_size(swap.used),
"percent": swap.percent
}
}
def disk(self, path: str = "/") -> Dict:
"""Get disk usage for specified path.
Args:
path: Path to check disk usage for
"""
try:
usage = psutil.disk_usage(path)
return {
"path": path,
"total": f"{usage.total / (1024**3):.1f}GB",
"used": f"{usage.used / (1024**3):.1f}GB",
"free": f"{usage.free / (1024**3):.1f}GB",
"percent": (usage.used / usage.total) * 100
}
except Exception as e:
return {"error": f"Could not get disk usage for {path}: {e}"}
def processes(self, top: int = 10, sort_by: str = "cpu") -> List[Dict]:
"""Get top processes by resource usage.
Args:
top: Number of top processes to show
sort_by: Sort by 'cpu', 'memory', or 'pid'
"""
processes = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
try:
processes.append(proc.info)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# Sort processes
reverse = True
if sort_by == "pid":
reverse = False
key_map = {
"cpu": "cpu_percent",
"memory": "memory_percent",
"pid": "pid"
}
if sort_by in key_map:
processes.sort(key=lambda x: x[key_map[sort_by]] or 0, reverse=reverse)
return processes[:top]
def alert(self) -> Dict:
"""Check system metrics against configured thresholds."""
alerts = []
thresholds = self.config["alert_thresholds"]
# Check CPU
cpu_percent = psutil.cpu_percent(interval=1)
if cpu_percent > thresholds["cpu_percent"]:
alerts.append(f"HIGH CPU: {cpu_percent:.1f}% (threshold: {thresholds['cpu_percent']}%)")
# Check Memory
memory_percent = psutil.virtual_memory().percent
if memory_percent > thresholds["memory_percent"]:
alerts.append(f"HIGH MEMORY: {memory_percent:.1f}% (threshold: {thresholds['memory_percent']}%)")
# Check Disk
disk_percent = (psutil.disk_usage("/").used / psutil.disk_usage("/").total) * 100
if disk_percent > thresholds["disk_percent"]:
alerts.append(f"HIGH DISK: {disk_percent:.1f}% (threshold: {thresholds['disk_percent']}%)")
return {
"timestamp": str(psutil.boot_time()),
"alerts": alerts,
"status": "OK" if not alerts else "ALERT"
}
def create_config_template(output_file: str = "sysmon_config.json"):
"""Create a configuration template file."""
template = {
"alert_thresholds": {
"cpu_percent": 80,
"memory_percent": 85,
"disk_percent": 90
},
"monitoring_interval": 5
}
with open(output_file, 'w') as f:
json.dump(template, f, indent=2)
return f"Configuration template created: {output_file}"
if __name__ == '__main__':
fire.Fire({
'monitor': SystemMonitor,
'create-config': create_config_template
})
Usage examples for the advanced CLI:
# Check CPU usage with per-core breakdown
./sysmonitor.py monitor cpu --per_cpu
# Get memory info with raw bytes
./sysmonitor.py monitor memory --format_bytes=False
# Show top 5 processes by memory usage
./sysmonitor.py monitor processes --top=5 --sort_by=memory
# Check for system alerts
./sysmonitor.py monitor alert
# Create configuration template
./sysmonitor.py create-config custom_config.json
# Use custom configuration
./sysmonitor.py monitor --config_file=custom_config.json alert
Comparison with Alternative CLI Frameworks
Feature | Python Fire | Click | Argparse | Typer |
---|---|---|---|---|
Setup Complexity | Minimal – One line | Medium – Decorators | High – Manual setup | Low – Type hints |
Type Conversion | Automatic | Manual specification | Manual specification | Automatic from hints |
Nested Commands | Natural (classes/modules) | Groups and commands | Subparsers | Classes and functions |
Help Generation | From docstrings | From decorators | Manual help text | From docstrings + hints |
Learning Curve | Very Low | Medium | High | Low |
Flexibility | Medium | High | Very High | High |
Best Practices and Common Pitfalls
Error Handling and Validation
Always validate inputs and provide meaningful error messages:
def process_file(filename: str, output_format: str = "json"):
"""Process a file with proper error handling."""
# Validate file exists
if not Path(filename).exists():
raise FileNotFoundError(f"File not found: {filename}")
# Validate format parameter
valid_formats = ["json", "yaml", "xml"]
if output_format not in valid_formats:
raise ValueError(f"Invalid format '{output_format}'. Must be one of: {valid_formats}")
try:
# Process file
return f"Processed {filename} as {output_format}"
except Exception as e:
raise RuntimeError(f"Processing failed: {str(e)}")
Documentation Best Practices
Use comprehensive docstrings with type information:
def backup_database(
database_name: str,
backup_path: str = "./backups",
compress: bool = True,
exclude_tables: List[str] = None
) -> Dict[str, str]:
"""Create a database backup with optional compression.
This function creates a backup of the specified database,
optionally compressing the output and excluding specific tables.
Args:
database_name: Name of the database to backup
backup_path: Directory to store backup files (default: ./backups)
compress: Whether to compress the backup file (default: True)
exclude_tables: List of table names to exclude from backup
Returns:
Dict containing backup status and file path
Example:
backup_database("mydb", compress=False, exclude_tables=["logs", "temp"])
"""
if exclude_tables is None:
exclude_tables = []
# Implementation here
return {
"status": "success",
"backup_file": f"{backup_path}/{database_name}_backup.sql"
}
Common Pitfalls to Avoid
- Mutable Default Arguments: Always use
None
for mutable defaults and initialize inside the function - Complex Return Types: Fire works best with simple return types. Use dictionaries for complex data
- Missing Type Hints: While not required, type hints improve CLI usability and help Fire with type conversion
- Poor Error Messages: Fire passes through Python exceptions, so make them user-friendly
- Overly Complex Classes: Keep CLI classes focused. Use composition for complex functionality
Integration with System Tools and Deployment
Create a proper package structure for your CLI tool:
mkdir -p mycli/{mycli,tests}
cd mycli
# Create setup.py
cat > setup.py << 'EOF'
from setuptools import setup, find_packages
setup(
name="mycli",
version="1.0.0",
packages=find_packages(),
install_requires=[
"fire>=0.4.0",
"psutil>=5.8.0",
],
entry_points={
'console_scripts': [
'mycli=mycli.main:main',
],
},
python_requires=">=3.8",
)
EOF
# Create main module
mkdir -p mycli
cat > mycli/main.py << 'EOF'
import fire
from .commands import SystemCommands
def main():
fire.Fire(SystemCommands)
if __name__ == '__main__':
main()
EOF
# Install in development mode
pip install -e .
For system-wide installation on Ubuntu 24:
# Build wheel
python setup.py bdist_wheel
# Install system-wide
sudo pip install dist/mycli-1.0.0-py3-none-any.whl
# Or create a .deb package
sudo apt install python3-stdeb dh-python
python setup.py --command-packages=stdeb.command bdist_deb
sudo dpkg -i deb_dist/python3-mycli_1.0.0-1_all.deb
Performance Considerations and Optimization
Fire adds minimal overhead, but you can optimize for better performance:
# Lazy loading for expensive imports
class OptimizedCLI:
def __init__(self):
self._heavy_module = None
@property
def heavy_module(self):
if self._heavy_module is None:
import heavy_computation_library
self._heavy_module = heavy_computation_library
return self._heavy_module
def process_data(self, data_file: str):
"""Process data using heavy computation library."""
return self.heavy_module.process(data_file)
# Use generators for large datasets
def process_large_file(filename: str, batch_size: int = 1000):
"""Process large files in batches."""
def batch_generator():
with open(filename) as f:
batch = []
for line in f:
batch.append(line.strip())
if len(batch) >= batch_size:
yield batch
batch = []
if batch:
yield batch
results = []
for batch in batch_generator():
# Process batch
results.extend(f"Processed: {item}" for item in batch)
return f"Processed {len(results)} items from {filename}"
Python Fire excels at rapid prototyping and creating intuitive command-line interfaces with minimal boilerplate. While it may not offer the fine-grained control of argparse or the decorator elegance of Click, its automatic introspection and natural command structure make it perfect for internal tools, system administration scripts, and rapid CLI development. The examples and patterns shown here provide a solid foundation for building production-ready command-line applications on Ubuntu 24.
For more advanced usage and complete documentation, check out the official Python Fire repository and the Python Fire documentation.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.