Python curses: Build Terminal Interfaces

Published on: August 26, 2026
Reading time: 6 minutes
Vivid close-up of code on a computer screen showcasing programming details.

The curses module builds advanced text interfaces for character-cell terminals, including windows, colors, menus, keyboard and mouse input, partial screen updates, and resize-aware layouts. It uses curses or ncurses instead of hardcoded ANSI sequences.

It is a strong choice for local dashboards, process monitors, installers, file managers, administration tools, and programs used over SSH. Reliable software must restore the terminal after errors, respect capabilities reported by terminfo, and handle Unicode, resize events, and very small screens.

Availability

curses is an optional module normally available on Unix. It is not supported on Android, iOS, or WASI. Windows may require a compatible third-party package.

try:
    import curses
except ImportError:
    curses = None

Provide a clear fallback or non-interactive mode when the module is unavailable.

Start with wrapper()

curses.wrapper() initializes curses, enables cbreak, disables echo, enables keypad decoding, initializes colors when supported, and restores the terminal when the function exits. It also restores state before re-raising an exception.

import curses


def main(stdscr):
    stdscr.clear()
    stdscr.addstr(0, 0, "Hello, terminal")
    stdscr.refresh()
    stdscr.getch()

curses.wrapper(main)

Prefer this to manual initscr() and endwin(). A traceback appears only after the terminal is sane again.

Coordinates

Curses uses (y, x): row first, column second. The upper-left corner is (0, 0).

height, width = stdscr.getmaxyx()
y = height // 2
x = width // 2
stdscr.addstr(y, max(0, x - 5), "Centered")

Mixing x and y is a common source of out-of-bounds writes.

Bound every write

addstr() and addch() raise curses.error when drawing outside a window. Writing the lower-right cell can also raise after printing.

def safe_addstr(win, y, x, text, attr=0):
    height, width = win.getmaxyx()
    if not (0 <= y < height and 0 <= x < width):
        return
    available = max(0, width - x - 1)
    if available:
        win.addnstr(y, x, text, available, attr)

Treat a tiny terminal as a normal state rather than a crash condition.

An event loop

def main(stdscr):
    stdscr.keypad(True)
    while True:
        stdscr.erase()
        stdscr.addstr(0, 0, "Press q to quit")
        stdscr.refresh()

        key = stdscr.getch()
        if key in (ord("q"), ord("Q")):
            break

Long-running work should not block redraw and cancellation. Move it to workers or split it into short units.

Special keys

With keypad(True), terminal sequences become constants such as KEY_UP, KEY_DOWN, KEY_LEFT, and KEY_RIGHT.

if key == curses.KEY_UP:
    selected = max(0, selected - 1)
elif key == curses.KEY_DOWN:
    selected = min(len(items) - 1, selected + 1)

Not every terminal exposes every key. Avoid making one uncommon key the only way to use a feature.

getch(), getkey(), and get_wch()

getch() returns an integer. getkey() returns text or a key name. get_wch() is better for Unicode: ordinary input returns a character and special keys return integers.

value = stdscr.get_wch()
if isinstance(value, str):
    handle_text(value)
elif value == curses.KEY_RESIZE:
    needs_redraw = True

Configure locale before initializing curses.

Timed and non-blocking input

nodelay(True) makes getch() return -1 when no input is ready. timeout(ms) waits for a bounded interval.

stdscr.timeout(100)
while True:
    key = stdscr.getch()
    update_metrics()
    if key == ord("q"):
        break

Do not create a busy loop with zero timeout and no sleep or meaningful work.

halfdelay()

halfdelay(tenths) waits from 0.1 to 25.5 seconds and raises a curses error if no key arrives. Window-specific timeout() is often easier to integrate.

Escape timing

set_escdelay(ms) controls how long curses waits after ESC to distinguish a standalone Escape key from a function-key sequence. Tune it for local terminals and slower SSH links.

Windows

newwin() creates independent screen regions.

height, width = stdscr.getmaxyx()
menu = curses.newwin(height - 2, 30, 1, 0)
content = curses.newwin(height - 2, width - 30, 1, 30)
menu.box()
content.box()

Subwindows share storage with parents, so drawing and refresh order must be coordinated.

Pads

newpad() creates an area larger than the physical screen, useful for logs and documents.

pad = curses.newpad(1000, 200)
for index in range(1000):
    pad.addstr(index, 0, f"Line {index}")
pad.refresh(offset, 0, 1, 0, height - 2, width - 1)

Validate both the virtual and screen rectangles before refreshing.

Update several windows efficiently

Repeated refresh() calls can increase flicker. Call noutrefresh() for each window and then one doupdate().

menu.noutrefresh()
content.noutrefresh()
curses.doupdate()

Colors

Call start_color() and check has_colors().

if curses.has_colors():
    curses.start_color()
    curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
    stdscr.addstr(0, 0, "OK", curses.color_pair(1))

COLORS and COLOR_PAIRS are available after color initialization.

Default colors in Python 3.14

assume_default_colors(fg, bg), added in Python 3.14, assigns terminal-default colors and can preserve a transparent background.

if hasattr(curses, "assume_default_colors"):
    curses.assume_default_colors(-1, -1)
    curses.init_pair(1, curses.COLOR_CYAN, -1)

Provide a fallback for older Python and terminals lacking the extension.

Visual attributes

A_BOLD, A_REVERSE, A_UNDERLINE, and A_DIM can be combined with color pairs.

Do not communicate state through color alone. Some terminals lack attributes, and users may have color-vision limitations.

Unicode display width

Python string length is not terminal-cell width. East Asian characters may occupy two cells, while combining marks may occupy zero.

Use a width-aware library such as wcwidth for robust alignment, and test emoji and combining sequences.

Window encoding

Each window has an encoding attribute, normally derived from the locale.

import locale
locale.setlocale(locale.LC_ALL, "")

Set locale once before curses initialization rather than changing it during the event loop.

Resize events

A terminal resize may produce KEY_RESIZE. Query dimensions again, rebuild layout, and redraw from the application model.

if key == curses.KEY_RESIZE:
    curses.update_lines_cols()
    stdscr.erase()
    rebuild_layout(stdscr)

resizeterm() updates standard windows, but pads require application-specific handling.

Minimum dimensions

Define a supported minimum and display a simple message when the terminal is too small.

height, width = stdscr.getmaxyx()
if height < 10 or width < 40:
    safe_addstr(stdscr, 0, 0, "Please enlarge the terminal")
    stdscr.refresh()
    return

Mouse input

mousemask() enables requested events. After KEY_MOUSE, call getmouse().

Mouse support varies across terminal emulators, tmux, screen, and SSH. Always provide keyboard navigation.

Text fields

curses.textpad.Textbox provides Emacs-like editing. Validate final text and cap its size.

Python 3.14 raises the maximum getstr() and instr() length from 1023 to 2047 characters, but applications should still impose smaller domain limits.

Handle curses.error carefully

Small terminals and boundary writes can raise curses.error. Catch expected local cases, but avoid a global except curses.error: pass that hides layout bugs.

TERM and terminfo

Curses uses the terminfo entry selected by TERM. Incorrect values can produce broken keys, colors, and cursor movement.

Do not force xterm-256color blindly. Fix the environment or install the proper terminfo entry.

Do not mix raw ANSI output

Printing escape sequences directly while curses is active can desynchronize its virtual and physical screen models. Use curses functions for cursor movement, attributes, and clearing.

Subprocesses

Before running an interactive external program, temporarily restore shell mode or close and recreate the curses interface.

For isolated automation, see Python pty.

Relationship to tty and termios

Curses manages cbreak, echo, keypad, and terminal details above the APIs explained in Python tty and Python termios.

Avoid changing termios flags behind curses unless you also understand its internal state.

Threads

Keep all curses calls in one thread. Worker threads can publish model updates through a queue, while the UI thread reads and renders them.

Graceful shutdown

Set a stop flag or event, leave the event loop normally, and let wrapper() restore the terminal.

Signal handlers should remain minimal. Python signal explains how to wake the main loop safely.

Sanitize untrusted text

Remote data may contain controls, newlines, tabs, and escape characters that corrupt layout.

def sanitize_text(value):
    return "".join(ch if ch.isprintable() else "?" for ch in str(value))

Also cap length to avoid expensive redraws and unbounded memory.

Testing

Test tiny terminals, repeated resize, no colors, 8 and 256 colors, Unicode, SSH, tmux, screen, different keyboards, missing mouse, drawing exceptions, and Ctrl+C shutdown.

Use pty for integration tests, but verify rendering manually in real terminals because terminfo and curses implementations differ.

Architecture

Separate application state, commands, and rendering. Read a key, convert it into an action, update the model, and render from the model. Avoid embedding business logic directly in addstr() calls.

Common mistakes

Common failures include skipping wrapper(), drawing outside windows, assuming len() equals display width, relying only on color, ignoring resize, busy-looping, mixing raw ANSI output, calling curses from several threads, and displaying untrusted control characters.

Conclusion

curses creates efficient portable terminal interfaces based on real terminal capabilities. Use wrapper(), validate dimensions, batch refreshes, handle Unicode and resize, and keep the UI in one thread.

Test with actual terminals and terminfo entries, and offer a fallback when curses is unavailable. Consult the official curses documentation and ncurses(3X).

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python posix: Direct Unix System Calls

    Understand Python posix, Unix calls, descriptors, permissions, processes, security, and why most programs should use os instead.

    Ler mais

    Tempo de leitura: 6 minutos
    26/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python grp: Query Unix Groups

    Learn Python grp to query Unix groups, GIDs, members, ownership, supplementary groups, NSS, and container identities safely.

    Ler mais

    Tempo de leitura: 5 minutos
    26/08/2026
    Close-up of HTML and CSS code displayed on a computer screen, ideal for tech and programming themes.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pwd: Query Unix User Accounts

    Learn Python pwd to query Unix users by UID or login, retrieve home, shell, and ownership without using the database

    Ler mais

    Tempo de leitura: 5 minutos
    26/08/2026
    Stack of cut logs covered in snow, showcasing a cold winter texture and woody elements.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python syslog: Send Logs to Unix

    Learn Python syslog to send Unix logs with priorities, facilities, masks, structured content, and protection against log injection.

    Ler mais

    Tempo de leitura: 5 minutos
    26/08/2026
    High-resolution close-up of a CPU processor, RAM sticks, and a hard drive, showcasing modern computer hardware.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python resource: CPU and Memory Limits

    Learn Python resource to measure CPU, peak memory, page faults, and set limits for files, processes, descriptors, and address space

    Ler mais

    Tempo de leitura: 5 minutos
    26/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pty: Automate Unix Terminals

    Learn Python pty to run and test interactive programs, control pseudo-terminals, handle EOF, resize, signals, timeouts, and cleanup.

    Ler mais

    Tempo de leitura: 5 minutos
    25/08/2026