143. Argparse (App V0.5)
RAM Manager v0.5 — proper CLI with argparse
143. Argparse (App V0.5)
He adds command-line flags so he doesn’t have to edit the code every time.
🤔 Socratic question: How does
git commit -m "my message"know what-mmeans? How doesnpm install --save-devknow the difference fromnpm install? You’re about to learn the exact same mechanism.
🏛️ History: The Unix pipe
|was invented by Doug McIlroy at Bell Labs in 1964. The idea: make every program do one thing, and make them composable.ps aux | grep chrome | awk '{print $2}'— three programs, one pipeline. Every CLI tool ever built follows this philosophy. You’re joining a 60-year tradition.
python ram_manager.py --threshold 90 --top-n 15 --save
python ram_manager.py --verboseHe learns argparse:
import argparse
parser = argparse.ArgumentParser(description='Monitor RAM usage')
parser.add_argument('--threshold', type=int, default=80,
help='Alert when RAM exceeds this %%')
parser.add_argument('--top-n', type=int, default=10,
help='Number of processes to show')
parser.add_argument('--save', action='store_true',
help='Save report to disk')
args = parser.parse_args()Three things catch him out:
- Missing
type=int— without it,--threshold 90gives the string'90', not the integer90. Comparison'90' > 80always returnsTrue(string comparison), silently wrong. action='store_true'notstore_false—store_truesets the flag toTruewhen present,Falsewhen absent. That’s “enable verbose mode.”- Positional vs optional —
--threshold(optional, pass it or not) vsthreshold(positional, required). Flags use--.
💡 Real-world:
git,npm,pip,docker,kubectl,ffmpeg,curl— every single one of these tools uses argparse-style argument parsing. When you rungit commit -m "fix", git is parsing-mthe same way you just learned. You now understand how the tools you use every day are built.
💡 Fun fact: argparse replaced the older optparse module in Python 2.7 (2010). Before optparse, developers parsed sys.argv by hand — a single mistake meant a crash on any unexpected input. argparse also gave Python the automatic --help flag: your parser documents itself for free without any extra code.
⚠️ Watch out: Forgetting type=int causes silent bugs that are hard to trace. --threshold 90 stores '90' (a string). Then snapshot['percent'] > args.threshold does 72.5 > '90' which raises TypeError in Python 3 — but before that, '90' > '80' (if you compared strings) is alphabetically correct by accident. Always declare type=int for numeric args.
🤔 Think about it: The solution uses --no-save with action='store_true' rather than --save. Why is “opt out of saving” better than “opt in to saving” as the default? How does this match the principle of least surprise for a monitoring tool that users run frequently?
⌨️ Aryan is tired of editing the source code every time he wants to change the threshold or output directory. He adds argparse but mixes up positional vs optional arguments, forgets type=int, and doesn’t thread the parsed args through to his functions.
Learning objectives
- Use – options (not positional args) for optional configuration
- Add type=int for integer arguments
- Use action=‘store_true’ for boolean flags
- Thread parsed args to functions instead of using defaults
Key concepts
- add_argument(‘–name’, type=int, default=N) — optional with default
- action=‘store_true’ — boolean flag (True when present)
- type=Path — auto-convert string to pathlib.Path
- args.top_n — access parsed value (hyphen becomes underscore)
Try it
Concept detail
App v0.5 — argparse CLI
Now users can configure the RAM manager without editing code:
# Run with defaults
python ram_manager.py
# Custom threshold and top-N
python ram_manager.py --threshold 70 --top-n 5
# Don't save report
python ram_manager.py --no-save
# Custom report directory
python ram_manager.py --output-dir /tmp/reports
# Show help
python ram_manager.py --helpusage: ram_manager.py [-h] [--threshold THRESHOLD] [--top-n TOP_N]
[--no-save] [--output-dir OUTPUT_DIR]
RAM Manager — monitor memory and alert on high usage
options:
-h, --help show this help message and exit
--threshold RAM % alert threshold (default: 80)
--top-n Number of top processes (default: 10)
--no-save Skip saving report to disk
--output-dir Directory for reportsSolution
import argparse
import psutil
import json
import csv
from pathlib import Path
from datetime import datetime
DEFAULT_THRESHOLD = 80
DEFAULT_TOP_N = 10
REPORT_DIR = Path.home() / '.ram_manager' / 'reports'
def take_snapshot(n: int = DEFAULT_TOP_N) -> dict:
mem = psutil.virtual_memory()
processes = []
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
try:
rss_mb = proc.info['memory_info'].rss / 1e6
processes.append({'name': proc.info['name'], 'pid': proc.info['pid'], 'rss_mb': rss_mb})
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return {
'percent': mem.percent,
'used_gb': mem.used / 1e9,
'total_gb': mem.total / 1e9,
'processes': sorted(processes, key=lambda p: p['rss_mb'], reverse=True)[:n],
'timestamp': datetime.now().isoformat(),
}
def save_report(snapshot: dict, report_dir: Path = REPORT_DIR) -> tuple[Path, Path]:
report_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
json_path = report_dir / f'report_{ts}.json'
with open(json_path, 'w') as f:
json.dump(snapshot, f, indent=2)
csv_path = report_dir / f'report_{ts}.csv'
if snapshot['processes']:
with open(csv_path, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['name', 'pid', 'rss_mb'])
writer.writeheader()
writer.writerows(snapshot['processes'])
return json_path, csv_path
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description='RAM Manager — monitor memory and alert on high usage'
)
parser.add_argument(
'--threshold', type=int, default=DEFAULT_THRESHOLD,
help=f'RAM %% alert threshold (default: {DEFAULT_THRESHOLD})'
)
parser.add_argument(
'--top-n', type=int, default=DEFAULT_TOP_N,
help=f'Number of top processes to show (default: {DEFAULT_TOP_N})'
)
parser.add_argument(
'--no-save', action='store_true', default=False,
help='Skip saving report to disk'
)
parser.add_argument(
'--output-dir', type=Path, default=REPORT_DIR,
help=f'Directory for reports (default: {REPORT_DIR})'
)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
snapshot = take_snapshot(n=args.top_n)
print(f"RAM: {snapshot['percent']:.1f}% "
f"({snapshot['used_gb']:.1f}/{snapshot['total_gb']:.1f} GB)")
if snapshot['percent'] > args.threshold:
print(f'⚠️ RAM above {args.threshold}% — top processes:')
for p in snapshot['processes'][:5]:
print(f" {p['name']:<20} {p['rss_mb']:.0f} MB")
if not args.no_save:
json_path, csv_path = save_report(snapshot, args.output_dir)
print(f'Saved: {json_path.name}')
if __name__ == '__main__':
main()Tests
from pathlib import Path
def test_threshold_default():
parser = build_parser()
args = parser.parse_args([])
assert args.threshold == 80
def test_threshold_is_int():
parser = build_parser()
args = parser.parse_args(['--threshold', '70'])
assert isinstance(args.threshold, int)
assert args.threshold == 70
def test_top_n_is_int():
parser = build_parser()
args = parser.parse_args(['--top-n', '5'])
assert isinstance(args.top_n, int)
assert args.top_n == 5
def test_no_save_default_false():
parser = build_parser()
args = parser.parse_args([])
assert args.no_save is False
def test_no_save_flag():
parser = build_parser()
args = parser.parse_args(['--no-save'])
assert args.no_save is True
def test_threshold_not_positional():
parser = build_parser()
args = parser.parse_args([])
assert args.threshold == 80, '--threshold must be optional with a default'
def test_output_dir_is_path():
parser = build_parser()
args = parser.parse_args(['--output-dir', '/tmp'])
assert isinstance(args.output_dir, Path)