You have completion working in Python — subcommands and options complete when you drive the protocol by hand. This guide is the other half: getting that completion to fire automatically in a real shell. Each shell loads completion scripts from its own place and in its own way, so "install completion" means three different recipes for bash, zsh, and fish. This walks through generating the script and placing it for each shell, choosing between eager and sourced loading, and fixing the classic failure where Tab does nothing at all. Enabling completion in your code is the sibling guide; here we assume that already works and focus on the shell.
TL;DR
- Generate the script. Click:
_YOURCLI_COMPLETE=bash_source yourcli(swapbash→zsh/fish). Typer:yourcli --show-completion, or letyourcli --install-completiondo it for you. - bash: source the script from
~/.bashrc, or drop it in abash-completiondirectory. Requires thebash-completionpackage. - zsh: the script must be on
fpathandcompinitmust run — set both in~/.zshrc. - fish: put a file named
yourcli.fishin~/.config/fish/completions/; fish autoloads it, no sourcing needed. - Nothing happens? Start a fresh shell, check the command is on
PATH, and in zsh runrehash. Ninety percent of "completion is broken" is one of these.
Generate the completion script
Every install starts by generating the shell script your CLI already knows how to emit. With Click you set the completion trigger to the *_source variant for the target shell and capture stdout:
$ _YOURCLI_COMPLETE=bash_source yourcli # prints the bash completion script
$ _YOURCLI_COMPLETE=zsh_source yourcli # zsh version
$ _YOURCLI_COMPLETE=fish_source yourcli # fish version
The variable name is derived from your console-script name: uppercased, hyphens to underscores, _COMPLETE appended (my-tool → _MY_TOOL_COMPLETE). With Typer you do not need to remember the variable — the app prints the script for you:
$ yourcli --show-completion # prints the script for your current shell
$ yourcli --install-completion # generates AND installs it in the right place
--install-completion is the fast path for end users; the manual placement below is what you reach for when you want the script checked into a package, deployed system-wide, or loaded a particular way.
Confirm which shell you are actually in before you generate — the login shell in your terminal is not always what you assume. echo "$0" or ps -p $$ -o comm= prints the running shell, and you want the script for that shell, not for whatever chsh says your default is. Generating a zsh script and sourcing it from a bash .bashrc is a surprisingly common way to get silent, do-nothing completion.
bash
bash completion relies on the bash-completion package (preinstalled on most Linux distributions; brew install bash-completion@2 on macOS). There are two good placements.
Sourced from .bashrc (simplest, per-user). Generate the script once into a file and source it on shell start. Generating to a file rather than eval-ing on every startup keeps your shell fast — you are not launching Python each time a shell opens.
$ _YOURCLI_COMPLETE=bash_source yourcli > ~/.yourcli-complete.bash
# ~/.bashrc
source ~/.yourcli-complete.bash
Dropped in a completion directory (per-user or system). bash-completion autoloads files from its user directory, so no source line is needed:
$ mkdir -p ~/.local/share/bash-completion/completions
$ _YOURCLI_COMPLETE=bash_source yourcli > ~/.local/share/bash-completion/completions/yourcli
For a system-wide install (a package's postinstall step, say), write to /usr/share/bash-completion/completions/yourcli instead. Either way, open a new shell to pick it up.
zsh
zsh needs two things to be true: your completion script must be on fpath, and the completion system (compinit) must have been initialised. Miss either and Tab stays silent. The robust per-user setup:
$ mkdir -p ~/.zfunc
$ _YOURCLI_COMPLETE=zsh_source yourcli > ~/.zfunc/_yourcli
# ~/.zshrc — order matters: fpath BEFORE compinit
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit
The leading underscore in the filename (_yourcli) is a zsh convention for completion functions — keep it. If you use a framework like Oh My Zsh that already calls compinit, just make sure your fpath= line runs before it; putting the fpath addition near the top of .zshrc is the safe choice. After editing, start a new shell or run exec zsh.
fish
fish is the easiest of the three: it autoloads any file in its completions directory, matched by command name. No sourcing, no init call.
$ mkdir -p ~/.config/fish/completions
$ _YOURCLI_COMPLETE=fish_source yourcli > ~/.config/fish/completions/yourcli.fish
The filename must match the command (yourcli.fish). fish picks it up in new shells automatically, and often in the current one too because it loads completions lazily on first use. For a system-wide install, write to /usr/share/fish/vendor_completions.d/yourcli.fish.
Eager (eval) vs sourced-from-file
You will see two styles in the wild and it is worth knowing the trade-off.
- Eager /
evalon every startup —eval "$(_YOURCLI_COMPLETE=bash_source yourcli)"directly in.bashrc. Always current, because it regenerates the script each time. The cost: it launches your Python program on every new shell, which adds startup latency you will feel if the tool is heavy. This is where a fast CLI pays off — see CLI startup performance and lazy loading. - Sourced from a generated file — generate once to a file, then
sourceor autoload that file. Zero Python at shell start, so it is the better default. The catch: the script is a snapshot. If you add subcommands or rename options, the shape of completion can go stale until you regenerate. (Dynamic value callbacks still run your program at Tab-time, so those stay fresh regardless.)
For most users, generate to a file. Reserve eval for a machine where you are actively developing the CLI and want the completion structure to track your code without regenerating.
Troubleshooting: completion never fires
When Tab does nothing, work down this list — the fixes are almost always mundane.
- You did not start a fresh shell. Completion registration happens at shell startup. After installing, open a new terminal or run
exec bash/exec zsh/ start a new fish. This is the single most common cause. - The command is not on
PATH. The generated script invokesyourclito compute candidates. Ifyourcliis not on thePATHof the shell where you press Tab, nothing comes back. Confirm withcommand -v yourcli. Tools installed with pipx oruv toolput a launcher onPATHfor you — see installing and distributing CLIs with pipx. - zsh needs a rehash. zsh caches the commands on
PATH. Right after installing a new CLI, zsh may not "see" it until you runrehash(or open a new shell). If the completion function itself is not found, re-check thatfpathis set beforecompinit. - Stale zsh completion cache. zsh caches completion metadata in
~/.zcompdump. After changing a completion script, remove it and reinitialise:rm -f ~/.zcompdump && compinit. - bash-completion is not installed. The generated bash script depends on the
bash-completionruntime. On a minimal system it may be missing; install thebash-completionpackage. - It works in one shell but not another. Each shell loads a different script from a different place, so a working bash setup tells you nothing about zsh. Install completion once per shell you actually use, and test each in its own fresh session.
- The Python side is actually broken. Rule this out fast by driving the protocol directly, with no shell in the loop:
$ _YOURCLI_COMPLETE=bash_complete COMP_WORDS="yourcli " COMP_CWORD=1 yourcli
plain,deploy
plain,status
If that prints candidates, your program is fine and the problem is placement or loading. If it prints nothing, fix completion in your code first — that is the enabling guide.
Production notes
- Ship the script, don't make users generate it. When you package the CLI, include a generated completion file per shell and document one-line install steps, or lean on Typer's
--install-completion. Regenerate the files as part of your release so their structure tracks the code. - The script name must match the console-script name. Both the completion trigger variable and the filename derive from the installed command. Renaming the entry point invalidates a shipped script; keep them in step with your entry points.
- System vs user directories. System paths (
/usr/share/...) suit OS packages; user paths (~/.local/share/...,~/.zfunc,~/.config/fish/completions) suit pip/pipx installs where you cannot write to system locations. - Test in a clean shell. Completion bugs hide behind an already-configured environment. Verify in
env -i bash --noprofile --norc(then source only your script) or a fresh container so you catch missing dependencies your own dotfiles paper over. - Pin versions. The generated script format is tied to the framework — pin
click>=8.1/typer>=0.12and regenerate scripts when you upgrade.
Related
- Enabling tab completion in Click and Typer — the sibling guide: make completion work in your Python code first.
- Shell completion for Python CLIs — the overview tying the Python and shell sides together.
- Installing and distributing CLIs with pipx — get the command onto users'
PATHso completion can invoke it. - Best practices for Python CLI entry points — the console-script name the completion script keys off.