Besides of that I support the idea of having constant values like window's height, width and input_row settings as class constants (uppercase names) and referencing negative kcell flag as not kcell instead of kcell == False here's some list of advises in terms of better code organizing, restructuring conditionals and eliminating duplicates:

  • window's half of width int(self.winwidth / 2) is calculated 13 times across 3 methods.
    Instead, we'll apply Extract variable technique, extracting precalculated expression into the instance field self.win_width_half = int(self.winwidth / 2) and referencing it in all places it's needed
    (like self.inputcol = [[36, 50], [self.win_width_half + 36, self.win_width_half + 50]])

  • setting "windows entry points" in initScreen method (should be renamed to init_screen): involves consecutive 20 calls of self.measurewin.addstr(...) function.
    Instead, we can define entry points attributes beforehand and then pass them in simple iteration:

    win_entry_points_attrs = [
        (2, self.inputcol[0][0] - 3, "1", self.white),
        (2, self.inputcol[0][1] - 3, "2", self.white),
        (2, self.inputcol[1][0] - 3, "1", self.white),
        (2, self.inputcol[1][1] - 3, "2", self.white),
    
        (5, 5, "A"),
        (9, 5, "B"),
        (13, 5, "C"),
        (17, 5, "D"),
        (16, 20, "D.I"),
        (18, 20, "D.II"),
        (22, 5, "E"),
        (21, 20, "E.I"),
        (23, 20, "E.II"),
    
        (5, self.win_width_half + 5, "F"),
        (9, self.win_width_half + 5, "G"),
        (13, self.win_width_half + 5, "H"),
        (12, self.win_width_half + 20, "H.I"),
        (14, self.win_width_half + 20, "H.II"),
        (18, self.win_width_half + 5, "J"),
        (17, self.win_width_half + 20, "J.I"),
        (19, self.win_width_half + 20, "J.II"),
    ]
    # print the windows entry points
    for attrs in win_entry_points_attrs:
        self.measurewin.addstr(*attrs) 
    

Optimizations within inputMeasures method (should be renamed to input_measures):

  • the condition:

    if ((ch == curses.KEY_UP and j > 0)
            or (ch == curses.KEY_DOWN and kcell == False)
            or (ch == curses.KEY_LEFT and (i != 0 or k != 0) and kcell == False)
            or (ch == curses.KEY_RIGHT and (i != 1 or k != 1) and kcell == False)
            or (ch in [ord("s"), ord("S")])
            or (ch in [ord("q"), ord("Q")])):
        break
    

    has a common check kcell == False (should be not kcell) in 3 branches (in the middle) and that's a sign of Consolidate conditional refactoring technique:

    if ((ch == curses.KEY_UP and j > 0)
            or (not kcell and (ch == curses.KEY_DOWN
                                or (ch == curses.KEY_LEFT and (i != 0 or k != 0))
                                or (ch == curses.KEY_RIGHT and (i != 1 or k != 1))))
            or (chr(ch).lower() in ("s", "q"))):
        break
    
  • the variable cursorcntr (cursor counter) deserves for a more meaningful variable name (Rename variable technique) - I would suggest cursor_cnt

  • the last complex if .. elif .. elif conditional of 6 branches at the end of method input_measures seems to be a good candidate for Replace Conditional with Polymorphism technique (OOP approach) but that would require more knowledge and vision about your program conception/settings.
    For now, the first 4 branches of that conditional perform the same action cursor_cnt *= 2 - we can eliminate duplication with additional check.
    It's good to move "save/quit" branches up, as they call self.exit() which will throw raise SystemExit to exit the program.
    Thus the reorganized conditional would look as:

    # set of key codes defined in the parent scope (at least before the loop or as instance variable)
    key_set = set((curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT))
    ...
    # check If the user wants to save/quit
    inp_char = chr(ch).lower()
    if inp_char == 's':
        self.exit("save")
    elif inp_char == 'q':
        self.exit("quit")
    
    if ch in key_set:
        cursor_cnt *= 2
    
    if ch == curses.KEY_UP:
        if kcell:
            kcell = False
        else:
            self.cirow -= 1
        # If I pressed an arrow key the value of the cursor counter is always set to a
        # multiple of two (which means that when an arrow key is entered I will always
        # get a blinking cursos in the destination cell)
    
    elif ch == curses.KEY_DOWN:
        if (i == 0 and j == 6) or (i == 1 and j == 5):
            kcell = True
        else:
            self.cirow += 1
    
    elif ch == curses.KEY_LEFT:
        self.cicol -= 1
        if i == 1 and k == 0:
            self.ciside -= 1
            self.cicol += 2
    
    elif ch == curses.KEY_RIGHT:
        self.cicol += 1
        if i == 0 and k == 1:
            self.ciside += 1
            self.cicol -= 2
            if self.cirow == 6:
                self.cirow -= 1
    
Answer from RomanPerekhrest on Stack Exchange
🌐
Python documentation
docs.python.org › 3 › howto › curses.html
Curses Programming with Python — Python 3.14.3 documentation
For example, ACS_PLMINUS is a +/- symbol, and ACS_ULCORNER is the upper left corner of a box (handy for drawing borders). You can also use the appropriate Unicode character. Windows remember where the cursor was left after the last operation, ...
🌐
GitHub
gist.github.com › claymcleod › b670285f334acd56ad1c
Python curses example · GitHub
I also thank Clay for his very nice code example. I took it on myself to improve it a little by adding code to more cleanly clear out the keycode line(s) and to get all three curses "key read" functions to show the results for getch(), get_wch() and getkey() all for the same single keystroke entered.
Discussions

Any examples of slick curses user interfaces built with Python? (post screenshots please!)
you should check out blessed . It's based on blessings . Which has a slick progress bar written for it called progressive . More on reddit.com
🌐 r/Python
11
38
September 22, 2015
I'm trying to make a fancy CLI interface in python. Curses is very confusing (and I can't find a good tutorial!) Any suggestions?
To be precise with terms—what you are asking is a textual user interface (TUI) , not command-line interface (CLI) . And, yes, blessings looks like the best candidate for the job. More on reddit.com
🌐 r/Python
24
24
October 7, 2012
Top answer
1 of 2
7

Besides of that I support the idea of having constant values like window's height, width and input_row settings as class constants (uppercase names) and referencing negative kcell flag as not kcell instead of kcell == False here's some list of advises in terms of better code organizing, restructuring conditionals and eliminating duplicates:

  • window's half of width int(self.winwidth / 2) is calculated 13 times across 3 methods.
    Instead, we'll apply Extract variable technique, extracting precalculated expression into the instance field self.win_width_half = int(self.winwidth / 2) and referencing it in all places it's needed
    (like self.inputcol = [[36, 50], [self.win_width_half + 36, self.win_width_half + 50]])

  • setting "windows entry points" in initScreen method (should be renamed to init_screen): involves consecutive 20 calls of self.measurewin.addstr(...) function.
    Instead, we can define entry points attributes beforehand and then pass them in simple iteration:

    win_entry_points_attrs = [
        (2, self.inputcol[0][0] - 3, "1", self.white),
        (2, self.inputcol[0][1] - 3, "2", self.white),
        (2, self.inputcol[1][0] - 3, "1", self.white),
        (2, self.inputcol[1][1] - 3, "2", self.white),
    
        (5, 5, "A"),
        (9, 5, "B"),
        (13, 5, "C"),
        (17, 5, "D"),
        (16, 20, "D.I"),
        (18, 20, "D.II"),
        (22, 5, "E"),
        (21, 20, "E.I"),
        (23, 20, "E.II"),
    
        (5, self.win_width_half + 5, "F"),
        (9, self.win_width_half + 5, "G"),
        (13, self.win_width_half + 5, "H"),
        (12, self.win_width_half + 20, "H.I"),
        (14, self.win_width_half + 20, "H.II"),
        (18, self.win_width_half + 5, "J"),
        (17, self.win_width_half + 20, "J.I"),
        (19, self.win_width_half + 20, "J.II"),
    ]
    # print the windows entry points
    for attrs in win_entry_points_attrs:
        self.measurewin.addstr(*attrs) 
    

Optimizations within inputMeasures method (should be renamed to input_measures):

  • the condition:

    if ((ch == curses.KEY_UP and j > 0)
            or (ch == curses.KEY_DOWN and kcell == False)
            or (ch == curses.KEY_LEFT and (i != 0 or k != 0) and kcell == False)
            or (ch == curses.KEY_RIGHT and (i != 1 or k != 1) and kcell == False)
            or (ch in [ord("s"), ord("S")])
            or (ch in [ord("q"), ord("Q")])):
        break
    

    has a common check kcell == False (should be not kcell) in 3 branches (in the middle) and that's a sign of Consolidate conditional refactoring technique:

    if ((ch == curses.KEY_UP and j > 0)
            or (not kcell and (ch == curses.KEY_DOWN
                                or (ch == curses.KEY_LEFT and (i != 0 or k != 0))
                                or (ch == curses.KEY_RIGHT and (i != 1 or k != 1))))
            or (chr(ch).lower() in ("s", "q"))):
        break
    
  • the variable cursorcntr (cursor counter) deserves for a more meaningful variable name (Rename variable technique) - I would suggest cursor_cnt

  • the last complex if .. elif .. elif conditional of 6 branches at the end of method input_measures seems to be a good candidate for Replace Conditional with Polymorphism technique (OOP approach) but that would require more knowledge and vision about your program conception/settings.
    For now, the first 4 branches of that conditional perform the same action cursor_cnt *= 2 - we can eliminate duplication with additional check.
    It's good to move "save/quit" branches up, as they call self.exit() which will throw raise SystemExit to exit the program.
    Thus the reorganized conditional would look as:

    # set of key codes defined in the parent scope (at least before the loop or as instance variable)
    key_set = set((curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT))
    ...
    # check If the user wants to save/quit
    inp_char = chr(ch).lower()
    if inp_char == 's':
        self.exit("save")
    elif inp_char == 'q':
        self.exit("quit")
    
    if ch in key_set:
        cursor_cnt *= 2
    
    if ch == curses.KEY_UP:
        if kcell:
            kcell = False
        else:
            self.cirow -= 1
        # If I pressed an arrow key the value of the cursor counter is always set to a
        # multiple of two (which means that when an arrow key is entered I will always
        # get a blinking cursos in the destination cell)
    
    elif ch == curses.KEY_DOWN:
        if (i == 0 and j == 6) or (i == 1 and j == 5):
            kcell = True
        else:
            self.cirow += 1
    
    elif ch == curses.KEY_LEFT:
        self.cicol -= 1
        if i == 1 and k == 0:
            self.ciside -= 1
            self.cicol += 2
    
    elif ch == curses.KEY_RIGHT:
        self.cicol += 1
        if i == 0 and k == 1:
            self.ciside += 1
            self.cicol -= 2
            if self.cirow == 6:
                self.cirow -= 1
    
2 of 2
8

I can't run this unfortunately. curses seems to have issues on Windows. I'll just focus mainly on style and design.

There's a few notable things about this chunk:

# I also exit the loop if one of the following conditions if verified
if ((ch == curses.KEY_UP and j > 0)
or (ch == curses.KEY_DOWN and kcell == False)
or (ch == curses.KEY_LEFT and (i != 0 or k != 0) and kcell == False)
or (ch == curses.KEY_RIGHT and (i != 1 or k != 1) and kcell == False)
or (ch in [ord("s"), ord("S")])
or (ch in [ord("q"), ord("Q")])):
    break
  • == False should really just be not instead
  • You need more indentation. It's confusing to see the ors aligned with the ifs. I'd indent it at least all the way up to align with the opening brace.
  • Those two ch in checks at the bottom could be cleaned up.

I'd write this closer to:

if ((ch == curses.KEY_UP and j > 0)
        or (ch == curses.KEY_DOWN and not kcell)
        or (ch == curses.KEY_LEFT and (i != 0 or k != 0) and not kcell)
        or (ch == curses.KEY_RIGHT and (i != 1 or k != 1) and not kcell)
        or (chr(ch).lower() in {"s", "q"})):
    break

I think you could probably factor out the not kcell check too, but my tired brain can't think of a good way at the moment.

There is another approach though that lets you skip all the ors: any. any is like a function version of or (and all is like and). You could change this to:

if any([ch == curses.KEY_UP and j > 0,
        ch == curses.KEY_LEFT and (i != 0 or k != 0) and not kcell,
        ch == curses.KEY_RIGHT and (i != 1 or k != 1) and not kcell,
        chr(ch).lower() in {"s", "q"}]):
    break

Normally, I'd say that this is an abuse of any, but chaining ors over multiple lines like you had isn't ideal either.


In terms of design decisions, does this really need to be a class? Honestly, I'd probably store the necessary state using a dataclass or NamedTuple (or a couple distinct states), then just pass around and alter state(s) as needed. Making self a grab-bag of everything you may need seem messy to me.

For example, winwidth and winheight appear to be constants. They never change throughout your program, so they should treated as constants. I'd have them outside at the top as:

WINDOW_WIDTH = 120
WINDOW_HEIGHT = 29

And do ciside, cicol and cirow need to be attributes of the class as well? It seems like they're only used in inputMeasures, so why aren't they just variables local to that function? By having them "global" within the object, you're forcing your reader to keep them in the back of their mind in case those states are needed elsewhere in the object.


Finally, in terms of naming, you're violating PEP8. Variable and function names should be in snake_case unless you have a good reason, and class names should be UpperCamelCase.

🌐
DEV Community
dev.to › threadspeed › curses-with-python-3b3o
Curses with Python - DEV Community
May 12, 2020 - import sys,os import curses def draw_menu(stdscr): k = 0 cursor_x = 0 cursor_y = 0 # Clear and refresh the screen for a blank canvas stdscr.clear() stdscr.refresh() # Start colors in curses curses.start_color() curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLUE) curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_BLUE) curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_GREEN) # Loop where k is the last character pressed while (k != ord('q')): # Initialization stdscr.clear() height, width = stdscr.getmaxyx() if k == curses.KEY_DOWN: cursor_y = cursor_y + 1 elif k == curses.KEY_UP: curs
🌐
DZone
dzone.com › coding › languages › python curses, part 2: how to create a python curses-enabled application
Part 2: How to Create a Python curses-Enabled Application
April 22, 2025 - Learn how to use the Python curses library to draw text in Linux. Generate your first "Hello, World" example and master text positioning techniques in Python.
Find elsewhere
🌐
Gnosis
gnosis.cx › publish › programming › charming_python_6.html
CHARMING PYTHON #6 -- Curses programming in Python: Tips for Beginners --
For interactive text mode programs (under Linux/Unix), the ncurses library, and Python's standard curses module as a wrapper for it, are just what you need for your program. This article discusses the use of curses in Python, and uses example source code in the form of a front-end to the Txt2Html ...
🌐
Reddit
reddit.com › r/python › i'm trying to make a fancy cli interface in python. curses is very confusing (and i can't find a good tutorial!) any suggestions?
r/Python on Reddit: I'm trying to make a fancy CLI interface in python. Curses is very confusing (and I can't find a good tutorial!) Any suggestions?
October 7, 2012 -

As the title suggests, I'm trying to make an interactive CLI interface for a small python app I've written. Basically, I need to be able to print characters anywhere on the screen and change their colors.

I've tried messing around with curses, but the API is horrendously complex. It also seems next-to-impossible to find a tutorial for the python module. Can anybody suggest a decent alternative or a decent tutorial for curses?

🌐
Open Book Project
openbookproject.net › books › bpp4awd › app_b.html
Using Curses with Python — Beginning Python Programming for Aspiring Web Developers
import curses def main(stdscr): curses.noecho() stdscr.addstr(9, 25, "Enter a character or '!' to quite: ") stdscr.refresh() ch = stdscr.getch() while ch != ord('!'): stdscr.clear() stdscr.addstr(9, 25, "Enter a character or '!' to quite: ") stdscr.addstr( 12, 18, f"You entered '{chr(ch)}', which has a numeric value of {ch}." ) stdscr.move(9, 60) stdscr.refresh() ch = stdscr.getch() if __name__ == "__main__": curses.wrapper(main) This method returns the numeric value of the key pressed, so we need to use Python’s char function convert into a printable character. This next example, interrogate_env.py, uses several of the available curses functions to get information about the window environment:
Top answer
1 of 1
58

I really recommend you look into using panels. Anytime you will have widgets that could possibly overlap, it makes life alot easier. This is a simple example that should get you started. (Neither curses.beep() or curses.flash() seem to work on my terminal, but that is beside the point)

#!/usr/bin/env python

import curses
from curses import panel


class Menu(object):
    def __init__(self, items, stdscreen):
        self.window = stdscreen.subwin(0, 0)
        self.window.keypad(1)
        self.panel = panel.new_panel(self.window)
        self.panel.hide()
        panel.update_panels()

        self.position = 0
        self.items = items
        self.items.append(("exit", "exit"))

    def navigate(self, n):
        self.position += n
        if self.position < 0:
            self.position = 0
        elif self.position >= len(self.items):
            self.position = len(self.items) - 1

    def display(self):
        self.panel.top()
        self.panel.show()
        self.window.clear()

        while True:
            self.window.refresh()
            curses.doupdate()
            for index, item in enumerate(self.items):
                if index == self.position:
                    mode = curses.A_REVERSE
                else:
                    mode = curses.A_NORMAL

                msg = "%d. %s" % (index, item[0])
                self.window.addstr(1 + index, 1, msg, mode)

            key = self.window.getch()

            if key in [curses.KEY_ENTER, ord("\n")]:
                if self.position == len(self.items) - 1:
                    break
                else:
                    self.items[self.position][1]()

            elif key == curses.KEY_UP:
                self.navigate(-1)

            elif key == curses.KEY_DOWN:
                self.navigate(1)

        self.window.clear()
        self.panel.hide()
        panel.update_panels()
        curses.doupdate()


class MyApp(object):
    def __init__(self, stdscreen):
        self.screen = stdscreen
        curses.curs_set(0)

        submenu_items = [("beep", curses.beep), ("flash", curses.flash)]
        submenu = Menu(submenu_items, self.screen)

        main_menu_items = [
            ("beep", curses.beep),
            ("flash", curses.flash),
            ("submenu", submenu.display),
        ]
        main_menu = Menu(main_menu_items, self.screen)
        main_menu.display()


if __name__ == "__main__":
    curses.wrapper(MyApp)

Some things to note when looking over your code.

Using curses.wrapper(callable) to launch your application is cleaner than doing your own try/except with cleanup.

Your class calls initscr twice which will probably generate two screens (havent tested if it returns the same screen if its setup), and then when you have multiple menus there is no proper handling of (what should be) different windows/screens. I think its clearer and better bookkeeping to pass the menu the screen to use and let the menu make a subwindow to display in as in my example.

Naming a list 'list' isn't a great idea, because it shadows the list() function.

If you want to launch another terminal app like 'top', it is probably better to let python exit curses cleanly first then launch in order to prevent any futzing with terminal settings.

🌐
Lsu
ld2016.scusa.lsu.edu › python-2.7.8-docs-html › howto › curses.html
Curses Programming with Python — Python 2.7.8 documentation
September 20, 2014 - If it’s a string, you’re limited to displaying characters between 0 and 255. SVr4 curses provides constants for extension characters; these constants are integers greater than 255. For example, ACS_PLMINUS is a +/- symbol, and ACS_ULCORNER is the upper left corner of a box (handy for drawing ...
🌐
DZone
dzone.com › coding › languages › python curses, part 1: drawing with text
Python curses, Part 1: Drawing With Text
April 21, 2025 - While the Python curses module ... those errors occurred. For example, say a programmer tries to instantiate a Python curses window object with a size larger than the terminal window....
🌐
Andresromero
andresromero.github.io › Basic-ncurses-python-example
Basic Ncurses-Python app example
#!/usr/bin/env python from os import system import curses def get_param(prompt_string): screen.clear() screen.border(0) screen.addstr(2, 2, prompt_string) screen.refresh() input = screen.getstr(10, 10, 60) return input def execute_cmd(cmd_string): system("clear") a = system(cmd_string) print "" if a == 0: print "Command executed correctly" else: print "Command terminated with error" raw_input("Press enter") print "" x = 0 while x != ord('4'): screen = curses.initscr() screen.clear() screen.border(0) screen.addstr(2, 2, "Please enter a number...") screen.addstr(4, 4, "1 - Add a user") screen.ad
🌐
Python
docs.python.org › 3 › library › index.html
The Python Standard Library — Python 3.14.3 documentation
curses — Terminal handling for character-cell displays · curses.textpad — Text input widget for curses programs · curses.ascii — Utilities for ASCII characters · curses.panel — A panel stack extension for curses · cmd — Support for line-oriented command interpreters ·
🌐
Real Python
realpython.com
Python Tutorials – Real Python
Learn Python online: Python tutorials for developers of all skill levels, Python books and courses, Python news, code examples, articles, and more.
🌐
Python
docs.python.org › 3 › library › curses.html
curses — Terminal handling for character-cell displays
Writing outside the window, subwindow, or pad raises curses.error. Attempting to write to the lower right corner of a window, subwindow, or pad will cause an exception to be raised after the string is printed. A bug in ncurses, the backend for this Python module, can cause SegFaults when resizing windows.
🌐
GitHub
github.com › pmbarrett314 › curses-menu
GitHub - pmbarrett314/curses-menu: A simple console menu system in python using the curses library · GitHub
menu = CursesMenu("Root Menu", "Root Menu Subtitle") item1 = MenuItem("Basic item that does nothing", menu) function_item = FunctionItem("FunctionItem, get input", input, ["Enter an input: "]) print(__file__) command_item = CommandItem( "CommandItem that opens another menu", f"python {__file__}", ) submenu = CursesMenu.make_selection_menu([f"item{x}" for x in range(1, 20)]) submenu_item = SubmenuItem("Long Selection SubMenu", submenu=submenu, menu=menu) submenu_2 = CursesMenu("Submenu Title", "Submenu subtitle") function_item_2 = FunctionItem("Fun item", input, ["Enter an input"]) item2 = Menu
Starred by 477 users
Forked by 49 users
Languages   Python
🌐
Steven
steven.codes › blog › cs10 › curses-tutorial
Curses Tutorial • steven.codes
October 10, 2016 - It returns a value that we can compare like so: c = stdscr.getch() c == ord('a') # Do this to check for a letter key c == curses.KEY_UP # Do this to check for special keys See https://docs.python.org/3.5/library/curses.html#constants for all special key values.
🌐
YouTube
youtube.com › playlist
Python Curses Tutorial - YouTube
Share your videos with friends, family, and the world