Building a Python CLI Tool
Learn how to build a CLI utility with argparse, structure your project, and package it.
Project-based tutorials are the core of SkillByExample. In this tutorial, we will build a command-line tool that fetches weather forecasts for a city.
Project Structure
Organizing code into packages from the start pays off quickly. Keeping all source files under a named package directory (here, weather/) separates your importable code from project-level files like setup.py and requirements.txt. This structure also makes the project installable as a proper Python package, which is what enables the entry_points trick that turns your script into a system-wide command.
weather-cli/
├── weather/
│ ├── __init__.py # marks this directory as a Python package
│ ├── api.py # HTTP logic lives here, separate from CLI logic
│ └── main.py # argument parsing and entry point
├── setup.py # packaging metadata and install configuration
└── requirements.txt # pinned third-party dependencies
Writing the CLI Logic
argparse is Python’s built-in library for defining command-line interfaces. It handles argument parsing, type coercion, and --help generation automatically — you describe what arguments your tool accepts and argparse takes care of validating user input and printing usage errors. Separating the CLI layer (main.py) from the API layer (api.py) keeps each file focused and makes the API logic independently testable without invoking the CLI.
# weather/main.py
import argparse
import sys
from weather.api import fetch_weather
def main():
# ArgumentParser generates --help text automatically from description and help= strings
parser = argparse.ArgumentParser(description="Fetch current weather details.")
parser.add_argument(
"city",
type=str,
help="Name of the city to query" # shown in --help output
)
parser.add_argument(
"--metric",
action="store_true", # flag — True if present, False if absent
help="Use metric scale instead of imperial"
)
args = parser.parse_args() # exits with a usage error if required args are missing
try:
data = fetch_weather(args.city, args.metric)
print(f"Weather in {args.city.capitalize()}: {data['temp']}°")
except Exception as e:
# Print errors to stderr so they don't pollute piped output
print(f"Error: {e}", file=sys.stderr)
sys.exit(1) # non-zero exit code signals failure to the shell
if __name__ == "__main__":
main()
Packaging the Application
setup.py tells Python’s packaging tools everything they need to install your project: its name, version, dependencies, and — critically — the entry_points mapping. The console_scripts entry point instructs the installer to create a weather executable on the system PATH that calls weather.main:main. This means users run weather london instead of python -m weather.main london, which is the difference between a script and a proper tool.
# setup.py
from setuptools import setup, find_packages
setup(
name="weather-cli",
version="0.1.0",
packages=find_packages(), # automatically discovers the weather/ package
install_requires=[
"requests>=2.25.1" # declared dependency — installed automatically
],
entry_points={
"console_scripts": [
# format: "command-name=package.module:function"
"weather=weather.main:main",
],
},
)
Running pip install -e . inside the project root installs the package in editable mode — the weather command becomes available system-wide, and any changes you make to the source files take effect immediately without reinstalling.