ASCII in Python
ord(), chr(), Bytes, and Character Encoding
Python 3 draws a hard line between str (a sequence of Unicode code points) and bytes (a sequence of raw octets). Every ASCII operation sits on that boundary. The built-in ord() returns the integer code point for a single character, chr() does the reverse, and .encode('ascii') bridges str to bytes with strict, ignore, or replace error handling. Knowing which side of that boundary you are on prevents the UnicodeDecodeError that kills more data pipelines than any logic bug.
1. ord() and chr(): The Core Pair
ord() takes a single character and returns its integer code point. chr() takes an integer and returns the corresponding character. For ASCII values 0 through 127, these map directly to the standard table.
>>> ord('A')
65
>>> ord('a')
97
>>> ord('0')
48
>>> chr(65)
'A'
>>> chr(10)
'\n'
The value 65 for uppercase A and 97 for lowercase a differ by exactly 32. Toggle case for any letter by XORing with 32: chr(ord('A') ^ 32) yields 'a'. This works because the ASCII committee designed the bit layout so that bit 5 controls case. See the full ASCII table to verify the pattern across all 26 letter pairs.
ord() accepts exactly one character. Pass it an empty string or a multi-character string and it raises TypeError. For bulk conversion, use a list comprehension: [ord(c) for c in 'Hello'] returns [72, 101, 108, 108, 111]. Try the text to ASCII converter to verify results interactively.
2. Encoding and Decoding: str vs bytes
Python 3 strings are Unicode by default. To get raw ASCII bytes, call .encode('ascii'). To go back, call .decode('ascii') on a bytes object.
>>> 'Hello'.encode('ascii')
b'Hello'
>>> b'Hello'.decode('ascii')
'Hello'
>>> type('Hello'.encode('ascii'))
<class 'bytes'>
The 'ascii' codec only handles code points 0 through 127. Anything outside that range triggers a UnicodeEncodeError on encode or a UnicodeDecodeError on decode. Three error-handling modes control the behavior:
'strict'(default) - raises an exception'ignore'- silently drops the offending character'replace'- substitutes?on encode,\ufffdon decode
>>> 'cafe\u0301'.encode('ascii', errors='ignore')
b'cafe'
>>> 'cafe\u0301'.encode('ascii', errors='replace')
b'cafe?'
>>> 'cafe\u0301'.encode('ascii', errors='strict')
UnicodeEncodeError: 'ascii' codec can't encode character '\u0301'
Use 'ignore' with caution in data pipelines. Silently dropping characters corrupts data without any signal that something went wrong. Prefer 'strict' and catch the exception, or use UTF-8 encoding instead.
3. str.isascii() - Python 3.7+
Added in Python 3.7 (PEP 538), str.isascii() returns True if every character in the string has a code point below 128, or if the string is empty.
>>> 'Hello World'.isascii()
True
>>> 'cafe\u0301'.isascii()
False
>>> ''.isascii()
True
>>> '\x00\x1f\x7f'.isascii()
True
Note that control characters (0-31 and 127) pass the check. This method tests the numeric range, not printability. To check for printable ASCII only, combine with str.isprintable() or test the range explicitly: all(32 <= ord(c) <= 126 for c in s). The 95 printable ASCII characters occupy positions 32 through 126.
4. Reading Files with Encoding Control
The open() function accepts an encoding parameter. Omitting it defaults to locale.getpreferredencoding(), which varies by platform (UTF-8 on macOS/Linux, cp1252 on many Windows installs). Always specify encoding explicitly.
# Read a strict ASCII file
with open('data.txt', 'r', encoding='ascii', errors='strict') as f:
content = f.read()
# Read binary data directly
with open('data.bin', 'rb') as f:
raw = f.read() # returns bytes, no decoding
Python 3.15 (scheduled for late 2026) changes the default encoding to UTF-8 on all platforms. Until then, unspecified encoding on Windows still defaults to the system code page. If your code reads ASCII files and skips the encoding parameter, it works fine until someone runs it on a Windows machine with a different locale. Specify encoding='ascii' or encoding='utf-8' to avoid this trap.
5. bytes and bytearray for Binary Protocols
When working with network protocols, serial devices, or binary file formats, you operate on bytes objects directly. Each element is an integer 0 through 255.
>>> data = b'HTTP/1.1 200 OK\r\n'
>>> data[0] # H = 72
72
>>> data[0:4] # b'HTTP'
b'HTTP'
>>> list(data[0:4])
[72, 84, 84, 80]
The struct module packs and unpacks binary data with format strings. For ASCII fields in binary protocols, use the s format (bytes string):
import struct
# Pack a 4-byte ASCII tag + 32-bit length
header = struct.pack('4sI', b'DATA', 1024)
# Unpack it back
tag, length = struct.unpack('4sI', header)
print(tag) # b'DATA'
Watch out for null-padding. struct.pack('10s', b'Hi') produces b'Hi\x00\x00\x00\x00\x00\x00\x00\x00'. The NUL character (code 0) fills the remaining space. Strip it with .rstrip(b'\x00').
6. Building an ASCII Lookup Table
Generate a complete ASCII table in Python with four lines:
for i in range(128):
char = chr(i) if 32 <= i <= 126 else f'(ctrl-{i})'
print(f'{i:3d} 0x{i:02X} 0b{i:07b} {char}')
This prints all 128 code points with decimal, hexadecimal, and binary representations. For interactive exploration, the base converter handles the same decimal-hex-binary-octal conversions. The ASCII chart view shows the traditional 16x8 grid layout.
When you need repeated lookups, build a dictionary once:
ASCII_TABLE = {i: chr(i) for i in range(32, 127)}
ASCII_TABLE[32] = 'SP' # label space explicitly
ASCII_TABLE[127] = 'DEL' # include the delete character
7. Common Pitfalls
UnicodeDecodeError from mixed-encoding files
Data from legacy systems often mixes ASCII with Windows-1252 or ISO 8859-1 characters. Opening with encoding='ascii' crashes on the first non-ASCII byte. Use chardet or charset-normalizer to detect the encoding first, or open with encoding='utf-8', errors='replace' to surface problems without crashing. The encoding comparison tool shows exactly where these encodings diverge.
Confusing code points with byte values
ord('A') returns 65 in both ASCII and Unicode because the first 128 Unicode code points match ASCII exactly. But ord('\u00e9') returns 233, which is a UTF-8 two-byte sequence (0xC3 0xA9), not a single byte. Do not assume ord() output equals a byte value for characters above 127.
String indexing vs byte indexing
'hello'[0] returns 'h', but 'hello'.encode('utf-8')[0] returns 104 (the integer). These are different types. Mixing str indexing with bytes operations causes subtle bugs in parsers and protocol handlers.
8. Practical Recipes
Strip non-ASCII characters
def strip_non_ascii(text: str) -> str:
return text.encode('ascii', errors='ignore').decode('ascii')
Check if a file is pure ASCII
def is_ascii_file(path: str) -> bool:
with open(path, 'rb') as f:
return all(byte < 128 for byte in f.read())
Convert between bases
char = 'Z'
code = ord(char)
print(f'Dec: {code}') # 90
print(f'Hex: 0x{code:02X}') # 0x5A
print(f'Oct: 0o{code:03o}') # 0o132
print(f'Bin: 0b{code:07b}') # 0b1011010
These conversions match the values shown on the character detail page for Z. For bulk conversions, the text to ASCII tool handles entire strings at once.
Case conversion via bit manipulation
def to_lower(c: str) -> str:
return chr(ord(c) | 32) if 'A' <= c <= 'Z' else c
def to_upper(c: str) -> str:
return chr(ord(c) & ~32) if 'a' <= c <= 'z' else c
This works because ASCII letters are arranged so that uppercase and lowercase versions differ only in bit 5 (value 32). The technique is faster than .lower() for single-character operations in tight loops, but .lower() handles Unicode correctly, so use it for general text processing.