Building Text-Based Interfaces

Not every program needs a graphical interface. For tools developers run in a terminal, text-based interfaces (TUIs) are faster to build, lighter to distribute, and easier to automate. Python has a rich ecosystem for them: plain print for simple scripts, argparse for CLIs, rich/textual for colour and layout, readline/cmd for interactive prompts, and curses for full-screen apps.

Start with structured output. Group related information with headings, align columns, colorize statuses. The standard library has no direct colour helper, but ANSI escape codes are well-supported on every modern terminal: "\033[32mOK\033[0m" prints “OK” in green. For anything more than a few colours, rich.console.Console is the de-facto choice — one dependency, dramatically better output.

For interactive prompts, input() is enough for one-off scripts; readline adds editing and history on POSIX systems automatically. For a custom REPL, cmd.Cmd takes care of parsing and dispatching commands. For a full-screen app (progress dashboards, editors, TUIs), textual is a modern framework; curses is the low-level alternative if you want no dependencies.

Progress feedback is where TUIs shine. A spinner, a percent bar, or a live table keeps users informed during long-running work. rich.progress or tqdm give you a professional-looking progress bar in one line. Even a bare \r + percentage is better than silent output.

Colour and tables

ANSI escapes: \033[31m red, \033[1m bold, \033[0m reset. Write a small helper def color(s, c): return f"\033[{c}m{s}\033[0m" to avoid sprinkling codes everywhere.

rich.table.Table, textual.widgets.DataTable, or tabulate render aligned tables with headers in a few lines. Reach for them as soon as columns become uneven.

Prompts and progress

input("? ") for single-line; getpass.getpass for secrets. Always validate and re-prompt on bad input.

rich.progress.Progress handles concurrent tasks with per-task bars; tqdm(iterable) wraps any iterator and shows an ETA.

TUI toolkit.

ToolPurpose
input(prompt)
built-in
Single-line interactive input.
cmd.Cmd
class
Build a command REPL from methods.
readline
module
Line editing and history on POSIX.
curses
module
Full-screen terminal apps.
rich
library
Colour, tables, progress, syntax highlighting.
textual
framework
Modern full-screen TUI apps.
tqdm
library
Progress bars for any iterable.
tabulate
library
Pretty-print tables from rows.

Building Text-Based Interfaces code example

The script uses plain ANSI escapes, a small table formatter, and a progress indicator — no third-party dependencies.

# Lesson: Building Text-Based Interfaces
import sys
import time


RESET = "\033[0m"
BOLD  = "\033[1m"
RED   = "\033[31m"
GREEN = "\033[32m"
YELLOW= "\033[33m"


def color(text: str, code: str) -> str:
    return f"{code}{text}{RESET}"


def table(rows: list[dict], headers: list[str]) -> str:
    widths = {h: max(len(h), *(len(str(r.get(h, ""))) for r in rows)) for h in headers}
    sep = "-+-".join("-" * widths[h] for h in headers)
    head = " | ".join(h.ljust(widths[h]) for h in headers)
    body = "\n".join(
        " | ".join(str(r.get(h, "")).ljust(widths[h]) for h in headers)
        for r in rows
    )
    return f"{head}\n{sep}\n{body}"


print(color("== Deployment Status ==", BOLD))

rows = [
    {"service": "api",    "status": color("OK",    GREEN),  "latency": "12ms"},
    {"service": "worker", "status": color("WARN",  YELLOW), "latency": "80ms"},
    {"service": "db",     "status": color("DOWN",  RED),    "latency": "-"},
]
print(table(rows, ["service", "status", "latency"]))

# Progress: \r redraws the same line
print("\nProcessing")
total = 20
for i in range(1, total + 1):
    pct = i * 100 // total
    bar = "#" * (i // 2) + "." * ((total - i) // 2)
    sys.stdout.write(f"\r  [{bar}] {pct:3d}%")
    sys.stdout.flush()
    time.sleep(0.01)
print("  done.")

# Interactive prompt with fallback default
def ask(prompt: str, default: str) -> str:
    try:
        ans = input(f"{prompt} [{default}]: ").strip()
    except EOFError:
        ans = ""
    return ans or default


# In a demo we don't block; in real code you'd call ask(...) here.
print(color("\nTips: Ctrl-C aborts, empty input uses the default.", BOLD))

Key moves:

1) ANSI codes in a helper keep formatting out of business logic.
2) A simple `table()` function produces aligned output without a dependency.
3) `\r` + `sys.stdout.flush()` is the minimal progress bar.
4) `input()` with a default value makes scripts easy to automate.

Use tqdm for a real progress bar.

# Requires: pip install tqdm
from tqdm import tqdm
import time

for _ in tqdm(range(30), desc="downloading"):
    time.sleep(0.01)

Verify the table formatter.

def fmt(rows, hs):
    ws = {h: max(len(h), *(len(str(r.get(h, ""))) for r in rows)) for h in hs}
    return " | ".join(h.ljust(ws[h]) for h in hs)
assert fmt([{"a": 1, "b": 2}], ["a", "b"]) == "a | b"

Running prints (colours elided here):

== Deployment Status ==
service | status | latency
--------+--------+--------
api     | OK     | 12ms
worker  | WARN   | 80ms
db      | DOWN   | -

Processing
  [##########..........] 100%  done.

Tips: Ctrl-C aborts, empty input uses the default.