The tty module provides convenient functions for placing a Unix terminal in raw or cbreak mode. It builds on termios and saves applications from manually changing many flags for common tasks such as reading a key without waiting for Enter.
Raw and cbreak modes are useful in terminal games, interactive menus, hotkeys, administration tools, and full-screen interfaces. They are also risky when state is not restored: the user’s shell can remain without echo, signal processing, or normal key behavior.
Availability
tty is Unix-only because it depends on termios. Windows requires another API or a cross-platform library.
try:
import tty
except ImportError:
tty = None
The descriptor must also refer to a real terminal. Check os.isatty() before changing standard input or output.
Raw versus cbreak
In cbreak mode, characters are delivered immediately and echo is disabled, while important terminal processing remains active. Ctrl+C normally still generates SIGINT, and Enter retains the platform’s normal carriage-return mapping.
Raw mode delivers bytes with very little transformation. Signals, newline conversion, software flow control, and output processing may be disabled. It offers maximum control but requires much more parsing and cleanup.
setcbreak()
import os
import sys
import termios
import tty
fd = sys.stdin.fileno()
original = tty.setcbreak(fd)
try:
key = os.read(fd, 1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, original)
print(key)
Since Python 3.12, setcbreak() returns the original attributes. Earlier versions returned None, so libraries supporting older Python must call tcgetattr() first.
setraw()
original = tty.setraw(fd)
try:
data = os.read(fd, 32)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, original)
Raw mode disables more driver behavior. Use it only when the program must interpret every byte, including Ctrl+C and terminal control sequences.
Always restore in finally
Restoration must run after KeyboardInterrupt, parsing errors, EOF, and unexpected exceptions.
def read_key():
fd = sys.stdin.fileno()
original = tty.setcbreak(fd)
try:
return os.read(fd, 1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, original)
Do not rely only on atexit. It does not run after SIGKILL or some native crashes.
The when argument
setraw(fd, when=...) and setcbreak() pass when to tcsetattr(). The default is TCSAFLUSH, which waits for output and discards pending input.
Use TCSADRAIN when old keystrokes should not be silently discarded. Choose according to the interaction protocol.
cfmakeraw()
Python 3.12 adds cfmakeraw(mode), which edits an attribute list without applying it immediately.
attributes = termios.tcgetattr(fd)
updated = termios.tcgetattr(fd)
tty.cfmakeraw(updated)
updated[6][termios.VMIN] = 0
updated[6][termios.VTIME] = 5
termios.tcsetattr(fd, termios.TCSADRAIN, updated)
This makes it easy to start from raw mode and customize timeouts or selected flags.
cfmakecbreak()
cfmakecbreak(mode) clears ECHO and ICANON and sets VMIN to one byte with no delay.
updated = termios.tcgetattr(fd)
tty.cfmakecbreak(updated)
termios.tcsetattr(fd, termios.TCSADRAIN, updated)
Since Python 3.12.2, it no longer clears ICRNL. This matches historical behavior and the cbreak mode described by Linux, macOS, and BSD.
Version compatibility
if hasattr(tty, "cfmakecbreak"):
tty.cfmakecbreak(attributes)
else:
attributes[3] &= ~(termios.ECHO | termios.ICANON)
attributes[6][termios.VMIN] = 1
attributes[6][termios.VTIME] = 0
Preserve ICRNL in fallbacks if you want current cbreak semantics.
Arrow keys
An arrow key normally sends a sequence beginning with ESC. Reading one byte does not identify the full key.
first = os.read(fd, 1)
if first == b"\x1b":
remaining = os.read(fd, 2)
sequence = first + remaining
Sequences vary by terminal and modifier keys. Robust applications should use curses or a library backed by terminfo.
Distinguish Escape from a sequence
The Escape key shares its first byte with many control sequences. Use VMIN/VTIME or select to wait briefly for additional bytes.
A timeout that is too short fails over slow SSH connections, while one that is too long makes Escape feel delayed.
Unicode
Raw and cbreak reads return bytes. A Unicode character can use several UTF-8 bytes. Use an incremental decoder instead of decoding every byte separately.
import codecs
decoder = codecs.getincrementaldecoder("utf-8")()
text = decoder.decode(chunk)
The parser must distinguish terminal control sequences from encoded text.
Ctrl+C
In cbreak mode, ISIG normally remains enabled and Ctrl+C raises KeyboardInterrupt. In raw mode, \x03 may arrive as ordinary input.
If raw mode disables signals, provide a dependable exit key. Do not trap users in an interface with no escape path.
Ctrl+Z and suspension
In cbreak mode, Ctrl+Z may suspend the process. After resume, confirm the window size and redraw the interface.
In raw mode, Ctrl+Z may simply be an input byte.
Wait with select()
import select
readable, _, _ = select.select([fd], [], [], 0.5)
if readable:
chunk = os.read(fd, 1024)
else:
run_periodic_work()
Python select explains readiness and non-blocking I/O.
A context manager
from contextlib import contextmanager
@contextmanager
def cbreak_mode(fd):
original = tty.setcbreak(fd)
try:
yield
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, original)
For Python before 3.12, save the attributes before calling setcbreak().
stdin, stdout, or /dev/tty?
Standard input may be redirected even when the process has a controlling terminal. On Unix, /dev/tty opens that terminal directly.
with open("/dev/tty", "rb+", buffering=0) as terminal:
fd = terminal.fileno()
This can fail in services, background jobs, or containers without a TTY. Handle the error.
Subprocesses
A subprocess started while the terminal is raw inherits that state. Ordinary commands may behave unexpectedly.
Restore normal mode temporarily before launching a command or use a pseudo-terminal to isolate the child.
Threads
Terminal state belongs to the device, not a thread. Two threads switching raw and cbreak can restore values out of order. Centralize interactive input in one thread.
Output and cursor behavior
Raw mode may disable output processing. A newline may not return the cursor to the first column. Write \r\n when appropriate or preserve output flags if full raw behavior is unnecessary.
Resize
Use termios.tcgetwinsize() for rows and columns. After SIGWINCH, mark the layout dirty and redraw in the normal event loop.
Testing with pty
The pty module creates a pseudo-terminal for automated interactive tests. It helps verify that code enters and leaves raw or cbreak mode without affecting the developer’s real TTY.
Recovery
If a bug leaves the shell without echo, type stty sane and press Enter even if the characters are invisible.
Security
Raw mode is not secure password masking. The program can still record every byte. Also avoid printing untrusted escape sequences, which can manipulate the terminal, title, or clipboard.
Recommended tests
Test raw and cbreak, Python 3.11 and 3.12+, Ctrl+C, Ctrl+Z, Escape, arrows, Unicode, redirected stdin, slow SSH, resize, exceptions, subprocesses, and missing TTY.
Common mistakes
Common failures include not restoring state, assuming setraw() returns attributes on old Python, confusing raw and cbreak, reading arrows as one byte, ignoring Unicode, discarding pending input unintentionally, and launching subprocesses in the wrong mode.
Conclusion
tty simplifies raw and cbreak terminal configuration on Unix. Choose cbreak for immediate keys while retaining signals; use raw only when the application must control every byte and transformation.
Restore in finally, handle Python version differences, and test with both real and pseudo-terminals. Consult the official tty documentation and Python termios.







