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.
| Tool | Purpose |
|---|---|
input(prompt)built-in | Single-line interactive input. |
cmd.Cmdclass | Build a command REPL from methods. |
readlinemodule | Line editing and history on POSIX. |
cursesmodule | Full-screen terminal apps. |
richlibrary | Colour, tables, progress, syntax highlighting. |
textualframework | Modern full-screen TUI apps. |
tqdmlibrary | Progress bars for any iterable. |
tabulatelibrary | 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.