Excellent — yes, that’s exactly the right approach for taming argcomplete’s ambiguity:
wrap your completer in a small factory that inspects the parse state (parsed_args) and only delegates to the inner completer if it’s actually “safe” to do so.
Let’s go step-by-step and build that.
You can absolutely write something like:
parser.add_argument("arg1", nargs="?").completer = arg1_completer
parser.add_argument("arg2", nargs="?").completer = require("arg1", arg2_completer)where require("arg1", arg2_completer) means:
“Only call
arg2_completerifarg1has already been assigned a non-default value.”
Otherwise, return [] so argcomplete has nothing to merge from that positional.
def require(required_attr, inner_completer):
"""Return a completer that runs only if parsed_args.<required_attr> is set."""
def wrapper(prefix, parsed_args, **kwargs):
# getattr defaults to None if arg not present
value = getattr(parsed_args, required_attr, None)
# You can also check for the default specifically if needed
if value in (None, ''):
return [] # don't yield anything
# delegate to inner completer
return inner_completer(prefix, parsed_args, **kwargs)
return wrapperimport argparse, argcomplete
def arg1_completer(prefix, **_):
return ["one", "two", "three"]
def arg2_completer(prefix, **_):
return ["alpha", "beta", "gamma"]
def require(required_attr, inner_completer):
def wrapper(prefix, parsed_args, **kwargs):
if getattr(parsed_args, required_attr, None) in (None, ''):
return []
return inner_completer(prefix, parsed_args, **kwargs)
return wrapper
parser = argparse.ArgumentParser()
parser.add_argument("arg1", nargs="?", default=None).completer = arg1_completer
parser.add_argument("arg2", nargs="?", default=None).completer = require("arg1", arg2_completer)
argcomplete.autocomplete(parser)
args = parser.parse_args()
print(args)| Command line | Completion results |
|---|---|
cmd <TAB> |
only options + arg1 completions |
cmd one <TAB> |
arg2 completions |
cmd one beta <TAB> |
none |
That fixes the “merged completions” problem elegantly.
If you have many cases, you can generalize this:
def conditional_completer(condition, inner_completer):
def wrapper(prefix, parsed_args, **kwargs):
if not condition(parsed_args):
return []
return inner_completer(prefix, parsed_args, **kwargs)
return wrapperThen you can express:
parser.add_argument("arg2", nargs="?").completer = conditional_completer(
lambda args: args.arg1 is not None and args.arg1 != '',
arg2_completer,
)parsed_argsis exactly whatargparse.parse_known_args()produced for the partial command line at completion time — so if the user hasn’t typed anything forarg1yet, it will still beNoneor its default.- Returning an empty list tells argcomplete “this completer yields nothing,” which effectively suppresses the merged results.
- You can combine this with more complex logic — e.g. check that
arg1’s value matches a particular prefix before enablingarg2completions.
✅ In summary
Yes — the “require” completer factory pattern works beautifully for this case:
def require(attr, inner):
def wrapper(prefix, parsed_args, **kw):
if getattr(parsed_args, attr, None) in (None, ''):
return []
return inner(prefix, parsed_args, **kw)
return wrapperIt gives you fine-grained control over when completions for later optional positionals appear, fixing the “merged completions” problem while keeping both args optional in the actual CLI.
Would you like me to show a variant where require() also checks that the previous arg’s value is not the default (i.e. parsed_args.arg1 != parser.get_default("arg1"))? That’s sometimes useful when your optional args have defaults that shouldn’t count as “filled.”
Perfect 👍 — let’s extend the require() factory so that it also checks whether the “required” argument has been explicitly provided (not just filled with its default value).
That way, completions for the next positional (e.g. arg2) only trigger after the user has typed a real value for arg1.
def require(required_attr, inner_completer, parser=None):
"""
Only run `inner_completer` if parsed_args.<required_attr> is set
AND differs from its default (if parser provided).
"""
def wrapper(prefix, parsed_args, **kwargs):
val = getattr(parsed_args, required_attr, None)
# Check against parser's default (if parser is available)
if parser is not None:
default = parser.get_default(required_attr)
else:
default = None
# Skip if not set or only default
if val in (None, '') or val == default:
return []
return inner_completer(prefix, parsed_args, **kwargs)
return wrapperimport argparse, argcomplete
def arg1_completer(prefix, **_):
return ["one", "two", "three"]
def arg2_completer(prefix, **_):
return ["alpha", "beta", "gamma"]
parser = argparse.ArgumentParser()
parser.add_argument("arg1", nargs="?", default="foo").completer = arg1_completer
parser.add_argument("arg2", nargs="?", default="bar").completer = require("arg1", arg2_completer, parser)
argcomplete.autocomplete(parser)
args = parser.parse_args()
print(args)| Command line | parsed_args.arg1 |
Completions shown | Why |
|---|---|---|---|
cmd <TAB> |
"foo" (default) |
only options + arg1 completions |
require() sees value = default → skip arg2 |
cmd one <TAB> |
"one" |
arg2 completions (alpha, beta, gamma) |
explicit non-default value |
cmd one beta <TAB> |
"one", "beta" |
none | all filled |
If you like syntactic sugar, you can even wrap parser registration:
def add_arg_with_require(parser, name, *a, require_arg=None, completer=None, **kw):
arg = parser.add_argument(name, *a, **kw)
if completer:
if require_arg:
arg.completer = require(require_arg, completer, parser)
else:
arg.completer = completer
return argUsage:
add_arg_with_require(parser, "arg1", nargs="?", default="foo", completer=arg1_completer)
add_arg_with_require(parser, "arg2", nargs="?", default="bar", require_arg="arg1", completer=arg2_completer)✅ Summary
Your improved require() pattern now:
-
suppresses premature completions for ambiguous optional positionals,
-
only activates the dependent completer when the prior argument:
- is present and
- differs from its default,
-
works seamlessly with
argcomplete.
Would you like me to show how to combine this with dynamic completions (e.g. where arg2’s completions depend on the value of arg1)?
Perfect — let’s extend your setup one step further:
now arg2’s completions will depend on the actual value of arg1, and won’t activate until arg1 is explicitly set (not just the default).
This pattern is super common for CLIs like:
mytool project <project> <environment>where the environments available depend on the chosen project.
import argparse
import argcomplete
# --- Example dynamic data source ---
# Let's pretend each project has its own environments
PROJECTS = {
"alpha": ["dev", "staging", "prod"],
"beta": ["test", "prod"],
"gamma": ["sandbox", "prod"],
}
# --- Base completers ---
def project_completer(prefix, **_):
"""Completes the first positional argument."""
return [p for p in PROJECTS if p.startswith(prefix)]
def environment_completer(prefix, parsed_args, **_):
"""Completes the second positional based on selected project."""
project = getattr(parsed_args, "project", None)
if not project or project not in PROJECTS:
return []
return [env for env in PROJECTS[project] if env.startswith(prefix)]
# --- The require() factory (improved) ---
def require(required_attr, inner_completer, parser=None):
"""
Only run `inner_completer` if parsed_args.<required_attr> is set
and not equal to its default value (if parser given).
"""
def wrapper(prefix, parsed_args, **kwargs):
val = getattr(parsed_args, required_attr, None)
default = parser.get_default(required_attr) if parser else None
if val in (None, '') or val == default:
return [] # suppress completions until requirement satisfied
return inner_completer(prefix, parsed_args, **kwargs)
return wrapper
# --- CLI definition ---
parser = argparse.ArgumentParser(prog="mytool")
parser.add_argument("project", nargs="?", default=None).completer = project_completer
parser.add_argument("environment", nargs="?", default=None).completer = require(
"project", environment_completer, parser
)
# Enable argcomplete
argcomplete.autocomplete(parser)
# Regular program logic
args = parser.parse_args()
print(f"Selected project: {args.project}, environment: {args.environment}")| Command | Completion result | Reason |
|---|---|---|
mytool <TAB> |
alpha beta gamma |
Completes project names |
mytool a<TAB> |
alpha |
Partial prefix |
mytool alpha <TAB> |
dev staging prod |
require() sees project set ⇒ runs environment_completer |
mytool beta <TAB> |
test prod |
Dynamically filtered by project |
mytool <TAB> (no arg1) |
no environments | require() suppresses premature completions |
-
You can chain this pattern for deeper hierarchies:
arg3.completer = require("arg2", arg3_completer, parser)
-
Works equally for optional positionals (
nargs="?") or flags (--something). -
require()prevents the “merged” completion problem and enables clean dependencies.
- Avoids noise: completions appear only when logically possible.
- Dynamically adapts based on previous argument values.
- Pure Python — no shell logic needed.
- Keeps your parser definition declarative.
Would you like me to show the minimal bash setup (how to enable and test argcomplete completions for this script interactively)?