Claude Code for questionary: Interactive Python CLI Prompts — Claude Skills 360 Blog
Blog / AI / Claude Code for questionary: Interactive Python CLI Prompts
AI

Claude Code for questionary: Interactive Python CLI Prompts

Published: February 4, 2028
Read time: 5 min read
By: Claude Skills 360

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.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free