Click and Typer ship shell completion built in; argparse does not. For the many CLIs built on the standard library — internal tools, scripts that grew up, projects that avoid dependencies — argcomplete fills the gap. It hooks into your existing ArgumentParser: when the shell asks for completions, argcomplete runs your program in a special mode, walks the parser to see what can come next — subcommands, options, choices — and asks completers attached to arguments for anything dynamic. No second definition of your interface is needed. This guide adds argcomplete to an argparse CLI with subcommands, writes a context-aware completer, wires up file completion with extension filtering, covers activation for bash and zsh, and tests completions without a shell. It belongs to the shell completion topic.
Prerequisites
- An argparse CLI, for example built as in argparse subparsers for subcommands.
uv add argcomplete(examples checked with argcomplete 3.x); bash or zsh for trying it interactively.
How argcomplete works
When you press Tab, the shell's completion function runs your program with environment variables describing the command line (COMP_LINE, COMP_POINT) and _ARGCOMPLETE=1. Your program starts normally; when it reaches argcomplete.autocomplete(parser), argcomplete sees the variables, parses the partial command line with your parser, computes candidates, writes them to a file descriptor the shell reads, and exits — your program's real work never runs. Without those variables, autocomplete() returns immediately and costs almost nothing.
Two consequences follow. autocomplete() must be called before parse_args(), and as early as possible — everything executed before it runs on every Tab press. And the program's top-level imports are paid for on every completion, so heavy imports belong inside the commands, as discussed in avoiding import-time side effects.
The recipe
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
# src/deploytool/cli.py
from __future__ import annotations
import argparse
from pathlib import Path
import argcomplete
ENVIRONMENTS = ["dev", "staging", "production"]
def service_completer(prefix: str, parsed_args: argparse.Namespace, **kwargs) -> list[str]:
"""Complete service names; dev has an extra sandbox service."""
services = ["api", "web", "worker", "scheduler"]
if getattr(parsed_args, "env", None) == "dev":
services.append("sandbox")
return [s for s in services if s.startswith(prefix)]
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="deploytool")
sub = parser.add_subparsers(dest="command", required=True)
deploy = sub.add_parser("deploy", help="deploy a service")
deploy.add_argument("--env", choices=ENVIRONMENTS, default="staging")
deploy.add_argument("service").completer = service_completer
deploy.add_argument("--manifest", type=Path).completer = \
argcomplete.completers.FilesCompleter(["yaml", "yml"])
sub.add_parser("status", help="show status")
return parser
def main() -> None:
parser = build_parser()
argcomplete.autocomplete(parser) # exits here when completing
args = parser.parse_args()
print(args)
if __name__ == "__main__":
main()
Three pieces do the work:
- The
# PYTHON_ARGCOMPLETE_OKmarker in the first lines of the executable script lets argcomplete's global activation recognise the program as argcomplete-aware. With per-command registration (below) it is not strictly needed, but it costs nothing. - Subcommands, options and
choicescomplete automatically from the parser:deploytool d<Tab>givesdeploy,--env <Tab>offers the three environments. - Completers handle everything else. A completer is any callable that receives the current
prefixand the arguments parsed so far, and returns candidates. Becauseparsed_argsis available, completion can depend on earlier options — here,--env devadds asandboxservice.FilesCompletercompletes paths, optionally filtered by extension.
A completer backed by a cache
Real candidate lists usually come from somewhere slow — an API listing services, a directory of hundreds of manifests. A completer can read a small cache file and refresh it only when it is stale, keeping each Tab press fast:
import json
import time
from pathlib import Path
CACHE = Path.home() / ".cache" / "deploytool" / "services.json"
TTL = 15 * 60
def cached_services(fetch, *, now: float | None = None) -> list[str]:
now = now or time.time()
try:
data = json.loads(CACHE.read_text(encoding="utf-8"))
if now - data["fetched_at"] < TTL:
return data["services"]
except (OSError, ValueError, KeyError):
pass
try:
services = fetch() # e.g. an API call with a 1-second timeout
except Exception:
return [] # never break the shell over a completion
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps({"fetched_at": now, "services": services}))
return services
def service_completer(prefix, parsed_args, **kwargs):
return [s for s in cached_services(fetch_services) if s.startswith(prefix)]
The first Tab after the cache expires pays for one fetch; every other press reads a few hundred bytes. If the fetch fails — offline, VPN down — the completer returns nothing rather than an error, and the user can still type the value.
Debugging completion
When completion silently produces nothing, set _ARC_DEBUG=1 in the shell and press Tab again: argcomplete prints what it parsed, which completer it called and any exception, to the terminal. The most common culprits are an exception in a completer, output printed before autocomplete(), and a console script whose entry point never reaches the autocomplete() call.
Activation
argcomplete needs the shell to call your program on Tab. There are two ways:
# per command — add to ~/.bashrc (or ~/.zshrc after enabling bashcompinit)
eval "$(register-python-argcomplete deploytool)"
# global — completes every program carrying the PYTHON_ARGCOMPLETE_OK marker
activate-global-python-argcomplete --user
Per-command registration is explicit and works for any entry point, including console scripts installed by pipx or uv; the global mode needs the marker to be visible in the executable the shell finds. For zsh, run autoload -U bashcompinit && bashcompinit before the eval. Fish is supported through register-python-argcomplete --shell fish deploytool | source. The general installation and packaging advice in installing shell completion for bash, zsh and fish applies unchanged.
UX considerations
- Keep completers fast. They run on every Tab; anything slower than about 100 ms feels broken. Cache network-backed candidates as described in dynamic completion values from APIs and files.
- Never print from code that runs before
autocomplete(). Output to stdout during completion corrupts the candidate list; argcomplete redirects stdout while completing, but code before the call runs unprotected. - Fail quietly. A completer that raises produces no candidates and, at most, a debug message; catch expected errors and return an empty list.
- Prefer
choicesfor fixed sets. They complete for free and are validated at parse time. - Document activation. One line in the README with the
evalcommand gets most users set up.
Testing the behaviour
Completion can be tested without a shell by setting argcomplete's environment variables and asking it to write candidates to a file:
# tests/test_completion.py
import os
import subprocess
import sys
from pathlib import Path
import pytest
SCRIPT = Path(__file__).resolve().parents[1] / "src" / "deploytool" / "cli.py"
def complete(line: str, tmp_path: Path, cwd: Path | None = None) -> list[str]:
out = tmp_path / "candidates.txt"
env = {**os.environ, "_ARGCOMPLETE": "1", "_ARGCOMPLETE_IFS": "\n",
"COMP_LINE": line, "COMP_POINT": str(len(line)),
"_ARGCOMPLETE_STDOUT_FILENAME": str(out)}
subprocess.run([sys.executable, str(SCRIPT)], env=env, cwd=cwd, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, pass_fds=())
return out.read_text().split()
@pytest.mark.parametrize("line,expected", [
("deploytool d", ["deploy"]),
("deploytool deploy --env ", ["dev", "staging", "production"]),
("deploytool deploy w", ["web", "worker"]),
("deploytool deploy --env dev s", ["scheduler", "sandbox"]),
])
def test_completions(tmp_path, line, expected):
assert complete(line, tmp_path) == expected
def test_manifest_completes_yaml_files(tmp_path):
work = tmp_path / "work"
work.mkdir()
(work / "app.yaml").write_text("")
(work / "notes.txt").write_text("")
assert complete("deploytool deploy api --manifest ", tmp_path, cwd=work) == ["app.yaml"]
The tests run the real script, exactly as the shell would, so they also catch completion-breaking mistakes outside the parser — a print at import time, a slow import, an exception before autocomplete(). The patterns in testing shell completion in Python CLIs cover the Click and Typer equivalents.
Conclusion
argcomplete gives argparse CLIs completion without duplicating the interface: call argcomplete.autocomplete(parser) before parse_args() and as early as possible, let subcommands, options and choices complete automatically, attach completers for dynamic values (they can read earlier arguments), use FilesCompleter for paths, and activate with register-python-argcomplete. Keep startup and completers fast, and test completions by running the script with argcomplete's environment variables.
Frequently asked questions
Does argcomplete slow down normal runs?
Barely: without _ARGCOMPLETE in the environment, autocomplete() returns after a few checks. The cost to watch is your own import time, which is paid on every Tab press.
Can completers call an API?
They can, with a short timeout and a cache. A completer that waits several seconds for a network call makes the whole shell feel frozen; fall back to cached or no candidates when the call is slow.
How do I complete a positional argument with a fixed list?
Use choices= if the value must be one of the list, or attach argcomplete.completers.ChoicesCompleter([...]) when you want suggestions without validation.
Can I generate a static completion script instead?
argcomplete is dynamic by design — it runs your program to complete. If you need static scripts for packaging, tools such as shtab generate bash, zsh and tcsh scripts from an argparse parser; the trade-off is that static scripts cannot offer dynamic values.
Does argcomplete work on Windows?
In Git Bash and other bash-compatible shells on Windows, yes, with the same registration. PowerShell and cmd are not supported by argcomplete's activation scripts; for PowerShell users, a small Register-ArgumentCompleter script that calls your program with argcomplete's environment variables set — and reads the candidates from the output file — is the usual route, and worth shipping if many users are on Windows.