CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-questionary

Python library to build pretty command line user prompts with interactive forms and validation

Overall
score

96%

Overview
Eval results
Files

forms.mddocs/

Forms and Multi-Question Workflows

Forms enable multi-question workflows that execute a series of prompts and return consolidated results, supporting field validation, conditional logic, and both synchronous and asynchronous execution.

Capabilities

Form Creation

Create forms from multiple Question instances with named fields for organized data collection.

def form(**kwargs: Question) -> Form:
    """
    Create a form from keyword arguments mapping field names to Questions.
    
    Args:
        **kwargs: Field names mapped to Question instances
        
    Returns:
        Form instance ready for execution
    """

class FormField(NamedTuple):
    key: str
    question: Question
    """
    Named tuple representing a question within a form.
    
    Attributes:
        key: Field identifier for result dictionary
        question: Question instance to execute
    """

Form Execution

Execute forms with comprehensive error handling and result consolidation.

class Form:
    def __init__(self, *form_fields: FormField) -> None:
        """
        Initialize form with FormField instances.
        
        Args:
            *form_fields: FormField instances defining the form structure
        """

    def ask(self, patch_stdout: bool = False, 
            kbi_msg: str = DEFAULT_KBI_MESSAGE) -> Dict[str, Any]:
        """
        Execute form synchronously with error handling.
        
        Args:
            patch_stdout: Patch stdout to prevent interference
            kbi_msg: Message displayed on keyboard interrupt
            
        Returns:
            Dictionary mapping field keys to user responses
            
        Raises:
            KeyboardInterrupt: User cancelled with appropriate message
        """

    def unsafe_ask(self, patch_stdout: bool = False) -> Dict[str, Any]:
        """
        Execute form synchronously without error handling.
        
        Args:
            patch_stdout: Patch stdout to prevent interference
            
        Returns:
            Dictionary mapping field keys to user responses
        """

    def ask_async(self, patch_stdout: bool = False, 
                  kbi_msg: str = DEFAULT_KBI_MESSAGE) -> Dict[str, Any]:
        """
        Execute form asynchronously with error handling.
        
        Args:
            patch_stdout: Patch stdout to prevent interference
            kbi_msg: Message displayed on keyboard interrupt
            
        Returns:
            Dictionary mapping field keys to user responses
        """

    def unsafe_ask_async(self, patch_stdout: bool = False) -> Dict[str, Any]:
        """
        Execute form asynchronously without error handling.
        
        Args:
            patch_stdout: Patch stdout to prevent interference
            
        Returns:
            Dictionary mapping field keys to user responses
        """

Usage Examples

Basic Form Creation

import questionary

# Create form using keyword arguments
user_info = questionary.form(
    name=questionary.text("Enter your name:"),
    email=questionary.text("Enter your email:"),
    age=questionary.text("Enter your age:"),
    confirmed=questionary.confirm("Is this information correct?")
).ask()

print(f"Name: {user_info['name']}")
print(f"Email: {user_info['email']}")
print(f"Age: {user_info['age']}")
print(f"Confirmed: {user_info['confirmed']}")

Advanced Form with Validation

import questionary
from questionary import Validator, ValidationError

# Custom validators
class EmailValidator(Validator):
    def validate(self, document):
        if "@" not in document.text:
            raise ValidationError(message="Please enter a valid email")

class AgeValidator(Validator):
    def validate(self, document):
        try:
            age = int(document.text)
            if age < 0 or age > 120:
                raise ValidationError(message="Please enter a valid age (0-120)")
        except ValueError:
            raise ValidationError(message="Please enter a number")

# Form with validated fields
registration = questionary.form(
    username=questionary.text(
        "Choose username:",
        validate=lambda x: "Username too short" if len(x) < 3 else True
    ),
    email=questionary.text(
        "Enter email:",
        validate=EmailValidator()
    ),
    age=questionary.text(
        "Enter age:",
        validate=AgeValidator()
    ),
    terms=questionary.confirm(
        "Do you accept the terms?",
        default=False
    )
).ask()

if not registration['terms']:
    print("Registration cancelled - terms not accepted")
else:
    print("Registration successful!")

Complex Form with Conditional Logic

import questionary

# First collect basic info
basic_info = questionary.form(
    user_type=questionary.select(
        "Account type:",
        choices=["personal", "business", "developer"]
    ),
    name=questionary.text("Full name:")
).ask()

# Conditional follow-up questions based on user type
if basic_info['user_type'] == 'business':
    business_info = questionary.form(
        company=questionary.text("Company name:"),
        employees=questionary.select(
            "Company size:",
            choices=["1-10", "11-50", "51-200", "200+"]
        ),
        industry=questionary.text("Industry:")
    ).ask()
    
    # Merge results
    result = {**basic_info, **business_info}
    
elif basic_info['user_type'] == 'developer':
    dev_info = questionary.form(
        languages=questionary.checkbox(
            "Programming languages:",
            choices=["Python", "JavaScript", "Java", "C++", "Go", "Rust"]
        ),
        experience=questionary.select(
            "Years of experience:",
            choices=["0-2", "3-5", "6-10", "10+"]
        )
    ).ask()
    
    result = {**basic_info, **dev_info}
    
else:
    result = basic_info

print("Collected information:", result)

Async Form Execution

import questionary
import asyncio

async def async_form_example():
    # Forms can be executed asynchronously
    config = await questionary.form(
        host=questionary.text("Database host:", default="localhost"),
        port=questionary.text("Database port:", default="5432"),
        database=questionary.text("Database name:"),
        ssl=questionary.confirm("Use SSL?", default=True)
    ).ask_async()
    
    print(f"Connecting to {config['host']}:{config['port']}")
    return config

# Run async form
# config = asyncio.run(async_form_example())

Form with Manual FormField Construction

import questionary
from questionary import FormField

# Manual FormField creation for more control
form_fields = [
    FormField("project_name", questionary.text("Project name:")),
    FormField("description", questionary.text("Description:", multiline=True)),
    FormField("license", questionary.select(
        "License:",
        choices=["MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause"]
    )),
    FormField("public", questionary.confirm("Make repository public?"))
]

project_form = questionary.Form(*form_fields)
project_config = project_form.ask()

print("Project configuration:")
for key, value in project_config.items():
    print(f"  {key}: {value}")

Error Handling in Forms

import questionary

try:
    settings = questionary.form(
        debug=questionary.confirm("Enable debug mode?"),
        log_level=questionary.select(
            "Log level:",
            choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
        ),
        max_connections=questionary.text(
            "Max connections:",
            validate=lambda x: "Must be a number" if not x.isdigit() else True
        )
    ).ask()
    
    print("Settings configured:", settings)
    
except KeyboardInterrupt:
    print("\nConfiguration cancelled by user")
except Exception as e:
    print(f"Configuration error: {e}")

Form with Skip Logic

import questionary

# Initial question to determine workflow
workflow = questionary.select(
    "What would you like to do?",
    choices=["create", "update", "delete"]
).ask()

if workflow == "create":
    create_form = questionary.form(
        name=questionary.text("Resource name:"),
        type=questionary.select(
            "Resource type:",
            choices=["database", "server", "storage"]
        ),
        region=questionary.select(
            "Region:",
            choices=["us-east-1", "us-west-2", "eu-west-1"]
        )
    ).ask()
    
    print(f"Creating {create_form['type']} '{create_form['name']}' in {create_form['region']}")

elif workflow == "update":
    update_form = questionary.form(
        resource_id=questionary.text("Resource ID to update:"),
        changes=questionary.checkbox(
            "What to update:",
            choices=["configuration", "scaling", "security", "tags"]
        )
    ).ask()
    
    print(f"Updating resource {update_form['resource_id']}: {update_form['changes']}")

elif workflow == "delete":
    delete_form = questionary.form(
        resource_id=questionary.text("Resource ID to delete:"),
        confirmation=questionary.text(
            "Type 'DELETE' to confirm:",
            validate=lambda x: True if x == "DELETE" else "Please type 'DELETE' to confirm"
        )
    ).ask()
    
    if delete_form['confirmation'] == "DELETE":
        print(f"Deleting resource {delete_form['resource_id']}")

Install with Tessl CLI

npx tessl i tessl/pypi-questionary

docs

autocomplete-paths.md

core-classes.md

execution-control.md

forms.md

index.md

selection-prompts.md

text-input.md

tile.json