Python - chr() and ord() Functions
Every character you see on screen is stored as a number. The letter 'A' is 65. The digit '0' is 48. The emoji '🐍' is 128013. This mapping between characters and integers is called character encoding,...
Key Insights
ord()andchr()are inverse functions that convert between characters and their Unicode code points, forming the foundation for character manipulation, encryption, and text processing in Python.- Understanding these functions unlocks powerful patterns like Caesar ciphers, custom case conversion, and character range generation without relying on string methods.
- While Python’s built-in string methods handle most common cases,
ord()andchr()give you fine-grained control when you need to work at the character level.
Introduction to Character Encoding in Python
Every character you see on screen is stored as a number. The letter ‘A’ is 65. The digit ‘0’ is 48. The emoji ‘🐍’ is 128013. This mapping between characters and integers is called character encoding, and Python uses Unicode—a universal standard that covers virtually every character from every writing system.
Python provides two built-in functions for working with this mapping: ord() converts a character to its integer code point, and chr() converts an integer back to a character. They’re inverse operations—feed the output of one into the other, and you get back what you started with.
>>> ord('A')
65
>>> chr(65)
'A'
>>> chr(ord('A'))
'A'
Why does this matter? Because once you can treat characters as numbers, you can do math on them. You can shift letters for encryption, generate character ranges, validate input, and implement algorithms that would be clumsy or impossible with string methods alone.
The ord() Function: Character to Integer
The ord() function takes a single character and returns its Unicode code point as an integer. The syntax is straightforward:
ord(character) -> int
The input must be a string of length 1. The output is always a non-negative integer.
# Basic letters
print(ord('A')) # 65
print(ord('Z')) # 90
print(ord('a')) # 97
print(ord('z')) # 122
# Digits (note: '0' is not 0)
print(ord('0')) # 48
print(ord('9')) # 57
# Special characters
print(ord(' ')) # 32 (space)
print(ord('\n')) # 10 (newline)
print(ord('@')) # 64
print(ord('€')) # 8364
# Emoji and extended Unicode
print(ord('🐍')) # 128013
print(ord('中')) # 20013
Notice the pattern with letters: uppercase A-Z occupy code points 65-90, and lowercase a-z occupy 97-122. The difference between corresponding cases is always 32. This isn’t coincidence—it’s a deliberate design choice in ASCII that makes case conversion trivial with bit manipulation.
The chr() Function: Integer to Character
The chr() function is the inverse of ord(). It takes an integer and returns the corresponding Unicode character:
chr(integer) -> str
The input must be an integer in the range 0 to 1,114,111 (0x10FFFF in hexadecimal). This upper limit represents the maximum valid Unicode code point.
# Basic conversions
print(chr(65)) # 'A'
print(chr(97)) # 'a'
print(chr(48)) # '0'
# Special characters
print(chr(32)) # ' ' (space)
print(chr(10)) # '\n' (newline)
print(chr(9)) # '\t' (tab)
# Extended Unicode
print(chr(128013)) # '🐍'
print(chr(8364)) # '€'
print(chr(20013)) # '中'
# Boundary values
print(chr(0)) # '\x00' (null character)
print(chr(1114111)) # '\U0010ffff' (maximum valid code point)
You can use chr() to generate characters that are difficult to type directly, including control characters, obscure symbols, and characters from non-Latin scripts.
Practical Applications
Caesar Cipher Implementation
The Caesar cipher shifts each letter by a fixed amount. With ord() and chr(), the implementation is clean and intuitive:
def caesar_encrypt(text: str, shift: int) -> str:
result = []
for char in text:
if char.isalpha():
# Determine the base (A=65 for uppercase, a=97 for lowercase)
base = ord('A') if char.isupper() else ord('a')
# Shift within the 26-letter alphabet
shifted = (ord(char) - base + shift) % 26 + base
result.append(chr(shifted))
else:
# Non-alphabetic characters pass through unchanged
result.append(char)
return ''.join(result)
def caesar_decrypt(text: str, shift: int) -> str:
return caesar_encrypt(text, -shift)
# Usage
plaintext = "Hello, World!"
encrypted = caesar_encrypt(plaintext, 3)
decrypted = caesar_decrypt(encrypted, 3)
print(f"Original: {plaintext}") # Hello, World!
print(f"Encrypted: {encrypted}") # Khoor, Zruog!
print(f"Decrypted: {decrypted}") # Hello, World!
The modulo operation (% 26) handles wraparound—shifting ‘Z’ by 1 gives ‘A’, not ‘[’.
Character Range Generation
Need to iterate over the alphabet? Generate it programmatically:
# Generate uppercase alphabet
uppercase = [chr(i) for i in range(ord('A'), ord('Z') + 1)]
print(uppercase) # ['A', 'B', 'C', ..., 'Z']
# Generate lowercase alphabet
lowercase = [chr(i) for i in range(ord('a'), ord('z') + 1)]
print(lowercase) # ['a', 'b', 'c', ..., 'z']
# Generate digits
digits = [chr(i) for i in range(ord('0'), ord('9') + 1)]
print(digits) # ['0', '1', '2', ..., '9']
# Generate a custom range
def char_range(start: str, end: str) -> list[str]:
"""Generate a list of characters from start to end, inclusive."""
return [chr(i) for i in range(ord(start), ord(end) + 1)]
print(char_range('A', 'F')) # ['A', 'B', 'C', 'D', 'E', 'F']
print(char_range('α', 'ω')) # Greek lowercase letters
Input Validation
Check character properties without relying on string methods:
def is_ascii_letter(char: str) -> bool:
"""Check if character is an ASCII letter (A-Z or a-z)."""
code = ord(char)
return (65 <= code <= 90) or (97 <= code <= 122)
def is_ascii_digit(char: str) -> bool:
"""Check if character is an ASCII digit (0-9)."""
return 48 <= ord(char) <= 57
def is_printable_ascii(char: str) -> bool:
"""Check if character is printable ASCII (space through tilde)."""
return 32 <= ord(char) <= 126
# Validate that a string contains only printable ASCII
def validate_ascii_input(text: str) -> bool:
return all(is_printable_ascii(c) for c in text)
print(validate_ascii_input("Hello123")) # True
print(validate_ascii_input("Hello\x00")) # False (contains null)
print(validate_ascii_input("Héllo")) # False (contains é)
Common Patterns and Tricks
Manual Case Conversion
Since uppercase and lowercase letters differ by exactly 32, you can convert cases with simple arithmetic:
def to_lowercase(char: str) -> str:
"""Convert uppercase letter to lowercase."""
if 'A' <= char <= 'Z':
return chr(ord(char) + 32)
return char
def to_uppercase(char: str) -> str:
"""Convert lowercase letter to uppercase."""
if 'a' <= char <= 'z':
return chr(ord(char) - 32)
return char
def swap_case(char: str) -> str:
"""Swap the case of a letter."""
if 'A' <= char <= 'Z':
return chr(ord(char) + 32)
elif 'a' <= char <= 'z':
return chr(ord(char) - 32)
return char
# Apply to strings
text = "Hello World"
swapped = ''.join(swap_case(c) for c in text)
print(swapped) # "hELLO wORLD"
Character Arithmetic
Treat characters as positions in a sequence:
def letter_position(char: str) -> int:
"""Return 1-based position of letter in alphabet (A=1, B=2, etc.)."""
char = char.upper()
if 'A' <= char <= 'Z':
return ord(char) - ord('A') + 1
raise ValueError(f"'{char}' is not a letter")
def position_to_letter(pos: int, uppercase: bool = True) -> str:
"""Convert 1-based position to letter."""
if not 1 <= pos <= 26:
raise ValueError(f"Position must be 1-26, got {pos}")
base = ord('A') if uppercase else ord('a')
return chr(base + pos - 1)
# Convert digit character to actual integer
def digit_to_int(char: str) -> int:
"""Convert digit character to integer without int()."""
if '0' <= char <= '9':
return ord(char) - ord('0')
raise ValueError(f"'{char}' is not a digit")
print(letter_position('G')) # 7
print(position_to_letter(7)) # 'G'
print(digit_to_int('7')) # 7
Error Handling and Edge Cases
Both functions raise exceptions for invalid input. Handle them appropriately:
def safe_ord(char: str) -> int | None:
"""Return code point or None if input is invalid."""
try:
return ord(char)
except TypeError:
# Input was not a string or was wrong length
return None
def safe_chr(code: int) -> str | None:
"""Return character or None if code point is invalid."""
try:
return chr(code)
except (ValueError, TypeError):
# Code point out of range or not an integer
return None
# Common mistakes
print(safe_ord("AB")) # None (string too long)
print(safe_ord(65)) # None (not a string)
print(safe_ord("")) # None (empty string)
print(safe_chr(-1)) # None (negative)
print(safe_chr(1114112)) # None (too large)
print(safe_chr("65")) # None (string, not int)
For production code, validate input before calling these functions:
def process_character(char: str) -> int:
if not isinstance(char, str):
raise TypeError(f"Expected str, got {type(char).__name__}")
if len(char) != 1:
raise ValueError(f"Expected single character, got string of length {len(char)}")
return ord(char)
Summary and Quick Reference
| Function | Signature | Description |
|---|---|---|
ord(c) |
str -> int |
Returns Unicode code point of character |
chr(i) |
int -> str |
Returns character for Unicode code point |
When to use ord() and chr():
- Implementing encryption or encoding algorithms
- Generating character sequences programmatically
- Performing arithmetic on characters
- Low-level input validation
- When you need explicit control over character codes
When to use built-in string methods instead:
- Simple case conversion (use
.upper(),.lower()) - Character classification (use
.isalpha(),.isdigit()) - Standard string operations (use
.replace(),.strip())
The built-in methods are more readable and handle Unicode edge cases better. But when you need to work at the character-integer boundary—for cryptography, custom encoding, or algorithmic challenges—ord() and chr() are indispensable tools.