questionary builds interactive CLI prompts. pip install questionary. Text: import questionary; answer = questionary.text("Name?").ask(). Password: questionary.password("Password?").ask(). Confirm: questionary.confirm("Continue?").ask() → True/False. Confirm default: questionary.confirm("Delete?", default=False).ask(). Select: questionary.select("Pick env:", choices=["dev","staging","prod"]).ask(). Checkbox: questionary.checkbox("Select features:", choices=["api","db","cache"]).ask() → list. Autocomplete: questionary.autocomplete("Search:", choices=["apple","banana","cherry"]).ask(). Path: questionary.path("Config file:").ask(). Rawselect: numbered list — questionary.rawselect("Action:", choices=["build","test","deploy"]).ask(). Default: questionary.text("Name?", default="Alice").ask(). Validate: questionary.text("Email?", validate=lambda v: "@" in v or "Invalid email").ask(). Style: from questionary import Style; custom = Style([("question","fg:cyan bold"),("answer","fg:green")]). questionary.text("Q?", style=custom).ask(). Choice with description: from questionary import Choice; Choice("production", value="prod", description="Live environment"). Instruction: questionary.text("Name?", instruction="(first and last)").ask(). Unsafe: questionary.text("Q?").unsafe_ask() — raises KeyboardInterrupt instead of returning None. Chained: questionary.form(name=questionary.text("Name?"), env=questionary.select("Env?", choices=["dev","prod"])).ask() → dict. Claude Code generates questionary prompt sequences, validators, and form flows.
CLAUDE.md for questionary
## questionary Stack
- Version: questionary >= 2.0 | pip install questionary
- Text: questionary.text("Prompt?", default="val").ask() → str | None
- Select: questionary.select("Pick:", choices=["a","b","c"]).ask()
- Checkbox: questionary.checkbox("Multi:", choices=[...]).ask() → list
- Validate: questionary.text("Email?", validate=lambda v: "@" in v or "msg")
- Form: questionary.form(k1=questionary.text(...), k2=questionary.select(...)).ask()
- Ctrl-C: .ask() returns None, .unsafe_ask() raises KeyboardInterrupt
questionary Interactive Prompt Pipeline
# app/prompts.py — questionary interactive CLI patterns
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import Any
import questionary
from questionary import Choice, Style
# ─────────────────────────────────────────────────────────────────────────────
# Custom style
# ─────────────────────────────────────────────────────────────────────────────
APP_STYLE = Style([
("qmark", "fg:#5f87ff bold"), # ? mark
("question", "bold"), # question text
("answer", "fg:#5fffaf bold"), # selected answer
("pointer", "fg:#5f87ff bold"), # > pointer
("highlighted", "fg:#5f87ff bold"), # highlighted choice
("selected", "fg:#5fffaf"), # checkbox selected
("separator", "fg:#6c6c6c"), # separator line
("instruction", "fg:#6c6c6c"), # instruction text
("text", ""),
("disabled", "fg:#6c6c6c italic"),
])
# ─────────────────────────────────────────────────────────────────────────────
# Validators
# ─────────────────────────────────────────────────────────────────────────────
def validate_not_empty(value: str) -> bool | str:
return True if value.strip() else "This field cannot be empty."
def validate_email(value: str) -> bool | str:
if "@" in value and "." in value.split("@")[-1]:
return True
return "Please enter a valid email address."
def validate_port(value: str) -> bool | str:
try:
port = int(value)
if 1 <= port <= 65535:
return True
return "Port must be between 1 and 65535."
except ValueError:
return "Port must be a number."
def validate_path_exists(value: str) -> bool | str:
if Path(value).exists():
return True
return f"Path does not exist: {value}"
# ─────────────────────────────────────────────────────────────────────────────
# 1. Single prompts
# ─────────────────────────────────────────────────────────────────────────────
def prompt_project_name() -> str | None:
"""Text prompt with non-empty validation."""
return questionary.text(
"Project name:",
validate=validate_not_empty,
instruction="(no spaces, use-hyphens)",
style=APP_STYLE,
).ask()
def prompt_secret() -> str | None:
"""Password prompt — input is hidden."""
return questionary.password(
"Enter API key:",
validate=validate_not_empty,
style=APP_STYLE,
).ask()
def prompt_confirm(message: str, default: bool = True) -> bool:
"""Yes/No prompt — returns bool (default True if user presses Enter)."""
result = questionary.confirm(message, default=default, style=APP_STYLE).ask()
return result if result is not None else False
def prompt_environment() -> str | None:
"""Single-choice select menu."""
return questionary.select(
"Target environment:",
choices=[
Choice("Development", value="dev", description="Local dev server"),
Choice("Staging", value="staging", description="Pre-production"),
Choice("Production", value="prod", description="Live — requires confirmation"),
],
style=APP_STYLE,
).ask()
def prompt_features() -> list[str]:
"""Multi-select checkbox."""
result = questionary.checkbox(
"Select features to enable:",
choices=[
Choice("REST API", value="api", checked=True),
Choice("Database", value="db", checked=True),
Choice("Redis cache", value="cache"),
Choice("Task queue", value="queue"),
Choice("Email service", value="email"),
Choice("OAuth2", value="oauth"),
],
instruction="(space to toggle, enter to confirm)",
style=APP_STYLE,
).ask()
return result or []
def prompt_database_url() -> str | None:
"""Autocomplete with predefined common values."""
return questionary.autocomplete(
"Database URL:",
choices=[
"postgresql://localhost/myapp_dev",
"postgresql://localhost/myapp_test",
"sqlite:///./dev.db",
"sqlite:///:memory:",
],
validate=validate_not_empty,
style=APP_STYLE,
).ask()
def prompt_config_file() -> str | None:
"""File path prompt — auto-completes filesystem paths."""
return questionary.path(
"Config file path:",
only_directories=False,
style=APP_STYLE,
).ask()
# ─────────────────────────────────────────────────────────────────────────────
# 2. Form — collect multiple answers in one call
# ─────────────────────────────────────────────────────────────────────────────
def prompt_server_config() -> dict | None:
"""
questionary.form() collects multiple prompts and returns a dict.
If the user cancels any field, the entire form returns None.
"""
return questionary.form(
host=questionary.text(
"Host:",
default="0.0.0.0",
validate=validate_not_empty,
style=APP_STYLE,
),
port=questionary.text(
"Port:",
default="8000",
validate=validate_port,
style=APP_STYLE,
),
workers=questionary.select(
"Workers:",
choices=["1", "2", "4", "8", "auto"],
default="auto",
style=APP_STYLE,
),
debug=questionary.confirm(
"Enable debug mode?",
default=False,
style=APP_STYLE,
),
).ask()
# ─────────────────────────────────────────────────────────────────────────────
# 3. Conditional wizard — multi-step flow
# ─────────────────────────────────────────────────────────────────────────────
def setup_wizard() -> dict:
"""
Step-by-step setup wizard — each question can branch based on prior answers.
.ask() returns None on Ctrl-C; check and exit gracefully.
"""
config: dict[str, Any] = {}
# Step 1
name = questionary.text("Application name:", validate=validate_not_empty,
style=APP_STYLE).ask()
if name is None:
sys.exit(0)
config["name"] = name.strip()
# Step 2
env = questionary.select(
"Environment:",
choices=["development", "staging", "production"],
style=APP_STYLE,
).ask()
if env is None:
sys.exit(0)
config["env"] = env
# Step 3 — conditional: prod requires extra confirmation
if env == "production":
confirmed = questionary.confirm(
"Deploying to PRODUCTION. Are you sure?",
default=False,
style=APP_STYLE,
).ask()
if not confirmed:
print("Aborted.")
sys.exit(0)
# Step 4
features = prompt_features()
config["features"] = features
# Step 5 — show only if db feature selected
if "db" in features:
db_url = prompt_database_url()
if db_url is None:
sys.exit(0)
config["database_url"] = db_url
return config
# ─────────────────────────────────────────────────────────────────────────────
# 4. Numbered rawselect — no arrow keys needed
# ─────────────────────────────────────────────────────────────────────────────
def prompt_action() -> str | None:
"""
rawselect shows numbered options — user types a number and presses Enter.
Useful for accessibility or remote terminals where arrow keys don't work.
"""
return questionary.rawselect(
"Choose action:",
choices=[
Choice("Build", value="build"),
Choice("Test", value="test"),
Choice("Deploy", value="deploy"),
Choice("Rollback",value="rollback"),
],
style=APP_STYLE,
).ask()
# ─────────────────────────────────────────────────────────────────────────────
# 5. Non-interactive / testing mode
# ─────────────────────────────────────────────────────────────────────────────
def is_interactive() -> bool:
"""Return False when running in CI or piped input — skip prompts."""
return sys.stdin.isatty() and sys.stdout.isatty()
def get_env_or_prompt(env_var: str, prompt: str, **kwargs) -> str:
"""
Read from environment variable first (for CI), fall back to interactive prompt.
This pattern makes scripts work both interactively and in automation.
"""
if (val := os.environ.get(env_var)):
return val
if not is_interactive():
raise RuntimeError(f"{env_var} not set and no interactive terminal available")
result = questionary.text(prompt, style=APP_STYLE, **kwargs).ask()
if result is None:
sys.exit(0)
return result
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__" and is_interactive():
print("=== questionary demo (interactive) ===")
print("Running setup wizard...")
cfg = setup_wizard()
print(f"\nConfiguration collected:")
for k, v in cfg.items():
print(f" {k}: {v}")
else:
print("questionary: non-interactive environment — demo skipped.")
print("Run as 'python prompts.py' in an interactive terminal to see prompts.")
For the input() alternative — Python’s built-in input() is a single-line text prompt with no history, no tab completion, no validation loop, no multi-select, no password masking, and no styling, while questionary’s checkbox() renders a navigable list where the user toggles items with Space, autocomplete() filters choices as the user types, password() masks characters with *, and validate= keeps the prompt open and shows an inline error message until the input is valid — the interactive UX difference between input("features?") and a questionary checkbox is the difference between a script and a polished CLI tool. For the PyInquirer alternative — PyInquirer is questionary’s predecessor and shares the same Inquirer.js API surface area, but has not received updates since 2020 and does not support Python 3.10+ type syntax or the form() multi-question collector, while questionary is actively maintained, supports questionary.form() for collecting multiple fields atomically, provides typed Choice objects with description tooltips in select(), and integrates cleanly with Click via @click.command wrappers. The Claude Skills 360 bundle includes questionary skill sets covering text/password/confirm/select/checkbox/autocomplete/path/rawselect prompt types, validate= inline validation with custom messages, default= values, instruction= hint text, Question.ask() vs unsafe_ask() for Ctrl-C handling, Style customization, Choice with value and description, questionary.form() multi-field collection, conditional wizard branching, and get_env_or_prompt for CI-compatible scripts. Start with the free tier to try interactive CLI prompt code generation.