mrkeyoor.com_
Wed 23 Sept 00:34 UTC
PyPICLI & Toolingupdated 21 Sept 2026

questionary review

Questionary 2.1.1 wraps `prompt_toolkit` with ready-made text, password, path, confirm, select, raw-select, checkbox, autocomplete, and keypress prompts. It can combine named questions into a form, validate or filter answers, return application values behind friendly labels, apply terminal styles, and run prompts through sync or async methods. Version 2.1.1 fixes an `AttributeError` triggered by `prompt_toolkit` 3.0.52 and raises the supported Python floor to 3.9. Our install confirmed a typed pure-Python package rather than a command parser or full terminal UI framework.

Verdict

Questionary 2.1.1 installed in 0.4 seconds and used 4 MB across 3 packages in our sandbox, with `py.typed` and 0 pip-audit findings; it fits human-run Python CLIs that need richer prompts than `input()`. Keep a flags-based path for automation, use async prompts inside event loops, and treat `None` as a normal cancellation result.

We installed it

Lab card: what happened when we installed questionaryScreenshot of questionary documentation
Install✓ · 0.4s3 packages on disk · 4 MB
Importimport questionary in 0.55s · pure Python · py.typed · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does questionary install cleanly?

Yes. In a fresh container with an empty cache, pip install questionary finished in 0.4s, leaving 3 packages and 4 MB on disk. pip-audit reported no known vulnerabilities.

What does questionary need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import questionary succeeded in 0.55s, and the package ships py.typed for type checkers.

questionary or InquirerPy: which should you use?

InquirerPy: You want fuzzy selection and a larger Inquirer-style prompt set. Questionary 2.1.1 installed in 0.4 seconds and used 4 MB across 3 packages in our sandbox, with py.typed and 0 pip-audit findings; it fits human-run Python CLIs that need richer prompts than input().

When should you not use questionary?

The program runs unattended in CI, cron, or redirected input: Questionary assumes an interactive terminal, so flags, environment variables, or a defined stdin format are safer

API stability4/5Version 2.1.1 retains the established builder functions, `Question` ask methods, `Choice`, `Style`, `form`, and dictionary question format. The release changes only Python support and compatibility with a prompt_toolkit layout API. PyPI still marks the project Beta, answer types intentionally accept arbitrary values, and the dependency range spans prompt_toolkit 2.x and 3.x, so terminal rendering and cancellation deserve regression tests when the dependency moves.
Docs4/5Read the Docs covers every prompt family, safe and unsafe cancellation, async calls, validation, styling, conditional questions, forms, and dictionary workflows. It specifically warns that checkbox does not accept the standard Validator interface and explains `patch_stdout`. The weak spots are operational: non-TTY behavior, screen-reader use, and automated interaction testing get little direct treatment, and some API returns remain broad enough that examples carry more meaning than annotations.
Maintenance4/5PyPI published 2.1.1 on August 28, 2025, GitHub shows an August 18, 2026 push, and the repository is not archived. It has 2,165 stars and 40 open issues after pull requests are excluded. The current release fixed a concrete prompt_toolkit 3.0.52 break and added Python 3.13 testing. Releases are infrequent for a small wrapper, and the Beta classifier remains, but repository work and dependency compatibility are current.
Ecosystem4/5PyPI reports 5,968,154 downloads in the latest week. Questionary uses prompt_toolkit's terminal engine, works beside argparse and Click, and covers the common prompt shapes without taking over command routing. `Choice` values let applications keep domain IDs behind readable labels. Its scope stays narrow: shell completion, command parsing, full TUI layout, and noninteractive automation belong to other packages, while prompt_toolkit behavior still defines the supported terminals.

Use it if

  • A human-operated Python CLI needs arrow-key selection, checkboxes, autocomplete, validation, or styled prompts
  • You want several named questions collected into one answer dictionary without building a prompt_toolkit layout
  • An asyncio application needs to await a terminal question through `ask_async()`
  • Displayed choice labels should stay readable while the program receives stable IDs or Python objects
Skip it if

Setup reality

Our fresh Python 3.12 install of Questionary 2.1.1 succeeded in 0.4 seconds. It left 3 packages taking 4 MB, declared 1 direct dependency, imported as questionary in 0.55 seconds, and had 0 known vulnerabilities in pip-audit. The package is pure Python, requires Python 3.9 or newer, uses the MIT license, and includes py.typed. Its single runtime dependency is prompt_toolkit 2.x or 3.x below 4.0.

Questionary does not parse commands. Keep argparse, Click, or Typer responsible for flags, help output, exit codes, and a noninteractive path. Each builder returns a Question; the terminal opens only when .ask(), .unsafe_ask(), or an async equivalent runs. A normal .ask() catches Ctrl+C and returns None, while a form returns an empty dictionary. Use the unsafe methods when cancellation should raise KeyboardInterrupt and terminate the command.

The display needs a working TTY. Tests should patch the Questionary call at your application boundary or use prompt_toolkit's test input and output helpers instead of waiting for a keyboard. Logs from other threads can overwrite an active prompt; patch_stdout=True routes output around the prompt renderer. Terminal color support varies, and any style token you omit falls back to Questionary's default style.

Inside an event loop, call await question.ask_async() because .ask() blocks the loop. Validation runs as the user edits, so a database or network check will make keystrokes lag. Choice(title=..., value=...) separates labels from stored values, while separators and disabled choices do not become answers. Form names are plain dictionary keys, which means misspellings and conditional references fail at runtime rather than in the type checker.

Patterns

Collect and validate text ask-text

import questionary

name = questionary.text(
    "Project name:",
    validate=lambda value: True if value.strip() else "Enter a name",
).ask()
if name is None:
    raise SystemExit(130)

Safe `.ask()` returns `None` after Ctrl+C, so handle cancellation before using the answer.

Read a hidden secret ask-password

import questionary

token = questionary.password("API token:").ask()
if token is None:
    raise SystemExit(130)

Password masking protects the display, but the returned string is still a secret in process memory and should not be logged.

Confirm a destructive operation confirm-action

import questionary

confirmed = questionary.confirm(
    "Delete the local cache?",
    default=False,
).ask()
if confirmed:
    delete_cache()

A safe confirm can return `None` on cancellation as well as `False` for rejection; both paths avoid the action in this example.

Show labels and return stable values return-choice-value

import questionary
from questionary import Choice

environment = questionary.select(
    "Deploy where?",
    choices=[
        Choice("Development", value="dev"),
        Choice("Staging", value="staging"),
        Choice("Production", value="prod"),
    ],
).ask()

`Choice.value` is returned instead of the visible title, which keeps application logic independent of display wording.

Collect multiple choices select-many

import questionary
from questionary import Choice

features = questionary.checkbox(
    "Enable features:",
    choices=[
        Choice("Audit log", value="audit", checked=True),
        Choice("Email reports", value="email"),
        Choice("Legacy export", disabled="Unavailable"),
    ],
).ask()

Disabled choices cannot be selected, and the advanced docs exclude checkbox from the standard Validator-class path.

Autocomplete from known values autocomplete-value

import questionary

region = questionary.autocomplete(
    "Region:",
    choices=["us-east-1", "us-west-2", "eu-west-1"],
    validate=lambda value: value in {"us-east-1", "us-west-2", "eu-west-1"},
).ask()

Completion narrows suggestions, while validation decides whether arbitrary typed text is accepted.

Prompt for an existing file ask-path

from pathlib import Path
import questionary

config_path = questionary.path(
    "Config file:",
    validate=lambda value: Path(value).is_file() or "Choose an existing file",
).ask()

Path completion only suggests filesystem entries; it does not prove the final text names an existing file.

Return named answers from a form collect-form

import questionary

answers = questionary.form(
    service=questionary.text("Service name:"),
    replicas=questionary.text("Replica count:", default="2"),
    deploy=questionary.confirm("Deploy now?", default=False),
).ask()
if not answers:
    raise SystemExit(130)

A cancelled form returns an empty dictionary, and its keys come directly from the names passed to `form()`.

Skip a question based on an earlier answer ask-conditionally

import questionary

answers = questionary.prompt([
    {"type": "confirm", "name": "deploy", "message": "Deploy now?"},
    {
        "type": "select",
        "name": "region",
        "message": "Region:",
        "choices": ["us-east-1", "eu-west-1"],
        "when": lambda current: current["deploy"],
    },
])

`when` receives answers collected so far, so it can only reference names that appear earlier in the list.

Await a prompt in an event loop ask-asynchronously

import asyncio
import questionary

async def choose_environment():
    return await questionary.select(
        "Environment:",
        choices=["dev", "staging", "prod"],
    ).ask_async()

answer = asyncio.run(choose_environment())

Use `ask_async()` inside asyncio code; synchronous `.ask()` blocks the event loop while the terminal waits.

Let Ctrl+C terminate the command propagate-cancellation

import questionary

try:
    answer = questionary.text("Release tag:").unsafe_ask()
except KeyboardInterrupt:
    print()
    raise SystemExit(130)

`unsafe_ask()` propagates `KeyboardInterrupt`; ordinary `.ask()` catches it and returns `None`.

Keep background output from tearing the prompt protect-prompt-output

import questionary

answer = questionary.select(
    "Active job:",
    choices=["build", "test", "deploy"],
).ask(patch_stdout=True)

`patch_stdout=True` lets prompt_toolkit route concurrent prints above the active prompt instead of overwriting its display.

Alternatives

PackageRegistryPick it when
InquirerPyPyPIYou want fuzzy selection and a larger Inquirer-style prompt set
prompt-toolkitPyPIYou need custom buffers, layouts, completions, or key bindings below Questionary's preset prompts
clickPyPICommand routing, options, generated help, and simple prompts matter more than interactive selection widgets
richPyPIYour main task is formatted output, progress, tables, or status displays rather than collecting answers

More cli & tooling guides

chalk · commander · typescript · esbuild · yargs · click · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.