docopt

An innovative library that automatically generates command-line argument parsers from docstrings. Takes a declarative approach.

pythonclidocstringdeclarative

GitHub Overview

docopt/docopt

Create *beautiful* command-line interfaces with Python

Stars7,984
Watchers161
Forks563
Created:April 7, 2012
Language:Python
License:MIT License

Topics

None

Star History

docopt/docopt Star History
Data as of: 7/25/2025, 02:04 AM

Framework

docopt

Overview

docopt is an innovative library that automatically generates command-line interfaces (CLI) from Python script docstrings. It's designed based on the philosophy that "your program's help message is its specification," enabling a declarative approach to defining CLIs. The original version is no longer maintained, and migration to docopt-ng is now recommended.

Details

docopt takes a completely different approach from traditional CLI libraries. It parses the docstring that describes how to use your program and automatically performs argument parsing based on that description. This method prevents divergence between documentation and code, ensuring the usage information is always up-to-date.

Maintenance Status (2024)

  • Original docopt: No updates since version 0.6.2 in 2013, with 28 pull requests remaining unprocessed. The project is effectively unmaintained.
  • docopt-ng: A fork maintained by the Jazzband project, providing type hints, complete test coverage, and ongoing maintenance. The latest release was in May 2023.
  • Future Outlook: Even docopt-ng faces uncertainty regarding long-term maintenance, with discussions about transitioning to more modern CLI frameworks. For new projects, modern frameworks with type hints and input validation are increasingly recommended over docopt-based solutions.

Key Features

  • Docstring-driven Development: Define CLI by simply writing a Usage: section in your program's docstring
  • Self-documenting: Help message and parser exist in the same place
  • Pattern-based Syntax: Uses conventional command-line help message notation
  • Minimal Code: Implement CLI with just a few lines
  • Dictionary Output: Parse results returned as an easy-to-use dictionary
  • POSIX Compliant: Follows standard command-line conventions

Usage Pattern Syntax

  • Positional Arguments: Enclosed in angle brackets like <file>, <host>
  • Options: Format as -h, --help, or -h, --help
  • Commands: Simple words like add, ship
  • Required Elements: Enclosed in () (e.g., (add|rm))
  • Optional Elements: Enclosed in [] (e.g., [--timeout=<seconds>])
  • Mutually Exclusive: Separated by | (e.g., (-a|-b))
  • Repeating Elements: Append ... (e.g., NAME...)

Pros and Cons

Pros

  • Improved maintainability through unified documentation and code
  • Intuitive and readable syntax
  • Low learning curve
  • Concise and elegant code
  • Complies with standard command-line conventions

Cons

  • May not be suitable for complex CLIs
  • No built-in data validation (requires separate implementation)
  • Difficult to customize error messages
  • Type hints are inherently limited (due to docstring-based operation)
  • Original version is completely unmaintained (no updates since 2013)
  • docopt-ng also faces long-term maintenance uncertainty
  • Can be challenging to debug
  • Modern CLI libraries are recommended for new projects

Key Links

Code Examples

Basic Usage Example

"""Simple greeting program

Usage:
    hello.py <name> [--times=<n>]
    hello.py -h | --help
    hello.py --version

Options:
    -h --help     Show this help message
    --version     Show version
    --times=<n>   Number of times to greet [default: 1]
"""
from docopt import docopt

def main():
    arguments = docopt(__doc__, version='Hello 1.0')
    
    name = arguments['<name>']
    times = int(arguments['--times'])
    
    for _ in range(times):
        print(f'Hello, {name}!')

if __name__ == '__main__':
    main()

Multiple Commands Example

"""File management tool

Usage:
    file_manager.py list [<directory>]
    file_manager.py copy <source> <destination> [--force]
    file_manager.py delete <file>... [--confirm]
    file_manager.py (-h | --help)
    file_manager.py --version

Commands:
    list            List directory contents
    copy            Copy a file
    delete          Delete files

Options:
    -h --help       Show this help message
    --version       Show version
    --force         Overwrite existing files
    --confirm       Confirm before deletion
"""
from docopt import docopt
import os
import shutil

def main():
    args = docopt(__doc__, version='File Manager 1.0')
    
    if args['list']:
        directory = args['<directory>'] or '.'
        files = os.listdir(directory)
        for f in files:
            print(f)
    
    elif args['copy']:
        source = args['<source>']
        destination = args['<destination>']
        if os.path.exists(destination) and not args['--force']:
            print(f'Error: {destination} already exists. Use --force to overwrite.')
        else:
            shutil.copy2(source, destination)
            print(f'Copied: {source} -> {destination}')
    
    elif args['delete']:
        files = args['<file>']
        if args['--confirm']:
            response = input(f'Delete {len(files)} files? (y/n): ')
            if response.lower() != 'y':
                print('Deletion cancelled.')
                return
        
        for file in files:
            os.remove(file)
            print(f'Deleted: {file}')

if __name__ == '__main__':
    main()

Example with Pydantic Validation

"""Coordinate calculator

Usage:
    calculator.py distance <x1> <y1> <x2> <y2>
    calculator.py midpoint <x1> <y1> <x2> <y2>
    calculator.py (-h | --help)
    calculator.py --version

Options:
    -h --help     Show this help message
    --version     Show version
"""
from docopt import docopt
from pydantic import BaseModel, Field, ValidationError
import math

class CoordinateArgs(BaseModel):
    x1: float = Field(..., alias='<x1>')
    y1: float = Field(..., alias='<y1>')
    x2: float = Field(..., alias='<x2>')
    y2: float = Field(..., alias='<y2>')
    distance: bool
    midpoint: bool

def calculate_distance(x1, y1, x2, y2):
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

def calculate_midpoint(x1, y1, x2, y2):
    return ((x1 + x2) / 2, (y1 + y2) / 2)

def main():
    raw_args = docopt(__doc__, version='Calculator 1.0')
    
    try:
        args = CoordinateArgs(**raw_args)
        
        if args.distance:
            result = calculate_distance(args.x1, args.y1, args.x2, args.y2)
            print(f'Distance: {result:.2f}')
        
        elif args.midpoint:
            mx, my = calculate_midpoint(args.x1, args.y1, args.x2, args.y2)
            print(f'Midpoint: ({mx:.2f}, {my:.2f})')
    
    except ValidationError as e:
        print('Error: Invalid arguments.')
        print(e)
        exit(1)

if __name__ == '__main__':
    main()

Advanced Pattern Example

"""Git-style tool with subcommands

Usage:
    mytool init [<name>] [--bare]
    mytool clone <url> [<directory>]
    mytool commit -m <message> [--amend]
    mytool push [<remote>] [<branch>]
    mytool pull [<remote>] [<branch>] [--rebase]
    mytool status [--short]
    mytool log [--oneline] [--graph] [-n <number>]
    mytool (-h | --help)
    mytool --version

Options:
    -h --help       Show this help message
    --version       Show version
    --bare          Create a bare repository
    -m <message>    Commit message
    --amend         Amend the previous commit
    --rebase        Use rebase when pulling
    --short         Show short format
    --oneline       Show each commit on one line
    --graph         Show graph display
    -n <number>     Number of commits to show [default: 10]
"""
from docopt import docopt

def main():
    args = docopt(__doc__, version='MyTool 1.0')
    
    # Command detection and processing
    if args['init']:
        name = args['<name>'] or 'myproject'
        bare = args['--bare']
        print(f'Initialized project "{name}"')
        if bare:
            print('(bare repository)')
    
    elif args['clone']:
        url = args['<url>']
        directory = args['<directory>'] or url.split('/')[-1]
        print(f'Cloning: {url} -> {directory}')
    
    elif args['commit']:
        message = args['<message>']
        amend = args['--amend']
        if amend:
            print(f'Amending previous commit: "{message}"')
        else:
            print(f'Committing: "{message}"')
    
    elif args['status']:
        if args['--short']:
            print('M  file1.txt\nA  file2.txt')
        else:
            print('Modified files:\n  file1.txt\nNew files:\n  file2.txt')

if __name__ == '__main__':
    main()