mrkeyoor.com_
Sat 08 Aug 21:00 UTC
PyPICLI & Toolingupdated 08 Aug 2026

questionary

Questionary is a Python wrapper around prompt_toolkit for interactive terminal questions. It supplies text, password, path, confirmation, single-select, raw-select, checkbox, autocomplete, and press-any-key prompts, plus forms that collect multiple answers into a dictionary. It adds validation, conditional questions, display labels with separate returned values, styling, synchronous and asynchronous execution, and consistent Ctrl+C handling without requiring callers to assemble prompt_toolkit applications themselves.

Verdict

Questionary is a good middle layer for polished, human-driven Python prompts without becoming a full terminal UI project. Keep a noninteractive path for automation, handle None after cancellation, and use a real CLI framework around it.

API stability4/5The public builder pattern, Question.ask methods, Choice, Style, form, and dictionary prompt format are established and still documented in version 2.1.1. The package is nevertheless classified Beta on PyPI, return types remain dynamic, and behavior depends on a broad prompt_toolkit range from 2.x through 3.x, so terminal edge cases deserve tests when dependencies move.
Docs4/5Read the Docs provides a quickstart, question-type reference, validation, safe and unsafe cancellation, async use, styling, conditional flows, and dictionary configuration. The repository also includes focused examples for every prompt family. API pages rely heavily on generated signatures, and operational topics such as non-TTY failure and automated prompt testing get less direct guidance.
Maintenance4/5The repository was pushed on 2026-07-22 and version 2.1.1 was uploaded on 2025-08-28, with declared support through Python 3.13. GitHub reports 65 open issues and pull requests. Activity is current enough for a small mature wrapper, though releases are not frequent and the Beta classifier remains, so pin prompt_toolkit-sensitive production CLIs.
Ecosystem4/5Questionary builds on prompt_toolkit, works beside common parsers such as Click and argparse, and covers the prompt types most CLI applications ask for. Weekly downloads are measured in millions and the repository has 2,150 stars. Its ecosystem is intentionally narrower than a command framework or full TUI toolkit, and it does not own shell completion or command routing.

Use it if

  • You are building a human-run Python CLI that needs arrows, checkboxes, autocomplete, validation, and styled prompts
  • You want several related questions returned as one named answer dictionary
  • Your CLI already has an asyncio event loop and needs an awaitable prompt instead of blocking it
  • You need display-friendly choice titles while returning stable IDs or Python objects to application code
Skip it if

Setup reality

`pip install questionary` installs version 2.1.1 and its only runtime dependency, prompt_toolkit from 2.x or 3.x but below 4. Python 3.9 or newer is required by current package metadata. The first surprise is architectural: Questionary is not a command framework. You still need argparse, Click, or another layer for flags, help text, noninteractive operation, and exit codes. Every builder such as `questionary.text(...)` creates a Question; nothing is shown until `.ask()`, `.unsafe_ask()`, or an async variant runs. Safe Question ask methods catch Ctrl+C, print a cancellation message, and return None; Form.ask catches the same interrupt and returns an empty dictionary. Code must distinguish cancellation from a real answer. Unsafe methods let KeyboardInterrupt propagate when cancellation must abort the process. Interactive terminal behavior depends on prompt_toolkit and a usable TTY; automated tests should patch Questionary at the application boundary or exercise the prompt_toolkit test helpers rather than wait for real keyboard input. If logs or background threads print while a prompt is open, pass `patch_stdout=True` to keep the display from tearing. In async programs use `await question.ask_async()`; calling synchronous `.ask()` blocks the event loop. Choice objects matter when labels and stored values differ, and separators or disabled choices are not returned as answers. Validation runs while the user edits, so slow filesystem or network checks make typing feel broken. Dictionary workflows support `when`, `validate`, and `filter`, but misspelled names remain runtime errors and downstream code receives a plain dictionary. Styling uses prompt_toolkit token names, terminal color support varies, and unspecified tokens fall back to defaults.

Patterns

Ask for validated textask-text

import questionary

name = questionary.text(
    "Project name",
    validate=lambda value: True if value.strip() else "Name is required",
).ask()

if name is None:
    raise SystemExit(130)

Safe ask catches Ctrl+C and returns None. Check cancellation before treating the answer as text.

Confirm a destructive actionconfirm-action

import questionary

confirmed = questionary.confirm(
    "Delete the generated cache?",
    default=False,
).ask()

if confirmed is not True:
    print("Cancelled")

Use `is True`, because False means rejection while None means the user interrupted the prompt.

Show labels but return stable valuesselect-value

import questionary
from questionary import Choice

region = questionary.select(
    "Deployment region",
    choices=[
        Choice("US East (Virginia)", value="us-east-1"),
        Choice("EU West (Ireland)", value="eu-west-1"),
    ],
).ask()

A plain string is both title and value. Choice separates user-facing text from the value stored by the application.

Collect multiple checkbox choicesselect-multiple-values

import questionary
from questionary import Choice

features = questionary.checkbox(
    "Enable features",
    choices=[
        Choice("Audit log", value="audit", checked=True),
        Choice("Metrics", value="metrics"),
        Choice("Email alerts", value="email"),
    ],
).ask()

The advanced guide says checkbox does not accept Validator validation. Validate the returned list after the prompt if a minimum selection is required.

Prompt for a hidden password with validationask-password

import questionary

password = questionary.password(
    "Database password",
    validate=lambda value: True if len(value) >= 12 else "Use at least 12 characters",
).ask()

Hidden terminal input only prevents shoulder-surfing. Do not log the returned string or persist it in shell history or plain configuration.

Offer autocomplete while allowing searchautocomplete-input

import questionary

environment = questionary.autocomplete(
    "Environment",
    choices=["development", "staging", "production"],
    match_middle=True,
).ask()

Autocomplete is interactive terminal UI, not shell completion. Noninteractive invocations still need a flag or environment-variable path.

Prompt for an existing file pathask-file-path

import os
import questionary

config_path = questionary.path(
    "Configuration file",
    only_files=True,
    validate=lambda value: True if os.path.isfile(value) else "File does not exist",
).ask()

Path completion does not make the result valid. Validate existence and permissions for the exact operation you will perform.

Collect a typed form into a dictionaryask-form

import questionary

answers = questionary.form(
    name=questionary.text("Service name"),
    replicas=questionary.text(
        "Replica count", default="2",
        validate=lambda value: True if value.isdigit() else "Enter a whole number",
    ),
    deploy=questionary.confirm("Deploy now?", default=False),
).ask()

if not answers:
    raise SystemExit(130)
answers["replicas"] = int(answers["replicas"])

Form keys become dictionary keys. Form.ask returns an empty dictionary on Ctrl+C, unlike a single Question.ask, which returns None.

Ask conditional questions from dictionariesbuild-dynamic-workflow

import questionary

answers = questionary.prompt([
    {"type": "confirm", "name": "tls", "message": "Configure TLS?", "default": True},
    {
        "type": "text",
        "name": "hostname",
        "message": "Public hostname",
        "when": lambda previous: previous.get("tls") is True,
    },
])

Dictionary names and types are checked at runtime. A skipped question may be absent from the returned answers, so use get for conditional keys.

Skip a question when configuration already existsskip-known-answer

import questionary

port = questionary.text(
    "HTTP port", default="8080"
).skip_if(configured_port is not None, default=str(configured_port)).ask()

skip_if takes a boolean now, not a callback evaluated later. Its default is returned directly when the condition is true.

Prompt without blocking an asyncio loopask-with-asyncio

import asyncio
import questionary

async def choose_target():
    return await questionary.select(
        "Target", choices=["dev", "staging", "prod"]
    ).ask_async(patch_stdout=True)

answer = asyncio.run(choose_target())

Use ask_async inside an existing event loop. patch_stdout keeps concurrent log output from corrupting the active prompt.

Apply a reusable terminal stylestyle-prompts

import questionary
from questionary import Style

cli_style = Style([
    ("qmark", "fg:#5f87ff bold"),
    ("question", "bold"),
    ("answer", "fg:#00af87 bold"),
    ("pointer", "fg:#5f87ff bold"),
    ("disabled", "fg:#808080 italic"),
])

mode = questionary.select("Mode", ["safe", "fast"], style=cli_style).ask()

Terminal color support varies. Unspecified Questionary token classes keep their default styles.

Alternatives

PackageRegistryPick it when
InquirerPyPyPIYou want an Inquirer-style prompt set with fuzzy selection and a more feature-heavy interactive workflow
prompt-toolkitPyPIYou need custom terminal applications, buffers, key bindings, and layouts below Questionary's abstraction
clickPyPIYou primarily need commands, options, help generation, and simple confirm or prompt calls
richPyPIYour main need is formatted terminal output, progress, tables, and status displays rather than forms