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() and chr() 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() and chr() 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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.