Skip to content
Thèmes
Python
Python Argparse: Build Command-Line Interfaces the Right Way

Python Argparse : bien concevoir des interfaces en ligne de commande

Mis à jour le

Vous avez écrit un script Python qui traite des données exactement comme vous en avez besoin. Puis votre collègue vous demande de l’utiliser. « Il suffit de changer le nom du fichier à la ligne 14 et le seuil à la ligne 37 », lui dites-vous. Il modifie la mauvaise ligne. Le script casse. Vous passez 20 minutes à déboguer la modification de quelqu’un d’autre sur votre code qui fonctionnait. Cela se produit à chaque fois que quelqu’un doit ajuster un paramètre, et cela empire à mesure que le script grossit. Les valeurs codées en dur dans les scripts ne passent pas à l’échelle — ni pour les équipes, ni pour l’automatisation, ni pour la production.

Le module argparse de Python corrige cela en transformant n’importe quel script en véritable outil en ligne de commande. Les utilisateurs passent des arguments lorsqu’ils lancent le script. Le module gère l’analyse, la conversion de types, la validation et la génération des messages d’aide. Il fait partie de la bibliothèque standard, donc rien n’est à installer. Ce guide parcourt toutes les fonctionnalités d’argparse dont vous avez besoin — des arguments positionnels de base aux sous-commandes et aux modèles de conception CLI du monde réel.

Ce que fait argparse et pourquoi il surpasse sys.argv

Chaque script Python a accès à sys.argv, une liste brute de chaînes provenant de la ligne de commande. Vous pouvez l’analyser manuellement :

import sys
 
filename = sys.argv[1]
threshold = float(sys.argv[2])
verbose = "--verbose" in sys.argv

Cela fonctionne pour des scripts jetables, mais se dégrade vite. Il n’y a pas de messages d’aide. Pas de validation de type. Pas de message d’erreur lorsque les utilisateurs oublient un argument. Aucun moyen de gérer correctement les indicateurs optionnels. L’indexation casse dès que vous ajoutez ou supprimez un paramètre.

argparse résout tous ces problèmes avec une API déclarative. Vous définissez les arguments acceptés par votre script, et argparse s’occupe du reste :

import argparse
 
parser = argparse.ArgumentParser(description="Process data files")
parser.add_argument("filename", help="Input file to process")
parser.add_argument("--threshold", type=float, default=0.5, help="Detection threshold")
parser.add_argument("--verbose", action="store_true", help="Enable detailed output")
args = parser.parse_args()
 
print(f"Processing {args.filename} with threshold {args.threshold}")

Ce que vous obtenez gratuitement :

  • --help automatique : exécutez python script.py --help et les utilisateurs voient chaque argument, son type et sa valeur par défaut.
  • Conversion de type : --threshold 0.8 devient automatiquement un float. Si quelqu’un passe --threshold abc, argparse affiche une erreur claire.
  • Validation : les arguments requis manquants produisent des messages utiles au lieu d’un traceback IndexError cryptique.
  • Ordre flexible : les arguments optionnels peuvent apparaître dans n’importe quel ordre. --verbose --threshold 0.8 fonctionne comme --threshold 0.8 --verbose.

Voici à quoi ressemble la sortie d’aide :

$ python script.py --help
usage: script.py [-h] [--threshold THRESHOLD] [--verbose] filename
 
Process data files
 
positional arguments:
  filename              Input file to process
 
options:
  -h, --help            show this help message and exit
  --threshold THRESHOLD Detection threshold
  --verbose             Enable detailed output

Votre premier script CLI

Construisons un script complet fonctionnel à partir de zéro. Cet outil salue un utilisateur par son nom, avec un nombre optionnel de répétitions du message.

#!/usr/bin/env python3
"""greet.py -- A simple greeting CLI tool."""
 
import argparse
 
def main():
    parser = argparse.ArgumentParser(
        prog="greet",
        description="Greet someone by name",
    )
    parser.add_argument("name", help="The person to greet")
    parser.add_argument(
        "-c", "--count",
        type=int,
        default=1,
        help="Number of times to greet (default: 1)",
    )
    parser.add_argument(
        "-u", "--uppercase",
        action="store_true",
        help="Print greeting in uppercase",
    )
 
    args = parser.parse_args()
 
    greeting = f"Hello, {args.name}!"
    if args.uppercase:
        greeting = greeting.upper()
 
    for _ in range(args.count):
        print(greeting)
 
if __name__ == "__main__":
    main()

Testez-le depuis le terminal :

$ python greet.py Alice
Hello, Alice!
 
$ python greet.py Bob --count 3
Hello, Bob!
Hello, Bob!
Hello, Bob!
 
$ python greet.py Charlie -c 2 -u
HELLO, CHARLIE!
HELLO, CHARLIE!
 
$ python greet.py
usage: greet [-h] [-c COUNT] [-u] name
greet: error: the following arguments are required: name

Remarquez trois choses dans ce script :

  1. La logique d’analyse vit dans main(), pas au niveau du module. Cela rend le script importable sans déclencher l’analyse des arguments.
  2. La garde if __name__ == "__main__" garantit que main() ne s’exécute que lorsque le script est lancé directement, et non lors d’un import.
  3. Les drapeaux courts et longs (-c et --count) offrent aux utilisateurs le choix entre concision et clarté.

Ces trois modèles apparaissent dans chaque script argparse bien écrit. Adoptez-les dès le départ.

Arguments positionnels

Les arguments positionnels sont définis sans tirets. Ils sont requis par défaut et sont interprétés selon leur position sur la ligne de commande.

import argparse
 
parser = argparse.ArgumentParser(description="Copy files")
parser.add_argument("source", help="Source file path")
parser.add_argument("destination", help="Destination file path")
args = parser.parse_args()
 
print(f"Copying {args.source} -> {args.destination}")
$ python copy.py report.csv /backup/report.csv
Copying report.csv -> /backup/report.csv

Plusieurs valeurs positionnelles avec nargs

Le paramètre nargs contrôle combien de valeurs un argument consomme :

import argparse
 
parser = argparse.ArgumentParser(description="Merge files")
 
# One or more files (at least one required)
parser.add_argument("files", nargs="+", help="Files to merge")
 
# Exactly two values
parser.add_argument("--range", nargs=2, type=int, metavar=("START", "END"),
                    help="Row range to extract")
 
args = parser.parse_args()
print(f"Merging: {args.files}")
if args.range:
    print(f"Range: {args.range[0]} to {args.range[1]}")
$ python merge.py data1.csv data2.csv data3.csv --range 10 500
Merging: ['data1.csv', 'data2.csv', 'data3.csv']
Range: 10 to 500

Voici la référence complète de nargs :

nargs valueMeaningResult typeExample
(omitted)Exactly one valueSingle value"input.csv"
N (integer)Exactly N valuesList of N items[10, 100]
"?"Zero or one valueSingle value or default"config.yml" or None
"*"Zero or more valuesList (possibly empty)[] or ["a", "b"]
"+"One or more valuesList (error if empty)["a"] or ["a", "b"]
argparse.REMAINDERAll remaining argsListEverything after this arg

Conversion de type sur les arguments positionnels

Les arguments positionnels sont des chaînes par défaut. Ajoutez type pour les convertir :

import argparse
 
parser = argparse.ArgumentParser(description="Calculate rectangle area")
parser.add_argument("width", type=float, help="Rectangle width")
parser.add_argument("height", type=float, help="Rectangle height")
args = parser.parse_args()
 
area = args.width * args.height
print(f"Area: {area:.2f}")
$ python area.py 3.5 7.2
Area: 25.20
 
$ python area.py three 7.2
usage: area.py [-h] width height
area.py: error: argument width: invalid float value: 'three'

Le message d’erreur est automatique et indique à l’utilisateur exactement ce qui n’a pas fonctionné.

Arguments optionnels

Les arguments optionnels commencent par - (forme courte) ou -- (forme longue). Ils sont optionnels par défaut et peuvent apparaître dans n’importe quel ordre.

import argparse
 
parser = argparse.ArgumentParser(description="Data exporter")
parser.add_argument("-o", "--output", default="output.csv", help="Output file path")
parser.add_argument("-d", "--delimiter", default=",", help="Field delimiter")
parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed progress")
parser.add_argument("-q", "--quiet", action="store_true", help="Suppress all output")
 
args = parser.parse_args()
print(f"Output: {args.output}")
print(f"Delimiter: {repr(args.delimiter)}")
print(f"Verbose: {args.verbose}")
$ python export.py --output results.tsv --delimiter "\t" --verbose
Output: results.tsv
Delimiter: '\\t'
Verbose: True
 
$ python export.py -o results.json -v
Output: results.json
Delimiter: ','
Verbose: True

Valeurs par défaut

Chaque argument optionnel a une valeur par défaut. Si vous n’en définissez pas explicitement, elle vaut None :

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--name", default="World")      # Default: "World"
parser.add_argument("--port", type=int, default=8080) # Default: 8080
parser.add_argument("--log-file")                     # Default: None
 
args = parser.parse_args()
print(f"Name: {args.name}, Port: {args.port}, Log: {args.log_file}")

Remarque : lorsqu’un nom d’argument utilise des tirets (--log-file), argparse les convertit en underscores pour le nom d’attribut : args.log_file.

Les actions store_true et store_false

Les drapeaux booléens ne prennent pas de valeur. Ils sont soit présents, soit absents :

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", action="store_true", help="Enable verbose mode")
parser.add_argument("--no-cache", action="store_true", help="Disable caching")
parser.add_argument("--dry-run", action="store_true", help="Preview without executing")
 
args = parser.parse_args()
# --verbose present -> args.verbose is True
# --verbose absent  -> args.verbose is False

L’action count pour les niveaux de verbosité

Certains outils utilisent des drapeaux répétés pour les niveaux de verbosité (-v, -vv, -vvv) :

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", action="count", default=0,
                    help="Increase verbosity (-v, -vv, -vvv)")
args = parser.parse_args()
 
if args.verbose >= 3:
    print("TRACE level: showing everything")
elif args.verbose >= 2:
    print("DEBUG level: detailed output")
elif args.verbose >= 1:
    print("INFO level: progress updates")
else:
    print("Default: errors only")
$ python tool.py -vvv
TRACE level: showing everything

L’action append pour des arguments répétés

Utilisez action="append" lorsque les utilisateurs doivent pouvoir spécifier plusieurs fois le même drapeau :

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--tag", action="append", default=[],
                    help="Add a tag (can be repeated)")
args = parser.parse_args()
print(f"Tags: {args.tag}")
$ python tool.py -t bug -t urgent -t backend
Tags: ['bug', 'urgent', 'backend']

Type et choices

Conversion de type intégrée

Le paramètre type accepte n’importe quel appelable qui prend une chaîne et renvoie une valeur. Vous pouvez utiliser les types intégrés, pathlib.Path pour les chemins de fichiers, ou des fonctions personnalisées :

import argparse
from pathlib import Path
 
parser = argparse.ArgumentParser()
parser.add_argument("--count", type=int, help="Number of items")
parser.add_argument("--rate", type=float, help="Processing rate")
parser.add_argument("--config", type=Path, help="Config file path")
args = parser.parse_args()

Restreindre les valeurs avec choices

Utilisez choices pour limiter un argument à des valeurs autorisées précises :

import argparse
 
parser = argparse.ArgumentParser(description="Deploy application")
parser.add_argument("environment", choices=["dev", "staging", "production"],
                    help="Target environment")
parser.add_argument("--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR"],
                    default="INFO", help="Logging level")
parser.add_argument("--replicas", type=int, choices=range(1, 11),
                    default=1, help="Number of replicas (1-10)")
args = parser.parse_args()
$ python deploy.py testing
usage: deploy.py [-h] {dev,staging,production}
deploy.py: error: argument environment: invalid choice: 'testing'
  (choose from 'dev', 'staging', 'production')

Fonctions de type personnalisées

Les fonctions de type personnalisées sont l’une des fonctionnalités les plus puissantes d’argparse. Elles permettent de valider et de transformer l’entrée au moment de l’analyse :

import argparse
from datetime import datetime
 
def positive_int(value):
    """Accept only positive integers."""
    ivalue = int(value)
    if ivalue <= 0:
        raise argparse.ArgumentTypeError(f"{value} is not a positive integer")
    return ivalue
 
def date_type(value):
    """Parse YYYY-MM-DD date strings."""
    try:
        return datetime.strptime(value, "%Y-%m-%d").date()
    except ValueError:
        raise argparse.ArgumentTypeError(
            f"Invalid date: '{value}'. Expected format: YYYY-MM-DD"
        )
 
def percentage(value):
    """Accept floats between 0 and 100."""
    fvalue = float(value)
    if not 0 <= fvalue <= 100:
        raise argparse.ArgumentTypeError(
            f"{value} is not a valid percentage (0-100)"
        )
    return fvalue
 
parser = argparse.ArgumentParser()
parser.add_argument("--workers", type=positive_int, default=4)
parser.add_argument("--since", type=date_type, help="Start date (YYYY-MM-DD)")
parser.add_argument("--sample", type=percentage, default=100.0,
                    help="Sample percentage (0-100)")
args = parser.parse_args()
$ python tool.py --workers -3
error: argument --workers: -3 is not a positive integer
 
$ python tool.py --since 2026-13-45
error: argument --since: Invalid date: '2026-13-45'. Expected format: YYYY-MM-DD
 
$ python tool.py --sample 150
error: argument --sample: 150 is not a valid percentage (0-100)

Chaque erreur de validation produit un message clair et exploitable, sans que vous ayez à écrire de logique if/else dans votre code principal.

Arguments optionnels requis

Vous pouvez forcer un argument optionnel à être requis :

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--config", required=True, help="Path to config file")
parser.add_argument("--output", required=True, help="Output directory")
parser.add_argument("--format", default="json", help="Output format")
args = parser.parse_args()
$ python tool.py --format csv
usage: tool.py [-h] --config CONFIG --output OUTPUT [--format FORMAT]
tool.py: error: the following arguments are required: --config, --output

Pourquoi c’est parfois une odeur de code : si chaque argument « optionnel » est en réalité obligatoire, vous devriez probablement utiliser des arguments positionnels à la place. Les optionnels requis sont utiles lorsque le nom du drapeau apporte de la clarté (--config est plus explicite qu’un simple chemin) ou lorsque vous avez de nombreux paramètres requis et que des drapeaux nommés évitent les erreurs d’ordre.

Un meilleur modèle pour des entrées réellement obligatoires est souvent un argument positionnel :

# Instead of --config (required optional)
parser.add_argument("config", help="Path to config file")
 
# Use required optionals when the name matters
parser.add_argument("--api-key", required=True, help="API authentication key")

Groupes mutuellement exclusifs

Parfois, des arguments entrent en conflit. Un script peut produire soit du JSON, soit du CSV, mais pas les deux à la fois. add_mutually_exclusive_group() impose cette contrainte :

import argparse
 
parser = argparse.ArgumentParser(description="Export data")
 
# Output format: pick exactly one
format_group = parser.add_mutually_exclusive_group(required=True)
format_group.add_argument("--json", action="store_true", help="Export as JSON")
format_group.add_argument("--csv", action="store_true", help="Export as CSV")
format_group.add_argument("--parquet", action="store_true", help="Export as Parquet")
 
# Verbosity: pick at most one
verbosity = parser.add_mutually_exclusive_group()
verbosity.add_argument("-v", "--verbose", action="store_true", help="Detailed output")
verbosity.add_argument("-q", "--quiet", action="store_true", help="Minimal output")
 
parser.add_argument("input_file", help="Input data file")
args = parser.parse_args()
 
if args.json:
    print(f"Exporting {args.input_file} as JSON")
elif args.csv:
    print(f"Exporting {args.input_file} as CSV")
elif args.parquet:
    print(f"Exporting {args.input_file} as Parquet")
$ python export.py data.csv --json --csv
error: argument --csv: not allowed with argument --json
 
$ python export.py data.csv
error: one of the arguments --json --csv --parquet is required
 
$ python export.py data.csv --parquet -v
Exporting data.csv as Parquet

Une alternative pratique pour choisir un format consiste à utiliser choices au lieu de drapeaux séparés :

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--format", choices=["json", "csv", "parquet"],
                    required=True, help="Output format")
args = parser.parse_args()

Les deux approches fonctionnent. Les groupes mutuellement exclusifs sont meilleurs lorsque chaque option a besoin de ses propres arguments supplémentaires. L’approche choices est meilleure lorsque les options sont de simples chaînes.

Sous-commandes avec add_subparsers

Les outils CLI réels utilisent des sous-commandes. Pensez à git commit, docker build, pip install. Chaque sous-commande a son propre ensemble d’arguments, son propre texte d’aide et sa propre fonction de traitement. argparse prend en charge ce modèle via add_subparsers() :

#!/usr/bin/env python3
"""project.py -- A project management CLI tool."""
 
import argparse
 
def handle_init(args):
    """Initialize a new project."""
    print(f"Creating project '{args.name}'")
    print(f"  Template: {args.template}")
    print(f"  Directory: {args.name}/")
 
def handle_build(args):
    """Build the project."""
    mode = "release" if args.optimize else "debug"
    print(f"Building in {mode} mode")
    if args.target:
        print(f"  Target: {args.target}")
 
def handle_test(args):
    """Run tests."""
    print(f"Running tests (coverage={'on' if args.coverage else 'off'})")
    if args.pattern:
        print(f"  Pattern: {args.pattern}")
 
def handle_deploy(args):
    """Deploy the project."""
    print(f"Deploying to {args.environment}")
    if args.dry_run:
        print("  (DRY RUN -- no actual deployment)")
 
def main():
    # Top-level parser
    parser = argparse.ArgumentParser(
        prog="project",
        description="Manage your project lifecycle",
    )
    parser.add_argument("--version", action="version", version="project 1.0.0")
 
    subparsers = parser.add_subparsers(dest="command", required=True,
                                        help="Available commands")
 
    # init subcommand
    init_cmd = subparsers.add_parser("init", help="Create a new project")
    init_cmd.add_argument("name", help="Project name")
    init_cmd.add_argument("--template", default="basic",
                          choices=["basic", "web", "api", "ml"],
                          help="Project template (default: basic)")
    init_cmd.set_defaults(func=handle_init)
 
    # build subcommand
    build_cmd = subparsers.add_parser("build", help="Build the project")
    build_cmd.add_argument("--optimize", action="store_true",
                           help="Enable release optimizations")
    build_cmd.add_argument("--target", help="Build target platform")
    build_cmd.set_defaults(func=handle_build)
 
    # test subcommand
    test_cmd = subparsers.add_parser("test", help="Run tests")
    test_cmd.add_argument("--coverage", action="store_true",
                          help="Generate coverage report")
    test_cmd.add_argument("--pattern", "-p", help="Test name pattern to match")
    test_cmd.set_defaults(func=handle_test)
 
    # deploy subcommand
    deploy_cmd = subparsers.add_parser("deploy", help="Deploy the project")
    deploy_cmd.add_argument("environment",
                            choices=["staging", "production"],
                            help="Target environment")
    deploy_cmd.add_argument("--dry-run", action="store_true",
                            help="Preview without deploying")
    deploy_cmd.set_defaults(func=handle_deploy)
 
    args = parser.parse_args()
    args.func(args)
 
if __name__ == "__main__":
    main()
$ python project.py init myapp --template web
Creating project 'myapp'
  Template: web
  Directory: myapp/
 
$ python project.py build --optimize
Building in release mode
 
$ python project.py test --coverage -p "test_api*"
Running tests (coverage=on)
  Pattern: test_api*
 
$ python project.py deploy production --dry-run
Deploying to production
  (DRY RUN -- no actual deployment)
 
$ python project.py --help
usage: project [-h] [--version] {init,build,test,deploy} ...
 
Manage your project lifecycle
 
positional arguments:
  {init,build,test,deploy}
                        Available commands
    init                Create a new project
    build               Build the project
    test                Run tests
    deploy              Deploy the project
 
$ python project.py deploy --help
usage: project deploy [-h] [--dry-run] {staging,production}
 
positional arguments:
  {staging,production}  Target environment
 
options:
  -h, --help            show this help message and exit
  --dry-run             Preview without deploying

Le point clé ici est set_defaults(func=handler). Chaque sous-commande stocke sa fonction de traitement dans l’espace de noms analysé, et la fonction principale délègue à cette fonction avec args.func(args). C’est l’approche standard utilisée dans les outils CLI de production.

Groupes d’arguments pour une meilleure aide

Lorsque votre outil comporte de nombreux arguments, vous pouvez les regrouper sous des en-têtes logiques :

import argparse
 
parser = argparse.ArgumentParser(description="Data pipeline tool")
 
input_group = parser.add_argument_group("Input options")
input_group.add_argument("--source", required=True, help="Data source path")
input_group.add_argument("--format", choices=["csv", "json", "parquet"], default="csv")
input_group.add_argument("--encoding", default="utf-8", help="File encoding")
 
transform_group = parser.add_argument_group("Transform options")
transform_group.add_argument("--filter", help="Filter expression")
transform_group.add_argument("--sort-by", help="Column to sort by")
transform_group.add_argument("--limit", type=int, help="Max rows to process")
 
output_group = parser.add_argument_group("Output options")
output_group.add_argument("-o", "--output", required=True, help="Output path")
output_group.add_argument("--compress", action="store_true", help="Compress output")
 
args = parser.parse_args()

La sortie de --help regroupe les arguments sous leurs en-têtes, ce qui rend les longues listes d’arguments plus lisibles. Il s’agit uniquement d’une fonctionnalité de présentation — cela ne change pas la manière dont les arguments sont analysés.

Aide et formatage personnalisés

Le paramètre formatter_class

import argparse
 
parser = argparse.ArgumentParser(
    prog="analyzer",
    description="Analyze datasets and generate reports.\n\n"
                "Supports CSV, JSON, and Parquet input formats.\n"
                "Output can be filtered, sorted, and aggregated.",
    epilog="Examples:\n"
           "  analyzer data.csv --format json --top 10\n"
           "  analyzer data.csv --columns name age --sort-by age\n"
           "  analyzer data.csv --filter 'age > 30' --output results.csv",
    formatter_class=argparse.RawDescriptionHelpFormatter,
)
FormatterWhat it does
HelpFormatterDefault. Wraps text to fit terminal width.
RawDescriptionHelpFormatterPreserves newlines in description and epilog.
RawTextHelpFormatterPreserves newlines everywhere, including argument help.
ArgumentDefaultsHelpFormatterAppends (default: X) to every argument's help text.

metavar et %(default)s

Contrôlez ce qui apparaît dans les messages d’utilisation :

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--top", type=int, default=10, metavar="N",
                    help="Show top N results (default: %(default)s)")
parser.add_argument("--format", default="table", metavar="FMT",
                    help="Output format (default: %(default)s)")

Cela produit --top N dans le texte d’utilisation au lieu de --top TOP, et le texte d’aide affiche la valeur réelle par défaut.

argparse vs sys.argv vs click vs typer

Python propose plusieurs approches pour analyser les arguments en ligne de commande. Voici comment elles se comparent :

Featuresys.argvargparseclicktyper
Part of stdlibYesYesNo (pip install)No (pip install)
Type conversionManualBuilt-inBuilt-inAutomatic (type hints)
Help generationNoneAutomaticAutomaticAutomatic
SubcommandsManualadd_subparsers()@group.command()app.command()
ValidationManualchoices, custom typeclick.Choice, callbacksValidators
BoilerplateHighMediumLowVery low
API styleImperativeImperativeDecoratorsDecorators + type hints
Tab completionNoneNoneVia pluginBuilt-in
Prompting / colorsManualNoneBuilt-inBuilt-in
TestingManualparse_args([...])CliRunnerCliRunner
Best forThrowaway scriptsStdlib-only projectsComplex CLIsModern Python 3.7+

Voici le même outil implémenté avec chaque approche :

# --- sys.argv: raw, fragile, no help ---
import sys
 
name = sys.argv[1] if len(sys.argv) > 1 else "World"
count = int(sys.argv[2]) if len(sys.argv) > 2 else 1
for _ in range(count):
    print(f"Hello, {name}!")
# --- argparse: stdlib, explicit argument definitions ---
import argparse
 
parser = argparse.ArgumentParser(description="Greet someone")
parser.add_argument("name", nargs="?", default="World", help="Name to greet")
parser.add_argument("-c", "--count", type=int, default=1, help="Repetitions")
args = parser.parse_args()
for _ in range(args.count):
    print(f"Hello, {args.name}!")
# --- click: decorators, pip install click ---
import click
 
@click.command()
@click.argument("name", default="World")
@click.option("-c", "--count", default=1, type=int, help="Repetitions")
def greet(name, count):
    """Greet someone."""
    for _ in range(count):
        click.echo(f"Hello, {name}!")
 
greet()
# --- typer: type hints, pip install typer ---
import typer
 
def greet(name: str = "World", count: int = 1):
    """Greet someone."""
    for _ in range(count):
        print(f"Hello, {name}!")
 
typer.run(greet)

Quand utiliser chacun :

  • sys.argv — Uniquement pour les scripts ponctuels où vous récupérez une seule valeur et n’avez pas besoin de validation.
  • argparse — Lorsque vous avez besoin d’une vraie CLI mais ne pouvez pas ajouter de dépendances externes. C’est le bon choix par défaut pour la plupart des projets.
  • click — Lorsque vous construisez une CLI complexe avec groupes de commandes imbriqués, invites interactives, sortie colorée et systèmes de plugins.
  • typer — Lorsque vous voulez un minimum de code répétitif et utilisez Python 3.7+. Il déduit les arguments à partir des type hints et s’appuie sur click.

Projet réel : CLI de traitement de données CSV

Voici un outil CLI complet, de style production, qui lit des fichiers CSV, filtre les données, calcule des agrégations et produit des résultats dans plusieurs formats. Cela montre comment les modèles argparse se combinent dans un vrai projet.

#!/usr/bin/env python3
"""csv_processor.py -- A CLI tool for analyzing CSV data."""
 
import argparse
import csv
import json
import sys
from collections import defaultdict
from pathlib import Path
 
 
def read_csv(filepath, encoding="utf-8"):
    """Read a CSV file and return a list of dictionaries."""
    with open(filepath, "r", encoding=encoding) as f:
        reader = csv.DictReader(f)
        return list(reader)
 
 
def cmd_info(args):
    """Display information about a CSV file."""
    rows = read_csv(args.file, args.encoding)
    columns = list(rows[0].keys()) if rows else []
 
    print(f"File:    {args.file}")
    print(f"Rows:    {len(rows)}")
    print(f"Columns: {len(columns)}")
 
    if args.verbose:
        print("\nColumn details:")
        for col in columns:
            non_empty = sum(1 for r in rows if r[col].strip())
            unique = len(set(r[col] for r in rows))
            print(f"  {col:30s}  non-empty: {non_empty:>6d}  unique: {unique:>6d}")
 
 
def cmd_filter(args):
    """Filter rows where a column matches a value."""
    rows = read_csv(args.file, args.encoding)
 
    if args.column not in rows[0]:
        print(f"Error: column '{args.column}' not found.", file=sys.stderr)
        print(f"Available columns: {', '.join(rows[0].keys())}", file=sys.stderr)
        sys.exit(1)
 
    if args.match == "exact":
        filtered = [r for r in rows if r[args.column] == args.value]
    elif args.match == "contains":
        filtered = [r for r in rows if args.value.lower() in r[args.column].lower()]
    elif args.match == "startswith":
        filtered = [r for r in rows if r[args.column].startswith(args.value)]
    elif args.match == "gt":
        threshold = float(args.value)
        filtered = [r for r in rows if _safe_float(r[args.column], float("-inf")) > threshold]
    elif args.match == "lt":
        threshold = float(args.value)
        filtered = [r for r in rows if _safe_float(r[args.column], float("inf")) < threshold]
 
    print(f"Matched {len(filtered)} of {len(rows)} rows", file=sys.stderr)
    _output_rows(filtered, args)
 
 
def cmd_aggregate(args):
    """Group by a column and compute aggregations."""
    rows = read_csv(args.file, args.encoding)
 
    if args.group_by not in rows[0]:
        print(f"Error: column '{args.group_by}' not found.", file=sys.stderr)
        sys.exit(1)
 
    groups = defaultdict(list)
    for row in rows:
        key = row[args.group_by]
        groups[key].append(row)
 
    results = []
    for key, group_rows in sorted(groups.items()):
        result = {args.group_by: key, "count": len(group_rows)}
 
        if args.sum_column:
            total = sum(_safe_float(r[args.sum_column], 0) for r in group_rows)
            result[f"sum_{args.sum_column}"] = round(total, 2)
 
        if args.avg_column:
            values = [_safe_float(r[args.avg_column], None) for r in group_rows]
            values = [v for v in values if v is not None]
            if values:
                result[f"avg_{args.avg_column}"] = round(sum(values) / len(values), 2)
 
        results.append(result)
 
    if args.sort_by == "count":
        results.sort(key=lambda r: r["count"], reverse=True)
 
    if args.top:
        results = results[:args.top]
 
    _output_rows(results, args)
 
 
def cmd_convert(args):
    """Convert CSV to another format."""
    rows = read_csv(args.file, args.encoding)
 
    if args.columns:
        rows = [{k: r.get(k, "") for k in args.columns} for r in rows]
 
    if args.limit:
        rows = rows[:args.limit]
 
    _output_rows(rows, args)
    count = min(args.limit, len(rows)) if args.limit else len(rows)
    print(f"Converted {count} rows", file=sys.stderr)
 
 
def _safe_float(value, default):
    """Try to convert a string to float, return default on failure."""
    try:
        return float(value)
    except (ValueError, TypeError):
        return default
 
 
def _output_rows(rows, args):
    """Write rows to the specified output in the specified format."""
    if not rows:
        return
 
    output_file = open(args.output, "w", newline="") if args.output else sys.stdout
 
    if args.format == "json":
        json.dump(rows, output_file, indent=2)
        output_file.write("\n")
    elif args.format == "csv":
        writer = csv.DictWriter(output_file, fieldnames=rows[0].keys())
        writer.writeheader()
        writer.writerows(rows)
    elif args.format == "tsv":
        writer = csv.DictWriter(output_file, fieldnames=rows[0].keys(),
                                delimiter="\t")
        writer.writeheader()
        writer.writerows(rows)
    elif args.format == "table":
        _print_table(rows, output_file)
 
    if args.output:
        output_file.close()
 
 
def _print_table(rows, out):
    """Print rows as an aligned text table."""
    if not rows:
        return
 
    headers = list(rows[0].keys())
    widths = {h: len(h) for h in headers}
    for row in rows:
        for h in headers:
            widths[h] = max(widths[h], len(str(row.get(h, ""))))
 
    header_line = "  ".join(h.ljust(widths[h]) for h in headers)
    separator = "  ".join("-" * widths[h] for h in headers)
    out.write(header_line + "\n")
    out.write(separator + "\n")
    for row in rows:
        line = "  ".join(str(row.get(h, "")).ljust(widths[h]) for h in headers)
        out.write(line + "\n")
 
 
def main():
    parser = argparse.ArgumentParser(
        prog="csv_processor",
        description="Analyze, filter, aggregate, and convert CSV files.",
        epilog="Examples:\n"
               "  csv_processor info sales.csv --verbose\n"
               "  csv_processor filter sales.csv --column region --value East\n"
               "  csv_processor aggregate sales.csv --group-by region --sum revenue\n"
               "  csv_processor convert sales.csv --format json -o sales.json\n",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--encoding", default="utf-8", help="File encoding")
 
    subparsers = parser.add_subparsers(dest="command", required=True,
                                        help="Available commands")
 
    # --- info ---
    info_p = subparsers.add_parser("info", help="Show CSV file information")
    info_p.add_argument("file", help="CSV file to inspect")
    info_p.add_argument("-v", "--verbose", action="store_true",
                        help="Show per-column statistics")
    info_p.set_defaults(func=cmd_info, format="table", output=None)
 
    # --- filter ---
    filter_p = subparsers.add_parser("filter", help="Filter rows by column value")
    filter_p.add_argument("file", help="Input CSV file")
    filter_p.add_argument("--column", "-c", required=True, help="Column to filter on")
    filter_p.add_argument("--value", "-V", required=True, help="Value to match against")
    filter_p.add_argument("--match", "-m",
                          choices=["exact", "contains", "startswith", "gt", "lt"],
                          default="exact", help="Match mode (default: exact)")
    filter_p.add_argument("--format", "-f",
                          choices=["csv", "json", "tsv", "table"],
                          default="table", help="Output format")
    filter_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    filter_p.set_defaults(func=cmd_filter)
 
    # --- aggregate ---
    agg_p = subparsers.add_parser("aggregate", help="Group and aggregate data")
    agg_p.add_argument("file", help="Input CSV file")
    agg_p.add_argument("--group-by", "-g", required=True, help="Column to group by")
    agg_p.add_argument("--sum", dest="sum_column", help="Column to sum")
    agg_p.add_argument("--avg", dest="avg_column", help="Column to average")
    agg_p.add_argument("--sort-by", choices=["name", "count"], default="name",
                        help="Sort results by name or count")
    agg_p.add_argument("--top", type=int, help="Show only top N groups")
    agg_p.add_argument("--format", "-f",
                        choices=["csv", "json", "tsv", "table"],
                        default="table", help="Output format")
    agg_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    agg_p.set_defaults(func=cmd_aggregate)
 
    # --- convert ---
    conv_p = subparsers.add_parser("convert", help="Convert CSV to another format")
    conv_p.add_argument("file", help="Input CSV file")
    conv_p.add_argument("--format", "-f", required=True,
                        choices=["json", "tsv", "csv", "table"],
                        help="Target format")
    conv_p.add_argument("--columns", nargs="+", metavar="COL",
                        help="Include only these columns")
    conv_p.add_argument("--limit", type=int, help="Max rows to convert")
    conv_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    conv_p.set_defaults(func=cmd_convert)
 
    args = parser.parse_args()
    args.func(args)
 
 
if __name__ == "__main__":
    main()

Cet outil combine sous-commandes, gestion de types personnalisés, plusieurs formats de sortie, groupes d’arguments et gestion correcte des erreurs. Chaque sous-commande dispose de son propre ensemble d’arguments ciblés. Les utilisateurs obtiennent une aide complète --help pour chaque sous-commande.

Visualiser les résultats : après avoir filtré ou agrégé des données CSV avec un outil CLI comme celui-ci, vous souhaitez souvent explorer la sortie visuellement. PyGWalker (opens in a new tab) transforme n’importe quel DataFrame pandas en interface de visualisation interactive, de type Tableau — directement dans un notebook Jupyter ou un script. Dirigez la sortie de votre CLI vers un DataFrame et utilisez PyGWalker pour créer des graphiques sans écrire de code de visualisation :

import pandas as pd
import pygwalker as pyg
 
# Load the CSV processor output
df = pd.read_csv("aggregated_results.csv")
 
# Launch interactive visual explorer
walker = pyg.walk(df)

Pour le développement interactif d’outils CLI comme celui-ci, RunCell (opens in a new tab) vous permet de créer et tester des outils CLI dans Jupyter avec l’aide de l’IA. Vous pouvez prototyper la logique d’analyse des arguments dans des cellules de notebook, tester différentes combinaisons d’arguments, puis exporter vers un script autonome.

Erreurs courantes et correctifs

1. error: unrecognized arguments

Cela se produit lorsque vous passez un argument qui n’a pas été défini :

$ python tool.py --output results.csv --compress
error: unrecognized arguments: --compress

Correctif : ajoutez l’argument à votre analyseur, ou utilisez parse_known_args() si vous voulez volontairement ignorer les arguments inconnus :

args, unknown = parser.parse_known_args()
# args contains recognized arguments
# unknown is a list of unrecognized strings

2. error: argument --count: invalid int value

$ python tool.py --count three
error: argument --count: invalid int value: 'three'

Correctif : argparse fonctionne correctement. L’utilisateur a passé une chaîne non entière à un argument type=int. Vos fonctions de type personnalisées devraient lever ArgumentTypeError avec un message clair.

3. Noms d’attributs avec des tirets

parser.add_argument("--log-level", default="INFO")
args = parser.parse_args()
 
# This is a syntax error:
# print(args.log-level)  # Python interprets this as args.log minus level
 
# Correct:
print(args.log_level)  # Dashes become underscores

4. La sous-commande ne déclenche aucune action

# Problem: no error when no subcommand is given
subparsers = parser.add_subparsers(dest="command")
# Solution: add required=True
subparsers = parser.add_subparsers(dest="command", required=True)

Sans required=True, exécuter le script sans sous-commande ne fait rien silencieusement. En Python 3.7+, définissez toujours required=True sur les sous-analyseurs.

5. Messages d’aide dupliqués à cause de l’héritage

Si un analyseur parent et une sous-commande définissent tous deux le même argument, les utilisateurs sont perdus :

# BAD: --verbose defined on both parent and child
parser.add_argument("--verbose", action="store_true")
sub = subparsers.add_parser("run")
sub.add_argument("--verbose", action="store_true")  # Shadows parent's --verbose
 
# GOOD: put shared arguments on the parent only
parser.add_argument("--verbose", action="store_true")

6. parse_args() s’exécute à l’import

# BAD: parse_args() fires when another module imports this file
parser = argparse.ArgumentParser()
parser.add_argument("--name")
args = parser.parse_args()  # Crashes if imported without CLI args
 
# GOOD: wrap everything in main()
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--name")
    args = parser.parse_args()
    return args
 
if __name__ == "__main__":
    main()

7. FileType ouvre les fichiers trop tôt

# Risky: file is opened at parse time, before your code validates other args
parser.add_argument("output", type=argparse.FileType("w"))
 
# Safer: accept a path string, open the file yourself with a context manager
parser.add_argument("output", help="Output file path")
args = parser.parse_args()
with open(args.output, "w") as f:
    f.write("data")

Modèles avancés

Analyseurs parents pour les arguments partagés

Lorsque plusieurs sous-commandes partagent le même ensemble d’arguments, utilisez des analyseurs parents pour éviter la répétition :

import argparse
 
# Shared arguments
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument("--verbose", "-v", action="store_true")
parent.add_argument("--output", "-o", default="output.csv")
 
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
 
# Both subcommands inherit --verbose and --output
cmd_a = subparsers.add_parser("analyze", parents=[parent])
cmd_a.add_argument("file", help="File to analyze")
 
cmd_b = subparsers.add_parser("compare", parents=[parent])
cmd_b.add_argument("file_a", help="First file")
cmd_b.add_argument("file_b", help="Second file")

Valeurs par défaut depuis les variables d’environnement

Récupérez les valeurs par défaut depuis les variables d’environnement pour plus de flexibilité au déploiement :

import argparse
import os
 
parser = argparse.ArgumentParser()
parser.add_argument("--api-key",
                    default=os.environ.get("API_KEY"),
                    help="API key (or set API_KEY env var)")
parser.add_argument("--port", type=int,
                    default=int(os.environ.get("PORT", "8080")),
                    help="Server port (default: $PORT or 8080)")
args = parser.parse_args()
 
if not args.api_key:
    parser.error("--api-key is required (or set API_KEY environment variable)")

Tester des scripts argparse

Vous pouvez intégrer les tests argparse dans vos suites unittest ou pytest. Testez vos outils CLI en passant des listes d’arguments à parse_args() :

import argparse
 
def create_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument("name")
    parser.add_argument("--count", type=int, default=1)
    return parser
 
# In your test file
def test_parser_defaults():
    parser = create_parser()
    args = parser.parse_args(["Alice"])
    assert args.name == "Alice"
    assert args.count == 1
 
def test_parser_with_options():
    parser = create_parser()
    args = parser.parse_args(["Bob", "--count", "5"])
    assert args.name == "Bob"
    assert args.count == 5

Séparer create_parser() de main() rend vos définitions d’arguments testables sans exécuter tout le script.

FAQ

À quoi sert Python argparse ?

Python argparse est le module de la bibliothèque standard pour analyser les arguments en ligne de commande. Il lit les chaînes de sys.argv, les convertit en objets Python typés, valide les entrées, génère la sortie --help et signale les erreurs lorsque les utilisateurs fournissent des arguments incorrects. Il sert à transformer des scripts en vrais outils CLI acceptant des entrées configurables sans modifier le code source.

Quelle est la différence entre arguments positionnels et optionnels dans argparse ?

Les arguments positionnels n’ont pas de tirets dans leur nom (par exemple, parser.add_argument("filename")). Ils sont requis par défaut et déterminés par leur position. Les arguments optionnels commencent par des tirets (par exemple, parser.add_argument("--verbose")). Ils sont optionnels par défaut et peuvent apparaître dans n’importe quel ordre. Vous pouvez rendre des arguments optionnels obligatoires avec required=True.

Comment créer des sous-commandes comme git commit ou docker build ?

Utilisez parser.add_subparsers() pour créer un groupe de sous-commandes. Appelez subparsers.add_parser("name") pour chaque sous-commande. Chaque sous-analyseur obtient son propre ensemble d’arguments. Utilisez set_defaults(func=handler_function) pour lier chaque sous-commande à un gestionnaire, puis appelez args.func(args) pour faire la distribution.

Dois-je utiliser argparse ou click pour mon CLI Python ?

Utilisez argparse lorsque vous voulez zéro dépendance externe et que votre CLI est simple. Utilisez click lorsque vous avez besoin de fonctionnalités avancées comme des invites interactives, une sortie colorée, des barres de progression ou des groupes de commandes profondément imbriqués. Typer est une autre option qui utilise les type hints Python et demande encore moins de code répétitif que click.

Comment valider des types d’entrée personnalisés avec argparse ?

Écrivez une fonction qui prend une chaîne et renvoie la valeur convertie. Si l’entrée est invalide, levez argparse.ArgumentTypeError avec un message descriptif. Passez cette fonction au paramètre type : parser.add_argument("--date", type=my_date_parser). argparse appelle votre fonction automatiquement et affiche le message d’erreur si la validation échoue.

Puis-je utiliser argparse avec des variables d’environnement ?

Oui. Définissez le paramètre default pour lire depuis os.environ : parser.add_argument("--api-key", default=os.environ.get("API_KEY")). Cela permet aux utilisateurs de configurer des valeurs via des variables d’environnement tout en laissant les drapeaux de ligne de commande les remplacer.

Conclusion

Python argparse transforme des scripts fragiles, qu’il faut modifier dans le code source, en véritables programmes en ligne de commande. Le module gère la conversion de types, la validation, la génération de l’aide et les messages d’erreur, afin que vous puissiez vous concentrer sur ce que fait le script, et non sur la manière dont il reçoit les entrées.

Les modèles à retenir :

  • Utilisez des arguments positionnels pour les entrées obligatoires, des arguments avec tirets pour les optionnels.
  • Définissez type pour la conversion automatique, choices pour les valeurs restreintes, nargs pour plusieurs valeurs.
  • Utilisez action="store_true" pour les drapeaux booléens et action="count" pour les niveaux de verbosité.
  • Construisez des outils complexes avec add_subparsers() et le modèle de distribution set_defaults(func=handler).
  • Utilisez des groupes mutuellement exclusifs lorsque des arguments entrent en conflit.
  • Encapsulez l’analyse dans main() et protégez-la avec if __name__ == "__main__".
  • Écrivez des fonctions de type personnalisées pour la validation spécifique au domaine.

argparse n’est pas la bibliothèque d’analyse d’arguments la plus récente de Python. Mais elle est fournie avec chaque installation de Python, couvre l’immense majorité des besoins CLI et produit des outils qui se comportent comme les utilisateurs s’y attendent. Pour la plupart des développeurs Python, c’est le bon choix. Lorsque votre outil CLI doit invoquer des commandes externes, combinez argparse avec le module subprocess pour obtenir une solution complète d’automatisation en ligne de commande.

Guides associés

📚