Skip to main content

How to Convert Roman Numerals to Decimals in Python

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:

CharValuevs PreviousActionRunning Total
V5(start)+55
I1< 5-14
C100> 1+100104
X10< 100-1094
M1000> 10+10001094
C100< 1000-100994
M1000> 100+10001994

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 the next character has a larger value, subtract current
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
note

Both algorithms produce identical results. The right-to-left version is slightly simpler because it avoids the bounds-checking needed for the lookahead.

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()

# Validate that all characters are valid Roman numeral symbols
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

# Valid inputs
print(roman_to_int_safe(" xiv "))

# Invalid inputs
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

# Convert Roman to integer
result = roman.fromRoman("XIV")
print(result)

# Convert integer to Roman
print(roman.toRoman(1994))

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

Output:

14
MCMXCIV
Invalid Roman numeral format
note

The roman library enforces strict formatting rules. For example, it rejects "IIII" (which should be "IV") and "VV" (which should be "X"). The manual implementations above are more lenient and will produce a numeric result for any sequence of valid Roman characters.

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")

# Ordered from largest to smallest, including subtractive pairs
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

SymbolValueSubtractive Form
I1-
IV4I before V
V5-
IX9I before X
X10-
XL40X before L
L50-
XC90X before C
C100-
CD400C before D
D500-
CM900C before M
M1000-

Standard Roman numerals can represent values from 1 to 3999.

Method Comparison

MethodDependenciesValidationBest For
Right-to-left loopNoneManualGeneral use, interviews, scripts
Left-to-right lookaheadNoneManualAlternative approach
roman librarypip install romanBuilt-in, strictProduction 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.

Best Practice

The right-to-left algorithm is O(n), dependency-free, and handles all valid Roman numerals correctly. Use the roman library when you need strict validation that rejects improperly formatted numerals like "IIII" or "VV", or when you need both conversion directions with guaranteed consistency.