Python’s curses.ascii module provides predicates and utilities for testing and converting ASCII characters — a pure-Python complement to the C <ctype.h> functions. from curses import ascii. Predicates: ascii.isalpha(c), ascii.isdigit(c), ascii.isalnum(c), ascii.isspace(c), ascii.isupper(c), ascii.islower(c), ascii.ispunct(c), ascii.isprint(c), ascii.isgraph(c), ascii.iscntrl(c), ascii.isblank(c), ascii.isxdigit(c), ascii.isascii(c), ascii.ismeta(c). All accept either a single-character string or an integer code point. Conversion: ascii.ascii(c) — strip the high bit (mask to 7-bit); ascii.ctrl(c) — return the control character for a letter (ctrl('A') → \x01); ascii.alt(c) — set the high bit (alt('a') → '\xe1'); ascii.unctrl(c) — printable representation (unctrl('\x01') → '^A', unctrl('\n') → '^J'). Unlike str.isalpha() and friends, curses.ascii predicates strictly test 7-bit ASCII — they return False for non-ASCII Unicode characters, making them useful for protocol parsing and terminal I/O where strict ASCII semantics are required. Claude Code generates terminal input classifiers, keystroke filters, cursor navigators, byte stream analyzers, and ANSI control sequence parsers.
CLAUDE.md for curses.ascii
## curses.ascii Stack
- Stdlib: from curses import ascii as _ascii
- Predicates (char or int):
- _ascii.isalpha(c) _ascii.isdigit(c) _ascii.isalnum(c)
- _ascii.isspace(c) _ascii.isupper(c) _ascii.islower(c)
- _ascii.ispunct(c) _ascii.isprint(c) _ascii.isgraph(c)
- _ascii.iscntrl(c) _ascii.isblank(c) _ascii.isxdigit(c)
- _ascii.isascii(c) _ascii.ismeta(c)
- Conversion:
- _ascii.ascii(c) → int (7-bit)
- _ascii.ctrl(c) → str control char ctrl('A') → '\x01'
- _ascii.alt(c) → str high-bit char
- _ascii.unctrl(c) → str printable repr unctrl('\x01') → '^A'
- Note: strict 7-bit ASCII — returns False for Unicode > 127
curses.ascii Character Classification Pipeline
# app/cursesasciiutil.py — classify, filter, scan, escape, keystroke, decode
from __future__ import annotations
from curses import ascii as _ascii
from dataclasses import dataclass, field
from typing import Iterator
# ─────────────────────────────────────────────────────────────────────────────
# 1. Character classification
# ─────────────────────────────────────────────────────────────────────────────
_CATEGORIES = [
("alpha", _ascii.isalpha),
("digit", _ascii.isdigit),
("alnum", _ascii.isalnum),
("space", _ascii.isspace),
("upper", _ascii.isupper),
("lower", _ascii.islower),
("punct", _ascii.ispunct),
("print", _ascii.isprint),
("graph", _ascii.isgraph),
("cntrl", _ascii.iscntrl),
("blank", _ascii.isblank),
("xdigit", _ascii.isxdigit),
("ascii", _ascii.isascii),
("meta", _ascii.ismeta),
]
def classify_char(c: "str | int") -> list[str]:
"""
Return all category names that apply to character c.
Example:
classify_char('A') # ["alpha", "alnum", "upper", "print", "graph", "ascii"]
classify_char('\x01') # ["cntrl", "ascii"]
classify_char(0x80) # ["cntrl", "meta"]
"""
return [name for name, fn in _CATEGORIES if fn(c)]
def char_info(c: "str | int") -> dict[str, object]:
"""
Return a full description dict for a character.
Example:
print(char_info('A'))
print(char_info('\x07'))
"""
ch = chr(c) if isinstance(c, int) else c
code = ord(ch)
return {
"char": repr(ch),
"code": code,
"hex": hex(code),
"unctrl": _ascii.unctrl(ch),
"categories": classify_char(ch),
}
def ascii_table() -> list[dict[str, object]]:
"""
Return a list of char_info dicts for all 128 ASCII code points.
Example:
for row in ascii_table():
if "cntrl" in row["categories"]:
print(row["code"], row["unctrl"])
"""
return [char_info(i) for i in range(128)]
# ─────────────────────────────────────────────────────────────────────────────
# 2. String filters and scanners
# ─────────────────────────────────────────────────────────────────────────────
def filter_printable(text: str) -> str:
"""
Remove all non-printable ASCII characters from a string.
Non-ASCII characters are preserved.
Example:
filter_printable("Hello\x07\x00World") # "HelloWorld"
"""
return "".join(c for c in text if _ascii.isprint(c) or ord(c) > 127)
def filter_ascii_only(text: str) -> str:
"""
Remove all characters outside the 7-bit ASCII range.
Example:
filter_ascii_only("café résumé") # "caf rsm"
"""
return "".join(c for c in text if _ascii.isascii(c))
def find_control_chars(data: "str | bytes") -> list[tuple[int, str]]:
"""
Find all control characters in a string or bytes, returning (offset, unctrl_repr) pairs.
Example:
find_control_chars("line1\r\nline2\x07")
# [(5, '^M'), (6, '^J'), (11, '^G')]
"""
results: list[tuple[int, str]] = []
seq = data if isinstance(data, str) else data.decode("latin-1")
for i, c in enumerate(seq):
if _ascii.iscntrl(c):
results.append((i, _ascii.unctrl(c)))
return results
def count_categories(text: str) -> dict[str, int]:
"""
Count characters per ASCII category in a string.
Example:
count_categories("Hello, World! 42")
# {"alpha": 10, "digit": 2, "punct": 2, "space": 2, ...}
"""
counts: dict[str, int] = {name: 0 for name, _ in _CATEGORIES}
for c in text:
for name, fn in _CATEGORIES:
if fn(c):
counts[name] += 1
return {k: v for k, v in counts.items() if v > 0}
# ─────────────────────────────────────────────────────────────────────────────
# 3. Control-character conversions
# ─────────────────────────────────────────────────────────────────────────────
def ctrl_key(letter: str) -> str:
"""
Return the control character for a letter (Ctrl+A → \\x01, etc.).
Example:
ctrl_key('C') # '\\x03' (Ctrl+C = SIGINT in many terminals)
ctrl_key('D') # '\\x04' (Ctrl+D = EOF)
ctrl_key('[') # '\\x1b' (Escape)
"""
return _ascii.ctrl(letter)
def describe_keypress(code: "str | int") -> str:
"""
Return a human-readable description of a keypress code.
Example:
describe_keypress(13) # "Ctrl+M (CR)"
describe_keypress(27) # "Ctrl+[ (ESC)"
describe_keypress(65) # "A"
"""
_NAMES = {
0: "NUL", 1: "SOH", 2: "STX", 3: "ETX (Ctrl+C)",
4: "EOT (Ctrl+D)", 7: "BEL", 8: "BS", 9: "HT (Tab)",
10: "LF (\\n)", 13: "CR (\\r)", 27: "ESC", 32: "Space",
127: "DEL",
}
c = chr(code) if isinstance(code, int) else code
n = ord(c)
if n in _NAMES:
return _NAMES[n]
if _ascii.iscntrl(c):
return f"Ctrl+{chr(n + 64)} ({_ascii.unctrl(c)})"
if _ascii.isprint(c):
return repr(c)
return f"0x{n:02x}"
# ─────────────────────────────────────────────────────────────────────────────
# 4. Byte stream scanner
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class StreamStats:
total_bytes: int
ascii_bytes: int
printable_bytes: int
control_bytes: int
meta_bytes: int
newlines: int
nul_bytes: int
@property
def ascii_ratio(self) -> float:
return self.ascii_bytes / self.total_bytes if self.total_bytes else 0.0
@property
def is_text(self) -> bool:
return self.ascii_ratio >= 0.95 and self.nul_bytes == 0
def analyze_bytes(data: bytes) -> StreamStats:
"""
Analyse a byte sequence for ASCII character distribution.
Example:
stats = analyze_bytes(open("file.txt", "rb").read())
print(stats.is_text, stats.ascii_ratio)
"""
total = len(data)
ascii_b = printable_b = control_b = meta_b = newlines = nul_b = 0
for b in data:
c = chr(b)
if _ascii.isascii(c):
ascii_b += 1
else:
meta_b += 1
if _ascii.isprint(c):
printable_b += 1
elif _ascii.iscntrl(c):
control_b += 1
if b == 10:
newlines += 1
if b == 0:
nul_b += 1
return StreamStats(
total_bytes=total,
ascii_bytes=ascii_b,
printable_bytes=printable_b,
control_bytes=control_b,
meta_bytes=meta_b,
newlines=newlines,
nul_bytes=nul_b,
)
# ─────────────────────────────────────────────────────────────────────────────
# 5. unctrl escape formatter
# ─────────────────────────────────────────────────────────────────────────────
def escape_control(text: str, *, verbose: bool = False) -> str:
"""
Return a printable representation of text showing control chars as ^X.
verbose=True uses full names like <NUL>, <CR>, <LF>.
Example:
escape_control("line1\r\nline2\x07!")
# "line1^M^Jline2^G!"
escape_control("line1\r\nline2\x07!", verbose=True)
# "line1<CR><LF>line2<BEL>!"
"""
_VERBOSE = {0: "NUL", 7: "BEL", 8: "BS", 9: "HT",
10: "LF", 13: "CR", 27: "ESC", 127: "DEL"}
parts: list[str] = []
for c in text:
n = ord(c)
if _ascii.iscntrl(c):
if verbose and n in _VERBOSE:
parts.append(f"<{_VERBOSE[n]}>")
else:
parts.append(_ascii.unctrl(c))
else:
parts.append(c)
return "".join(parts)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== curses.ascii demo ===")
# ── classify_char ──────────────────────────────────────────────────────
print("\n--- classify_char ---")
for sample in ['A', 'z', '5', ' ', '\t', '\n', '\x01', '\x7f', chr(0x80)]:
cats = classify_char(sample)
print(f" {_ascii.unctrl(sample):6s} {cats}")
# ── count_categories ───────────────────────────────────────────────────
print("\n--- count_categories ---")
msg = "Hello, World! 42\n"
for cat, n in sorted(count_categories(msg).items()):
print(f" {cat:8s}: {n}")
# ── find_control_chars ─────────────────────────────────────────────────
print("\n--- find_control_chars ---")
data = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\nBody"
for offset, rep in find_control_chars(data):
print(f" offset={offset:3d} {rep!r}")
# ── describe_keypress ──────────────────────────────────────────────────
print("\n--- describe_keypress ---")
for code in [0, 3, 4, 8, 9, 10, 13, 27, 32, 65, 97, 127, 0x80]:
print(f" 0x{code:02x} ({code:3d}) → {describe_keypress(code)}")
# ── ctrl_key ───────────────────────────────────────────────────────────
print("\n--- ctrl_key ---")
for letter in "ABCDZ[":
ck = ctrl_key(letter)
print(f" Ctrl+{letter} = {_ascii.unctrl(ck):4s} (0x{ord(ck):02x})")
# ── escape_control ─────────────────────────────────────────────────────
print("\n--- escape_control ---")
raw = "line1\r\nline2\x07end"
print(f" compact : {escape_control(raw)!r}")
print(f" verbose : {escape_control(raw, verbose=True)!r}")
# ── analyze_bytes ──────────────────────────────────────────────────────
print("\n--- analyze_bytes ---")
stats = analyze_bytes(b"Hello, World!\r\nLine two.\n\x00\x80\xff")
print(f" total={stats.total_bytes} ascii={stats.ascii_bytes}"
f" printable={stats.printable_bytes} control={stats.control_bytes}")
print(f" meta={stats.meta_bytes} newlines={stats.newlines}"
f" nul={stats.nul_bytes} is_text={stats.is_text}")
print("\n=== done ===")
For the string stdlib companion — string.printable, string.ascii_letters, string.digits, string.punctuation provide pre-built character set strings for membership tests (c in string.ascii_letters) that are slightly more Pythonic for simple checks — use string.* constants and str.isalpha() / str.isdigit() for Unicode-aware classification; use curses.ascii predicates when you need strict 7-bit ASCII semantics (e.g., protocol parsing, terminal I/O where non-ASCII is an error, not a valid character). For the charset-normalizer (PyPI) alternative — charset_normalizer.from_bytes(raw).best().encoding provides full encoding detection including multi-byte charsets and confidence scores for non-ASCII bytestreams — use charset-normalizer (or the chardet alternative) when dealing with text of unknown encoding; use curses.ascii.analyze_bytes() patterns for quick text-vs-binary detection on streams you expect to be ASCII. The Claude Skills 360 bundle includes curses.ascii skill sets covering classify_char()/char_info()/ascii_table() classifiers, filter_printable()/filter_ascii_only()/find_control_chars()/count_categories() string filters, ctrl_key()/describe_keypress() keystroke helpers, StreamStats/analyze_bytes() byte stream analyzer, and escape_control() control character formatter. Start with the free tier to try ASCII classification patterns and curses.ascii pipeline code generation.