Typed successor to docopt. The usage message is the parser spec.
A drop-in replacement for docopt - a superset, not a rewrite.
What is docopt2?
docopt2 is a command-line argument parser for Python. Most CLI libraries have you build the parser in code and generate a --help. docopt2 inverts that: you write the usage message, and that message is the parser.
Put the Usage: and Options: text in your docstring - the help you would write anyway - and docopt2 parses the command line against it. The help users read and the parser that runs are the same, and cannot drift.
The full documentation walks through Getting started and the guides.
Features
|
Typed results Pass a schema, get a typed result back - fields coerced to their types, never a dict[str, Any].
|
Rustc-style diagnostics Errors point at the mistake: colored carets in your argv and the usage, plus the closest usage line. |
|
Subcommand dispatch Route each subcommand to its own handler - no if args[...] ladder.
|
Shell completion Tab-completion that knows your grammar: bash, zsh, fish, PowerShell, nushell. |
|
Schema codegen Never hand-write the schema - docopt2 stub generates it from your usage, in three styles.
|
Layered fallback Resolve an option from [env: VAR] or [config: key] - CLI, then env, then config, then default.
|
Self-documenting --helpOpt into a colored, scoped help that shows where each value resolves from - env, config, default. |
Example generation Sample the argvs your usage accepts - for drift detection, fuzzing, and a Hypothesis strategy. |
|
Static usage linter Catch a broken usage before it ships - docopt2 check flags dead defaults and unusable options.
|
Usage formatter Keep the Options: block aligned - docopt2 fmt reformats it, the format half to check's lint.
|
|
Round-trip codec Turn a parsed result back into an argv that parses to it - format_argv, the inverse of docopt.
|
Compatibility checkingdocopt2 compat old new reports the changes that would break callers - a breaking-change detector for your CLI.
|
Quick start
pip install docopt2 # just change the import
"""Naval Fate.
Usage:
naval ship new <name>...
naval ship <name> move <x> <y> [--speed=<kn>]
naval --help
Options:
--speed=<kn> Speed in knots [default: 10].
"""
from docopt2 import docopt
args = docopt(__doc__)
# args is a dict: {"ship": True, "<name>": ["titanic"], "move": True, "--speed": "10", ...}
docopt2 parses the same usage grammar, so switching over is a one-line import change and everything else is opt-in.
Usage syntax
docopt2 reads the same usage DSL as docopt - the Usage: and Options: blocks are the spec.
| Syntax | Meaning |
|---|---|
command | A literal (sub)command, matched as-is. |
<arg>, ARG | A positional argument. |
-o, --option | An option (flag). |
--option=<val> | An option that takes a value. |
[ ] | Optional element(s). |
( ) | Required group. |
a | b | Mutually exclusive: choose one. |
element... | Repeatable: one or more. |
[options] | Stands in for every option listed under Options:. |
-- | Ends option parsing. The rest is positional. |
[default: <val>] | An option's default value, declared under Options:. |
[env: <var>] | An option's environment-variable fallback. |
[config: <key>] | An option's config-file fallback (CLI > env > config > default). |
The legend covers the essentials. Precedence and edge cases are in the full usage grammar.
Why typed docopt?
docopt hands you a dict of strings - no autocomplete, no static types, coercion by hand at every call site:
args = docopt("Usage: app <host> <port> [--verbose]")
host = args["<host>"] # unchecked, a bare string
port = int(args["<port>"]) # coerce by hand, at every call site
docopt2 takes a schema and gives you a typed result back:
@dataclasses.dataclass
class Args:
host: str
port: int # coerced from the parsed string
verbose: bool
args = docopt("Usage: app <host> <port> [--verbose]", schema=Args)
args.port # statically an int, not a string
A dataclass, a TypedDict, the Cli base class, or a pydantic model all work as schema= -
and you don't hand-write it, docopt2 stub generates it from the usage.
Validated choices. A Literal or Enum field becomes a closed set - a bad value is rejected with the valid set listed and the closest match suggested.
Diagnostics that point at the problem
When the arguments don't match, docopt reprints the usage and leaves you to find the mistake:
Usage:
git commit [--message=<msg>] [--amend]
git push [--force] <remote>
docopt2 points at it - in the argv and the usage that rejected it. A mistyped option or command also gets a "did you mean" hint.
The same two carets flag a malformed usage at import time - a broken spec fails loudly, not silently - and a value that will not coerce to its typed field:
Closest usage line. docopt2 finds the usage line you got furthest into and carets the single element it still needs, rather than reprinting the whole usage. It needs a matched command or option to go on: an argv that matches no line at all falls back to the plain mismatch message.
Subcommand dispatch
Dispatch routes each command to its own handler - no if args["..."] ladder:
from docopt2 import Dispatch
app = Dispatch("""Usage:
git add <path>...
git commit --message=<msg>
""")
@app.on("add")
def add(args):
print(f"adding {args['<path>']}")
@app.on("commit")
def commit(args):
print(f"committing {args['--message']!r}")
app.run() # parse argv, call the matched command's handler
app.check() lints the table before any run.
Shell completion
Generate the completion script for your shell. Tab then narrows to exactly what is valid at the cursor,
straight from the usage:
$ naval <TAB>
--help --speed ship
$ naval ship <TAB>
--speed new
$ naval ship titanic move 1 2 <TAB>
--speed
With a schema, Tab also completes the values of a Literal or an Enum field and the file names of a Path field.
A program prints its own script: eval "$(_DOCOPT2_COMPLETE=bash naval)".
Generate the schema from the usage
Don't hand-write the schema - docopt2 stub generates it from your usage (a module docstring, a text file, or stdin):
$ docopt2 stub naval.py
@dataclasses.dataclass
class Args:
ship: bool
new: bool
name: list[str]
move: bool
x: str | None
y: str | None
speed: str
help: bool
Add --style=typeddict or --style=cli for the other shapes.
Widen a field by hand (speed: int) and the coercion is automatic.
Layered value resolution
Declare an option's fallback sources in the usage with [env: VAR] and [config: key]. docopt2 resolves each -
the command-line argument first, then the environment, then a config mapping you pass, then the [default: ...]:
doc = "Usage: prog [--port=<n>]\n\nOptions:\n --port=<n> Port [default: 80] [env: APP_PORT] [config: server.port]."
docopt(doc, "", config={"server": {"port": 8080}}) # {'--port': '8080'} - config
os.environ["APP_PORT"] = "7000"
docopt(doc, "", config={"server": {"port": 8080}}) # {'--port': '7000'} - env wins
docopt(doc, "--port=9000", config={"server": {"port": 8080}}) # {'--port': '9000'} - CLI wins
docopt2 never reads the file - you load it however you like (TOML, JSON, a [tool.<prog>] table) and pass the mapping, so you are never tied to one format.
Know where a value came from. args.source(name) reports the layer that actually won - so you can log or branch on provenance instead of guessing:
docopt(doc, "--port=9000", config={"server": {"port": 8080}}).source("--port") # Source.CLI
Scaffold the config file from your usage. docopt2 config-template writes a ready-to-fill TOML skeleton - every [config: key], seeded with its default:
$ docopt2 config-template app.py
[server]
port = 80 # --port, env APP_PORT
Self-documenting --help
--help prints your usage verbatim by default. Opt into help_style="rich" for an aligned, colored screen that documents where each value resolves from, reading the [env, config, default] chain straight out of the usage you already wrote:
Because the sources are declared right in the usage text, the help writes itself. It also scopes to the subcommand the user typed - git commit --help shows only commit.
Sample the invocations your usage accepts
docopt2 examples (or generate_examples) walks the usage grammar into concrete invocations it accepts -
derived from the spec, so they cannot drift from what docopt parses:
$ docopt2 examples naval.py --count=4 --seed=5
--help
ship v1 move v2 v3
ship new v4 v5
ship new v6
Golden-file them to catch grammar drift, fuzz your parser with the accepted set, or add --invalid for the reject-set.
Property-test with Hypothesis. The same sampler is a shrinking Hypothesis strategy - every draw is an argv your usage accepts.
Lint the usage before it ships
docopt2 check (or docopt2.check(doc) in code) lints the usage grammar itself -
catching defects the parser would otherwise accept in silence:
It flags dead [default: ...] values, options declared but never usable,
ambiguous variadic positionals, and redundant alternatives.
A usage that does not parse is reported as the error docopt() would raise, and docopt2 check exits 1.
With check(doc, schema=Args) it also reports where the schema disagrees with the usage.
Format the usage
docopt2 fmt (or format_usage) reformats the Options: block - aligning every description into one column,
normalizing each spec, stripping trailing space - the format half to check's lint. Take a drifted block:
Options:
--port=<n> Port [default: 8080].
--host=<h> Interface [default: 127.0.0.1].
-v, --verbose Be loud.
docopt2 fmt lines it up, without changing what the usage parses to and leaving the Usage: patterns untouched:
Options:
--port=<n> Port [default: 8080].
--host=<h> Interface [default: 127.0.0.1].
-v --verbose Be loud.
The change is layout-only and idempotent. docopt2 fmt prints to stdout - pipe it back to rewrite the file, or diff it in CI to flag a usage that is not formatted.
Round-trip: results back to argv
docopt parses an argv into a result. format_argv does the inverse - the same usage message drives both directions:
doc = "Usage: prog <src> <dst> [--force]"
args = docopt(doc, "a b --force")
format_argv(args, doc) # ['a', 'b', '--force'] - which docopt parses straight back to args
It emits every element that carries a value: what you supplied, plus whatever [env:] or [config:] resolved.
Each candidate is verified by re-parsing, and an ambiguous grammar raises rather than return a wrong argv.
Compatibility checking
Your usage message is your interface, so check_compat (and docopt2 compat) reports the changes that would break callers - invocations the old usage accepts that the new one rejects:
check_compat("Usage: git push [--force] <remote>", "Usage: git push <remote> <branch>")
# ['option `--force` removed', '`push v1` no longer accepted']
It reports only definite breaks - never a "compatible" all-clear - so docopt2 compat before after drops into a release gate like a linter.
Read the full documentation · start with Getting started
MIT License · derived from docopt · see NOTICE
Metadata
Release files for docopt2 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| docopt2-1.3.0.tar.gz | 938.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| docopt2-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.0 MB
Release files / docopt2-1.3.0.tar.gz
| Download URL | docopt2-1.3.0.tar.gz |
|---|---|
| Size | 938.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f877e102dead7c13256bffdeed2971ac0a5c53fd2cc27e7715aa06ffeec4c263
|
|
BLAKE2b-256 checksum How to use checksums |
5777cca7d0ca7055a147ac80b8917e798aeb89b36b17456246daf85f68550db1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / docopt2-1.3.0-py3-none-any.whl
| Download URL | docopt2-1.3.0-py3-none-any.whl |
|---|---|
| Size | 103.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0c88fa5708d0954e537403ff07515d53e901b88ebcda4a27d516ea5cbf47afff
|
|
BLAKE2b-256 checksum How to use checksums |
9d656b1ced6c5d2968e7409b3717922c8aac95b1de1b14d6d33cc0947b883048
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log