Skip to content

Getting started

Install

pip install docopt2

docopt2 has zero runtime dependencies. The core needs nothing outside the Python standard library. pydantic support is optional and reflective (docopt2[pydantic]).

Coming from the original docopt?

docopt2 is a drop-in replacement on Python 3.10+. Change the import and the same usage message parses to the same mapping.

It is a superset, not a bit-identical clone. It accepts some argvs the original rejects, and it fixes three of the original's parsing bugs. Every divergence is pinned by name.

Your first parse

The usage message is the parser spec. Describe the interface as text, hand it to docopt, and read the parsed values back by name. Start with two positional arguments:

from docopt2 import docopt

args = docopt("Usage: prog <host> <port>", "127.0.0.1 8080")
args["<host>"]  # "127.0.0.1"
args["<port>"]  # "8080"

In a real program you call docopt(__doc__) and it reads sys.argv. The examples pass an argv explicitly so that each one stands on its own.

Add an option that takes a value. [--port=<n>] is optional, and the [default: ...] declared under Options: fills it in when the flag is absent:

doc = "Usage: prog <host> [--port=<n>]\n\nOptions:\n  --port=<n>  [default: 8000]"

docopt(doc, "localhost")["--port"]              # "8000" - the default fills in
docopt(doc, "localhost --port=9000")["--port"]  # "9000" - the argument wins

A bare flag carries no value. It is True when present, False when absent:

doc = "Usage: prog [--verbose] <host>"

docopt(doc, "--verbose example.com")["--verbose"]  # True
docopt(doc, "example.com")["--verbose"]            # False

A command is a literal word, and like a flag it comes back as a bool. A trailing ... makes an element repeatable, and its value arrives as a list:

args = docopt("Usage: prog add <file>...", "add a.txt b.txt")
args["add"]     # True
args["<file>"]  # ['a.txt', 'b.txt']

That is most of the vocabulary. Groups, alternation, [options] and -- are the rest, and a real program is those pieces combined. Save this as naval_fate.py:

"""Naval Fate.

Usage:
  naval_fate ship new <name>...
  naval_fate ship <name> move <x> <y> [--speed=<kn>]
  naval_fate ship shoot <x> <y>
  naval_fate mine (set|remove) <x> <y> [--moored | --drifting]
  naval_fate -h | --help
  naval_fate --version

Options:
  -h --help     Show this screen.
  --version     Show the version.
  --speed=<kn>  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.

"""

from docopt2 import docopt

if __name__ == "__main__":
    arguments = docopt(__doc__, version="Naval Fate 2.0")
    print(arguments)

from docopt2 import docopt is the only line a docopt user changes.

The module docstring is the whole parser. docopt(__doc__, ...) reads the Usage: and Options: blocks out of it, then matches sys.argv[1:] against them. Run it:

$ python naval_fate.py ship new Titanic Bismarck
{'--drifting': False,
 '--help': False,
 '--moored': False,
 '--speed': '10',
 '--version': False,
 '<name>': ['Titanic', 'Bismarck'],
 '<x>': None,
 '<y>': None,
 'mine': False,
 'move': False,
 'new': True,
 'remove': False,
 'set': False,
 'ship': True,
 'shoot': False}

Every element in the usage becomes one key:

  • a command is True or False, or a count once it repeats
  • a positional holds its string, a list under ..., or None when absent
  • an option is keyed by its long form when it has one, so -h --help is a single --help key

--speed already carries its [default: 10]. A second call fills different keys:

$ python naval_fate.py ship Titanic move 1 2 --speed=15
arguments["ship"]      # True
arguments["move"]      # True
arguments["<name>"]    # ['Titanic']
arguments["<x>"]       # '1'
arguments["--speed"]   # '15'

Help and version are handled for you

--help prints the docstring and exits. --version prints the version= you passed.

Both are driven by the help= and version= keywords, not by their presence in the usage. Deleting the naval_fate -h | --help line does not turn --help off. Pass help=False to take it over yourself.

$ python naval_fate.py --version
Naval Fate 2.0

When the arguments do not match, docopt does not return a broken result. It points at what is missing and exits non-zero. Move a ship, but forget the second coordinate:

$ python naval_fate.py ship Titanic move 1 error: missing required `<y>` | | in the usage: | naval_fate ship <name> move <x> <y> [--speed=<kn>] | ^^^ required here | = note: of 6 usage patterns, your arguments came closest to this one

docopt2 finds the usage line you came closest to and carets the one element it still needs. The full usage is printed beneath it, trimmed here.

Other mismatches get the same treatment: an unknown option, or a value that will not fit its type. Each one cross-references your argv against the usage. See Diagnostics.

The full grammar lives in Usage DSL: groups, alternation, repetition, [options], and --.

Typed results

Reading string keys out of a mapping is the original docopt contract. Pass a schema instead and you get a typed object back, with values coerced to the field types:

import dataclasses

from docopt2 import docopt


@dataclasses.dataclass
class Args:
    host: str
    port: int  # coerced from the parsed string


args = docopt("Usage: prog <host> <port>", "127.0.0.1 8080", schema=Args)
args           # Args(host='127.0.0.1', port=8080)
args.port      # 8080, now an int

The port field is annotated int, so the parsed "8080" comes back as an int. Static type checkers see args.port as an int too.

A schema can be a dataclass, a TypedDict, a Cli subclass, or a pydantic model. Typed results covers each shape, the coercions, and what a failure looks like.

You do not have to write the schema by hand. docopt2 stub naval_fate.py reads the usage and prints a ready-to-edit dataclass, TypedDict, or Cli subclass, with every field typed from the grammar.

The grammar only knows that --speed holds a string, so it emits speed: str. Narrow it to speed: int and the coercion follows. See Schema stubs.

Next steps