Skip to content

CLI Python avec Click : Guide Complet pour Applications Pro

Click révolutionne la création d’applications en ligne de commande (CLI) en Python. Développé par l’équipe derrière Flask, ce framework transforme des scripts Python simples en interfaces CLI robustes et intuitives en quelques lignes seulement.

Pourquoi choisir Click ?

  • ✅ Syntaxe basée sur les décorateurs (propre et lisible)
  • ✅ Gestion automatique des messages d’aide
  • ✅ Validation intégrée des arguments
  • ✅ Support des couleurs et styles
  • ✅ Testing simplifié avec CliRunner

Dans ce guide, vous apprendrez à :

  • Créer vos premières commandes CLI
  • Gérer arguments et options efficacement
  • Structurer des CLI complexes avec des groupes
  • Valider et sécuriser les entrées utilisateur
  • Tester professionnellement vos applications

Terminal window
pip install click

Vérification de l’installation :

Terminal window
python -c "import click; print(f'Click version: {click.__version__}')"
hello.py
import click
@click.command()
@click.option('--name', default='Monde', help='Nom à saluer')
def hello(name):
"""Commande simple qui salue une personne."""
click.echo(f"Bonjour, {name}!")

Exécution :

Terminal window
python hello.py --name "Riyad"
# Sortie : Bonjour, Riyad!
python hello.py
# Sortie : Bonjour, Monde!

📝 Note importante :

  • @click.command() transforme une fonction en commande CLI
  • @click.option() définit une option facultative
  • click.echo() est préférable à print() pour la compatibilité

🛠️ 2. Arguments vs Options : La Différence Essentielle

Section titled “🛠️ 2. Arguments vs Options : La Différence Essentielle”
greet.py
@click.command()
@click.argument('prenom')
@click.argument('nom')
def saluer(prenom, nom):
"""Salue une personne avec son prénom et nom."""
click.echo(f"Bienvenue {prenom} {nom}!")

Utilisation :

Terminal window
python greet.py Riyad Odjouade
# Sortie : Bienvenue Riyad Odjouade!

❌ Sans arguments :

Terminal window
python greet.py
# Click affiche automatiquement : Error: Missing argument 'PRENOM'
@click.command()
@click.option('--verbose', '-v', is_flag=True, help='Mode verbeux')
@click.option('--count', '-c', default=1, type=int, help='Nombre de répétitions')
def repeat(verbose, count):
"""Répète un message."""
if verbose:
click.echo("Mode verbeux activé")
for i in range(count):
click.echo(f"Message {i+1}")

Exemples d’utilisation :

Terminal window
python script.py --verbose --count 3
python script.py -v -c 3
python script.py # Utilise les valeurs par défaut

📊 Tableau comparatif :

CaractéristiqueArgumentOption
Obligatoire✅ Oui❌ Non
PositionFixeN’importe où
Syntaxevaleur--nom valeur
Valeur par défautNonOui
AbréviationNon-n pour --nom

🏗️ 3. Groupes de Commandes : Architecture Pro

Section titled “🏗️ 3. Groupes de Commandes : Architecture Pro”
cli.py
import click
@click.group()
def monapp():
"""Application de gestion de projet."""
pass
@monapp.command()
def init():
"""Initialise un nouveau projet."""
click.echo("Projet initialisé")
@monapp.command()
@click.option('--env', default='dev', help='Environnement')
def deploy(env):
"""Déploie le projet."""
click.echo(f"Déploiement sur {env}")

Arborescence créée :

monapp
├── init
└── deploy --env [prod|dev|test]
@click.group()
def db():
"""Commandes de base de données."""
pass
@db.command()
def migrate():
"""Applique les migrations."""
click.echo("Migrations appliquées")
@db.command()
def seed():
"""Remplit la base avec des données."""
click.echo("Base peuplée")
# Ajout au groupe principal
monapp.add_command(db)

Utilisation :

Terminal window
python cli.py db migrate
python cli.py db seed

🛡️ 4. Validation Avancée des Entrées

Section titled “🛡️ 4. Validation Avancée des Entrées”
@click.command()
@click.option('--age', type=click.IntRange(0, 120))
@click.option('--score', type=click.FloatRange(0.0, 100.0))
@click.option('--file', type=click.Path(exists=True))
def validate(age, score, file):
"""Exemple de validation multiple."""
click.echo(f"Âge: {age}, Score: {score}, Fichier: {file}")
@click.command()
@click.option('--role',
type=click.Choice(['admin', 'user', 'guest']),
prompt='Sélectionnez un rôle')
def set_role(role):
"""Définit le rôle utilisateur."""
click.echo(f"Rôle défini: {role}")
@click.command()
@click.argument('input', type=click.File('r'))
@click.argument('output', type=click.File('w'))
def process(input, output):
"""Traite un fichier."""
content = input.read()
output.write(content.upper())
click.echo("Fichier traité avec succès")
def validate_email(ctx, param, value):
"""Valide le format d'email."""
if '@' not in value:
raise click.BadParameter("Email invalide")
return value
@click.command()
@click.option('--email', callback=validate_email)
def register(email):
"""Enregistre un utilisateur."""
click.echo(f"Utilisateur {email} enregistré")

@click.command()
def status():
"""Affiche le statut avec style."""
click.secho("✓ Succès", fg="green", bold=True)
click.secho("⚠ Avertissement", fg="yellow")
click.secho("✗ Erreur", fg="red")
click.secho("ℹ Information", fg="blue")

Couleurs disponibles :

  • black, red, green, yellow
  • blue, magenta, cyan, white
  • bright_* variants
import time
@click.command()
def process_files():
"""Traite des fichiers avec barre de progression."""
files = list(range(100))
with click.progressbar(files, label='Traitement') as bar:
for file in bar:
time.sleep(0.01) # Simulation traitement
@click.command()
def configure():
"""Configuration interactive."""
# Demande simple
nom = click.prompt('Entrez votre nom', type=str)
# Mot de passe masqué
mdp = click.prompt('Mot de passe', hide_input=True)
# Confirmation
if click.confirm('Confirmer la configuration?'):
click.echo("Configuration sauvegardée")
else:
click.echo("Annulé")
# Choix multiple
lang = click.prompt(
'Langage préféré',
type=click.Choice(['Python', 'JavaScript', 'Rust'])
)

🧪 6. Testing Professionnel avec CliRunner

Section titled “🧪 6. Testing Professionnel avec CliRunner”
test_cli.py
import click
from click.testing import CliRunner
@click.command()
@click.option('--name', default='World')
def hello(name):
click.echo(f"Hello {name}")
# Tests
def test_hello_default():
runner = CliRunner()
result = runner.invoke(hello)
assert result.exit_code == 0
assert "Hello World" in result.output
def test_hello_name():
runner = CliRunner()
result = runner.invoke(hello, ['--name', 'Riyad'])
assert result.exit_code == 0
assert "Hello Riyad" in result.output
def test_validation_error():
@click.command()
@click.option('--age', type=int)
def check_age(age):
if age < 18:
raise click.UsageError("Âge minimum: 18")
runner = CliRunner()
result = runner.invoke(check_age, ['--age', '16'])
assert result.exit_code != 0
assert "Âge minimum" in result.output
def test_prompt():
@click.command()
def ask_name():
name = click.prompt('Nom')
click.echo(f"Bonjour {name}")
runner = CliRunner()
result = runner.invoke(ask_name, input='Riyad\n')
assert "Bonjour Riyad" in result.output
class TestCLI:
def setup_method(self):
self.runner = CliRunner()
def test_success_flow(self):
result = self.runner.invoke(main_command, ['arg1', '--opt', 'val'])
assert result.exit_code == 0
def test_error_flow(self):
result = self.runner.invoke(main_command, ['invalid'])
assert result.exit_code == 2 # Code d'erreur Click
def test_help(self):
result = self.runner.invoke(main_command, ['--help'])
assert "Usage:" in result.output

📁 7. Structure de Projet Professionnelle

Section titled “📁 7. Structure de Projet Professionnelle”
mon-cli-app/
├── cli/
│ ├── __init__.py
│ ├── main.py # Point d'entrée
│ ├── commands/
│ │ ├── __init__.py
│ │ ├── db.py # Commandes base de données
│ │ └── deploy.py # Commandes déploiement
│ └── utils.py # Utilitaires partagés
├── tests/
│ ├── __init__.py
│ ├── test_main.py
│ └── test_commands/
├── pyproject.toml # Dépendances
├── README.md
└── setup.py
cli/main.py
import click
from .commands import db, deploy
@click.group()
def cli():
"""Mon application CLI professionnelle."""
pass
# Ajout des groupes de commandes
cli.add_command(db.group)
cli.add_command(deploy.group)
if __name__ == '__main__':
cli()
cli/commands/db.py
import click
@click.group()
def group():
"""Commandes de gestion de base de données."""
pass
@group.command()
@click.option('--backup/--no-backup', default=True)
def migrate(backup):
"""Applique les migrations."""
if backup:
click.echo("Backup créé")
click.echo("Migrations appliquées")
setup.py
from setuptools import setup, find_packages
setup(
name="mon-cli-app",
version="1.0.0",
packages=find_packages(),
install_requires=["click>=8.0.0"],
entry_points={
'console_scripts': [
'monapp=cli.main:cli',
],
},
)

Installation :

Terminal window
pip install -e .
monapp --help # Disponible globalement

@click.command()
@click.option('--host', default='localhost', show_default=True)
@click.option('--port', default=8080, show_default=True)
def server(host, port):
"""Démarre le serveur.
Exemples:
server --host 0.0.0.0 --port 9000
"""
click.echo(f"Serveur démarré sur {host}:{port}")
@click.command()
@click.option('--api-key',
envvar='API_KEY',
help='Clé API (variable: API_KEY)')
def call_api(api_key):
"""Appelle l'API externe."""
click.echo(f"Clé API: {api_key}")
def validate_db(ctx, param, value):
"""Valide la connexion DB avant exécution."""
if not check_database_connection(value):
raise click.BadParameter("DB inaccessible")
return value
@click.command()
@click.option('--db-url', callback=validate_db)
def backup(db_url):
"""Crée un backup de la base."""
click.echo("Backup réussi")
import logging
@click.command()
@click.option('--verbose', '-v', count=True)
def log_demo(verbose):
"""Démonstration de logging."""
level = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}[verbose]
logging.basicConfig(level=level)
logging.debug("Message debug")
logging.info("Message info")
logging.warning("Message warning")
CritèreargparseclickRecommandation
Courbe d’apprentissageRaideDouce✅ Click
Code boilerplateBeaucoupMinimal✅ Click
ValidationManuelIntégrée✅ Click
TestingComplexeSimple (CliRunner)✅ Click
DocumentationManuelAuto-générée✅ Click
CommunautéStandardLarge (Pallets)✅ Click

Testez votre maîtrise de Click en 3 minutes. Seuil de réussite : 80%.

À vous de jouer ! 🚀



Exercice 1 : Créer un Gestionnaire de Tâches CLI

Énoncé : Créez une CLI task avec les commandes :

  • add : ajoute une tâche (option --priority)
  • list : liste toutes les tâches
  • done : marque une tâche comme terminée

Template :

import click
@click.group()
def task():
"""Gestionnaire de tâches."""
pass
# Votre code ici...
Solution
tasks = []
@click.group()
def task():
"""Gestionnaire de tâches."""
pass
@task.command()
@click.argument('description')
@click.option('--priority', default='medium',
type=click.Choice(['low', 'medium', 'high']))
def add(description, priority):
"""Ajoute une nouvelle tâche."""
task = {
'id': len(tasks) + 1,
'description': description,
'priority': priority,
'done': False
}
tasks.append(task)
click.secho(f"✓ Tâche ajoutée (ID: {task['id']})", fg='green')
@task.command()
def list():
"""Liste toutes les tâches."""
if not tasks:
click.echo("Aucune tâche.")
return
for t in tasks:
status = "" if t['done'] else ""
color = 'green' if t['done'] else 'yellow'
click.secho(f"{t['id']}. [{status}] {t['description']} ({t['priority']})",
fg=color)
@task.command()
@click.argument('task_id', type=int)
def done(task_id):
"""Marque une tâche comme terminée."""
for task in tasks:
if task['id'] == task_id:
task['done'] = True
click.secho(f"Tâche {task_id} terminée !", fg='green')
return
click.secho(f"Tâche {task_id} introuvable", fg='red')
if __name__ == '__main__':
task()

Tests :

Terminal window
python tasks.py add "Apprendre Click" --priority high
python tasks.py list
python tasks.py done 1

Exercice 2 : Convertisseur Fichier JSON → YAML

Énoncé : Créez un convertisseur CLI qui :

  • Prend un fichier JSON en entrée
  • Convertit en YAML
  • Sauvegarde ou affiche le résultat

Exigences :

  • Validation du fichier d’entrée
  • Option --output pour sauvegarder
  • Option --indent pour l’indentation
Solution
import json
import click
import yaml
@click.command()
@click.argument('input_file', type=click.Path(exists=True, dir_okay=False))
@click.option('--output', '-o', type=click.Path(writable=True))
@click.option('--indent', default=2, help='Indentation YAML')
def json2yaml(input_file, output, indent):
"""Convertit JSON vers YAML."""
# Lecture JSON
try:
with open(input_file, 'r') as f:
data = json.load(f)
except json.JSONDecodeError:
raise click.BadParameter("Fichier JSON invalide")
# Conversion YAML
yaml_data = yaml.dump(data, indent=indent, default_flow_style=False)
# Sortie
if output:
with open(output, 'w') as f:
f.write(yaml_data)
click.secho(f"✓ Converti vers {output}", fg='green')
else:
click.echo(yaml_data)
if __name__ == '__main__':
json2yaml()

Utilisation :

Terminal window
python converter.py data.json --output data.yaml --indent 4
python converter.py data.json # Affiche à l'écran

Exercice 3 : API Client avec Authentification

Énoncé : Créez un client CLI pour une API REST avec :

  • Authentification via token
  • Commandes get, post, delete
  • Gestion des erreurs HTTP
Solution
import click
import requests
@click.group()
@click.option('--token', envvar='API_TOKEN', required=True)
@click.pass_context
def api(ctx, token):
"""Client API REST."""
ctx.ensure_object(dict)
ctx.obj['TOKEN'] = token
ctx.obj['BASE_URL'] = 'https://api.example.com'
def make_request(ctx, method, endpoint, **kwargs):
"""Fait une requête authentifiée."""
headers = {'Authorization': f'Bearer {ctx.obj["TOKEN"]}'}
url = f"{ctx.obj['BASE_URL']}/{endpoint}"
try:
response = requests.request(method, url, headers=headers, **kwargs)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
click.secho(f"Erreur: {e}", fg='red')
ctx.exit(1)
@api.command()
@click.argument('resource')
@click.pass_context
def get(ctx, resource):
"""Récupère une ressource."""
data = make_request(ctx, 'GET', resource)
click.echo(json.dumps(data, indent=2))
@api.command()
@click.argument('resource')
@click.option('--data', required=True)
@click.pass_context
def post(ctx, resource, data):
"""Crée une ressource."""
try:
json_data = json.loads(data)
except json.JSONDecodeError:
raise click.BadParameter("JSON invalide")
result = make_request(ctx, 'POST', resource, json=json_data)
click.secho("✓ Créé avec succès", fg='green')
click.echo(json.dumps(result, indent=2))
if __name__ == '__main__':
api()

Exercice 4 : Monitoring System avec Barre de Progression

Énoncé : Créez un outil de monitoring qui :

  • Scanne des serveurs en parallèle
  • Affiche une barre de progression
  • Sort les résultats dans un tableau
Solution
import click
import time
import concurrent.futures
servers = [
'server1.example.com',
'server2.example.com',
'server3.example.com'
]
def check_server(server):
"""Simule une vérification de serveur."""
time.sleep(0.5) # Simulation
return {
'server': server,
'status': 'up' if hash(server) % 3 != 0 else 'down',
'response_time': hash(server) % 100
}
@click.command()
@click.option('--timeout', default=10, help='Timeout par serveur')
def monitor(timeout):
"""Monitore plusieurs serveurs."""
click.secho("🔍 Scan des serveurs...", bold=True)
with concurrent.futures.ThreadPoolExecutor() as executor:
# Barre de progression
with click.progressbar(
executor.map(check_server, servers),
length=len(servers),
label='Vérification'
) as results:
data = list(results)
# Affichage tableau
click.echo("\n" + "="*50)
click.secho(f"{'Serveur':<25} {'Statut':<10} {'Temps (ms)':>10}", bold=True)
click.echo("-"*50)
for server_info in data:
color = 'green' if server_info['status'] == 'up' else 'red'
status_icon = '' if server_info['status'] == 'up' else ''
click.secho(
f"{server_info['server']:<25} "
f"{status_icon} {server_info['status']:<7} "
f"{server_info['response_time']:>10}",
fg=color
)


À propos de l'auteur

Riyad ODJOUADEExpert en cybersécurité & infrastructure

Je partage des guides techniques pratiques sur la cybersécurité, l'administration système et le développement web. Tous les contenus sont basés sur des expérimentations réelles.

Dernière mise à jour : 07 Janvier 2026