Right, so you’ve built a CLI. It’s got flags, it’s got options, it’s got --help text that would make a technical writer weep with joy. But it’s missing something. It’s… polite. It waits for you to tell it exactly what to do. What if you want a conversation? What if you want an application that prompts the user for input, offers choices, validates on the fly, and maybe even has some snazzy syntax highlighting? You don’t want a patient butler; you want a brilliant co-pilot.

That’s where prompt_toolkit comes in. It is, without exaggeration, the library you didn’t know you needed until you use it, and then you wonder how you ever lived without it. It’s the foundation upon which tools like ipython and mycli are built. Think of it as not just a way to get input, but a full-fledged framework for building rich, interactive command-line applications. It handles all the nasty terminal escape sequences and cross-platform compatibility headaches so you can focus on the logic.

Why Not Just input()?

Because input() is a bicycle and prompt_toolkit is a sports car. The built-in input() is fine for asking “what’s your name?” but the moment you need to do anything more complex—like offer tab-completion, validate input as it’s typed, or provide a multi-line editing experience—you’re suddenly building that sports car from scratch, with spare parts, and blindfolded.

prompt_toolkit gives you power steering, anti-lock brakes, and a turbocharger. It provides:

  • History: Up-arrow brings back previous entries.
  • Auto-suggestion: Grayed-out text suggests completions based on history.
  • Syntax Highlighting: You can make your prompts pretty and informative.
  • Multi-line Input: Edit long chunks of text comfortably.
  • Custom Key Bindings: Redefine what any key does.
  • Validation: Check input as the user types.

Trying to do this with raw input() and curses is a one-way ticket to madness. Trust me, I’ve been there. The terminal is a capricious beast; let prompt_toolkit tame it for you.

Your First Prompt: Beyond “Hello, World”

Let’s start simple, but immediately useful. We’ll create a prompt that uses a default value, a feature input() can’t do without you writing a bunch of logic yourself.

from prompt_toolkit import prompt
from prompt_toolkit.history import InMemoryHistory

# A simple prompt with a default value
name = prompt('What is your name (default: Anon)?: ', default="Anon")
print(f"Hello, {name}")

# A prompt with history (even in-memory history is useful)
history = InMemoryHistory()
command = prompt('SQL> ', history=history)
print(f"You executed: {command}")

This code is deceptively simple. The default="Anon" parameter is already a huge UX win. If the user just hits enter, they get the sensible default. The InMemoryHistory object instantly gives your prompt a memory, making it feel like a real REPL.

The Real Magic: Prompt Toolkit as an Application Framework

The prompt() function is just the tip of the iceberg. The real power is in building full, stateful applications. This is where you create things like custom database clients or interactive configuration wizards.

Let’s build a small application that presents a dropdown list of choices. This is infinitely better than making the user type a string exactly right.

from prompt_toolkit import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import Layout, VSplit, HSplit, Window
from prompt_toolkit.widgets import RadioList, Button, Dialog, Label
from prompt_toolkit.styles import Style

# Define our choices. The value is the return value, the string is the label.
choices = [
    ("bash", "Bourne-Again Shell (bash)"),
    ("zsh", "Z Shell (zsh)"),
    ("fish", "Friendly Interactive Shell (fish)"),
]

# Create a RadioList widget
radio_list = RadioList(choices)

# Create a simple dialog layout
def ok_clicked():
    application.exit(result=radio_list.current_value)

def cancel_clicked():
    application.exit()

ok_button = Button(text="OK", handler=ok_clicked)
cancel_button = Button(text="Cancel", handler=cancel_clicked)

dialog = Dialog(
    title="Choose your preferred shell",
    body=HSplit([radio_list]),
    buttons=[ok_button, cancel_button],
    width=60,
    height=10,
)

layout = Layout(dialog)

# Key bindings to handle navigation beyond just the widgets
kb = KeyBindings()

@kb.add("c-c")
def _(event):
    """Pressing Ctrl-C will exit the application."""
    application.exit()

# Build the application
application = Application(
    layout=layout,
    key_bindings=kb,
    style=Style.from_dict({
        'dialog': 'bg:#88ff88',
        'button': 'bg:#884444 #ffffff',
        'button.focused': 'bg:#ff0000 bold',
    }),
    mouse_support=True,  # Because we can, and it's awesome
)

# Run it and get the result
result = application.run()
print(f"You chose: {result}")

This is a lot, I know. But look at what you get: a fully navigable UI with keyboard and mouse support, focus management, a coherent style, and clean separation of layout and logic. You’re building a TUI (Text-based User Interface), not just a script. The user can’t type an invalid choice because the RadioList widget prevents them from doing so. That’s robust design.

The Killer Feature: Auto-completion and Validation

This is where prompt_toolkit moves from “cool” to “indispensable.” You can provide context-aware tab-completion and validate input as the user is typing it.

from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit.document import Document

# Define a list of commands we auto-complete
command_completer = WordCompleter(['insert', 'select', 'update', 'delete', 'drop'], ignore_case=True)

# Define a validator to check for non-empty input
class NonEmptyValidator(Validator):
    def validate(self, document: Document):
        text = document.text
        if not text.strip():
            raise ValidationError(
                message="Input cannot be empty. Don't just hit enter!",
                cursor_position=len(text),
            )

# Use it all together
try:
    user_input = prompt(
        'SQL> ',
        completer=command_completer,
        validator=NonEmptyValidator(),
        validate_while_typing=True,  # This is the magic sauce
    )
except KeyboardInterrupt:
    # Handle Ctrl-C gracefully
    user_input = "EXIT"
    print("\nGoodbye!")

print(f"Executing: {user_input}")

The validate_while_typing=True parameter is a game-changer. The user gets immediate, inline feedback if they’ve done something wrong. It turns a frustrating “run-it-and-see-it-fail” cycle into a smooth, guided experience. The WordCompleter is just one type; you can build vastly more complex completers that understand context, like the current word position in a SQL statement.

Best Practices and Pitfalls

  1. Don’t Overdo It: The biggest pitfall is getting carried away. You’re building a CLI, not a web app. Use these rich features where they dramatically improve the user experience (e.g., for commands with complex syntax or destructive operations like delete), not for every single input. A simple input() is still the right tool for a “yes/no” question.

  2. Performance Matters: If your completer has to search a list of 100,000 items, it’s going to feel laggy. Make your completers efficient. Use FuzzyCompleter to wrap another completer if you want fuzzy matching, but be aware of the performance cost on very large datasets.

  3. Accessibility: Remember that not all users can or want to use the mouse. Ensure every feature is accessible via the keyboard. prompt_toolkit’s focus-based navigation is great for this.

  4. Test, Test, Test: The variety of terminals (Windows Terminal, iTerm2, GNOME Terminal, the basic Windows CMD) is a nightmare of slightly different behaviors. Test your application on the terminals your users are likely to use. prompt_toolkit does the heavy lifting, but it’s not a perfect abstraction.

prompt_toolkit is the secret weapon for making your CLI tools feel modern, intuitive, and powerful. It acknowledges that the command line isn’t just for automation; it’s also for interaction. And it gives you the tools to make that interaction brilliant.