Skip to content

How to Convert Roman Numerals to Decimals in Python

Learn how to implement Roman numeral conversion in Python using right-to-left and left-to-right algorithms, add input validation, and choose between manual implementations or the roman library.

Parsing Roman numerals like "MCMXCIV" requires understanding subtractive notation, where a smaller value placed before a larger one is subtracted rather than added. For example, IV equals 4 (5 minus 1), IX equals 9 (10 minus 1), and XC equals 90 (100 minus 10).

In this guide, you will learn how to implement Roman numeral to integer conversion using different algorithms, add input validation, handle the reverse conversion, and choose between manual implementations and library solutions.

Iterating backwards through the string simplifies the subtractive logic: if the current value is smaller than the previous one, subtract it. Otherwise, add it:

def roman_to_int(s):
    """Convert a Roman numeral string to an integer."""
    roman_map = {
        'I': 1, 'V': 5, 'X': 10, 'L': 50,
        'C': 100, 'D': 500, 'M': 1000
    }

    total = 0
    prev_value = 0

    for char in reversed(s.upper()):
        value = roman_map[char]

        if value < prev_value:
            total -= value  # Subtractive: IV -> 5 - 1 = 4
        else:
            total += value  # Additive: VI -> 5 + 1 = 6

        prev_value = value

    return total

print(roman_to_int("III"))      # Output: 3
print(roman_to_int("XIV"))      # Output: 14
print(roman_to_int("MCMXCIV"))  # Output: 1994
print(roman_to_int("MMXXIV"))   # Output: 2024

Output:

3
14
1994
2024

How the Algorithm Works

For "MCMXCIV" (1994), processing from right to left:

Char Value vs Previous Action Running Total
V 5 (start) +5 5
I 1 < 5 -1 4
C 100 > 1 +100 104
X 10 < 100 -10 94
M 1000 > 10 +1000 1094
C 100 < 1000 -100 994
M 1000 > 100 +1000 1994

The key insight is that in valid Roman numerals, a smaller value only appears before a larger value when it should be subtracted. Reading right to left makes this comparison natural because you always compare the current value against the one you just processed.

Left-to-Right with Lookahead

An alternative approach reads left to right, checking whether the next character has a larger value:

def roman_to_int_forward(s):
    """Convert a Roman numeral using forward iteration with lookahead."""
    roman_map = {
        'I': 1, 'V': 5, 'X': 10, 'L': 50,
        'C': 100, 'D': 500, 'M': 1000
    }

    s = s.upper()
    total = 0

    for i, char in enumerate(s):
        value = roman_map[char]

        if i + 1 < len(s) and value < roman_map[s[i + 1]]:
            total -= value
        else:
            total += value

    return total

print(roman_to_int_forward("MCMXCIV"))  # Output: 1994
print(roman_to_int_forward("XLII"))     # Output: 42

Output:

1994
42

Adding Input Validation

When processing user input or data from external sources, validation prevents cryptic errors:

def roman_to_int_safe(s):
    """Convert a Roman numeral with input validation."""
    if not s or not isinstance(s, str):
        raise ValueError("Input must be a non-empty string")

    roman_map = {
        'I': 1, 'V': 5, 'X': 10, 'L': 50,
        'C': 100, 'D': 500, 'M': 1000
    }

    s = s.upper().strip()

    for char in s:
        if char not in roman_map:
            raise ValueError(f"Invalid Roman numeral character: '{char}'")

    total = 0
    prev_value = 0

    for char in reversed(s):
        value = roman_map[char]

        if value < prev_value:
            total -= value
        else:
            total += value

        prev_value = value

    if total <= 0:
        raise ValueError("Invalid Roman numeral sequence")

    return total

print(roman_to_int_safe("  xiv  "))

try:
    roman_to_int_safe("ABC")
except ValueError as e:
    print(f"Error: {e}")

Output:

14
Error: Invalid Roman numeral character: 'A'

Using the roman Library

For production code that requires strict validation of proper Roman numeral formatting, the roman library handles edge cases and formatting rules automatically:

pip install roman
import roman

result = roman.fromRoman("XIV")
print(result)

print(roman.toRoman(1994))

try:
    roman.fromRoman("IIII")  # Invalid: should be IV
except roman.InvalidRomanNumeralError:
    print("Invalid Roman numeral format")

Output:

14
MCMXCIV
Invalid Roman numeral format

Decimal to Roman Conversion

The reverse operation converts an integer to a Roman numeral string by repeatedly subtracting the largest possible value:

def int_to_roman(num):
    """Convert an integer to a Roman numeral string."""
    if not 0 < num < 4000:
        raise ValueError("Number must be between 1 and 3999")

    values = [
        (1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'),
        (100, 'C'),  (90, 'XC'),  (50, 'L'),  (40, 'XL'),
        (10, 'X'),   (9, 'IX'),   (5, 'V'),   (4, 'IV'),
        (1, 'I')
    ]

    result = []

    for value, numeral in values:
        while num >= value:
            result.append(numeral)
            num -= value

    return ''.join(result)

print(int_to_roman(1994))   # Output: MCMXCIV
print(int_to_roman(2024))   # Output: MMXXIV
print(int_to_roman(14))     # Output: XIV

Output:

MCMXCIV
MMXXIV
XIV

Verifying Round-Trip Consistency

A quick way to verify both functions are correct is to confirm that converting in both directions produces the original value:

for n in [1, 4, 9, 49, 99, 499, 999, 1994, 3999]:
    roman = int_to_roman(n)
    back = roman_to_int(roman)
    print(f"{n:>4} -> {roman:<15} -> {back}")

Output:

   1 -> I               -> 1
   4 -> IV              -> 4
   9 -> IX              -> 9
  49 -> XLIX            -> 49
  99 -> XCIX            -> 99
 499 -> CDXCIX          -> 499
 999 -> CMXCIX          -> 999
1994 -> MCMXCIV         -> 1994
3999 -> MMMCMXCIX       -> 3999

Batch Conversion

When processing multiple numerals at once, a batch function with error handling prevents a single invalid entry from stopping the entire conversion:

def batch_roman_to_int(numerals):
    """Convert a list of Roman numerals to integers."""
    results = {}

    for numeral in numerals:
        try:
            results[numeral] = roman_to_int(numeral)
        except (KeyError, ValueError):
            results[numeral] = None

    return results

numerals = ["I", "V", "X", "L", "C", "D", "M", "XIV", "MCMXCIV"]
converted = batch_roman_to_int(numerals)

for numeral, value in converted.items():
    print(f"{numeral:>10} = {value}")

Output:

         I = 1
         V = 5
         X = 10
         L = 50
         C = 100
         D = 500
         M = 1000
       XIV = 14
   MCMXCIV = 1994

Roman Numeral Reference

Symbol Value Subtractive Form
I 1 -
IV 4 I before V
V 5 -
IX 9 I before X
X 10 -
XL 40 X before L
L 50 -
XC 90 X before C
C 100 -
CD 400 C before D
D 500 -
CM 900 C before M
M 1000 -

Standard Roman numerals can represent values from 1 to 3999.

Method Comparison

Method Dependencies Validation Best For
Right-to-left loop None Manual General use, interviews, scripts
Left-to-right lookahead None Manual Alternative approach
roman library pip install roman Built-in, strict Production code with format enforcement

Conclusion

Converting Roman numerals to integers in Python is a classic problem with an elegant solution. The right-to-left algorithm is the recommended approach for most use cases: it runs in O(n) time, requires no dependencies, and handles the subtractive notation naturally by comparing each value against the previous one. For production applications that need strict validation of proper Roman numeral formatting, the roman library provides built-in error checking and supports both conversion directions.

Navigation