How to convert a roman numeral to an integer in Python
Discover multiple ways to convert Roman numerals to integers in Python. Get tips, see real-world uses, and learn to debug common errors.

The conversion of Roman numerals to integers is a classic programming challenge. Python offers elegant solutions for this task, a great exercise for developers to practice logic and string manipulation.
In this guide, you'll explore techniques to build a converter, with practical tips and real-world applications. You'll also get debugging advice to help you write efficient, clean code.
Basic conversion with character mapping
def roman_to_int(s):
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
for i in range(len(s)):
if i > 0 and values[s[i]] > values[s[i-1]]:
total += values[s[i]] - 2 * values[s[i-1]]
else:
total += values[s[i]]
return total
print(roman_to_int("XIV")) # 14--OUTPUT--14
The roman_to_int function uses a dictionary to map each character to its integer value. The function then iterates through the string, handling both additive and subtractive cases. The key is the if condition that checks if the current numeral's value is greater than the previous one.
- If true, it signifies a subtractive pair like "IX". The code adds the larger value (10) and subtracts the smaller value (1) twice. This corrects for the smaller value having already been added in the prior loop.
- Otherwise, it's a standard additive case, and the numeral's value is simply added to the total.
Common implementation approaches
Building on that initial function, you can also implement the conversion by iterating backward, handling subtractive pairs more explicitly, or adding validation to make it more robust.
Using the reversed iteration technique
def roman_to_int(s):
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
prev_value = 0
for char in reversed(s):
current = values[char]
total = total + current if current >= prev_value else total - current
prev_value = current
return total
print(roman_to_int("MCMXCIV")) # 1994--OUTPUT--1994
Iterating backward simplifies the logic. By processing the string from right to left using reversed(s), you can decide whether to add or subtract a numeral's value based on the one that came before it—the prev_value.
- If the
currentnumeral's value is greater than or equal to theprev_value, you add it to the total. - If it's smaller, you subtract it. This elegantly handles cases like "IV" (5 - 1) and "IX" (10 - 1) without complex lookaheads or corrections.
Handling special subtractive pairs
def roman_to_int(s):
pairs = {'IV': 4, 'IX': 9, 'XL': 40, 'XC': 90, 'CD': 400, 'CM': 900}
singles = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
i, total = 0, 0
while i < len(s):
if i+1 < len(s) and s[i:i+2] in pairs:
total += pairs[s[i:i+2]]
i += 2
else:
total += singles[s[i]]
i += 1
return total
print(roman_to_int("CDXLIV")) # 444--OUTPUT--444
This approach explicitly defines all six subtractive pairs—like 'IV' and 'CM'—in their own dictionary. The function then iterates through the input string, checking for these two-character combinations before handling single numerals. This makes the logic very direct and easy to follow.
- If the slice
s[i:i+2]is a known subtractive pair, its value is added to the total, and the index advances by two. - If not, the code processes just the single character
s[i], adds its value, and advances the index by one.
Adding validation for Roman numerals
import re
def roman_to_int(s):
if not re.match(r'^[IVXLCDM]+$', s):
raise ValueError("Invalid Roman numeral")
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
prev = 0
for char in reversed(s):
curr = values[char]
total = total + curr if curr >= prev else total - curr
prev = curr
return total
print(roman_to_int("LVII")) # 57--OUTPUT--57
This version adds a crucial validation step to make the function more robust. Before processing, it checks if the input string contains only valid Roman numeral characters. This prevents errors from unexpected inputs like "XQ" or "123".
- It uses Python's
remodule with the expressionr'^[IVXLCDM]+$'to perform this check. - If the input
scontains any character not in the allowed set, the function raises aValueError, stopping execution and alerting the user to the invalid input.
This simple check makes your converter more reliable before it proceeds with the conversion logic.
Advanced techniques and optimizations
Beyond these foundational approaches, you can enhance your converter with sophisticated pattern validation, an object-oriented structure, and clever performance tweaks for maximum efficiency.
Using regular expressions for pattern validation
import re
def roman_to_int(s):
pattern = r'(M{0,3})(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
if not re.match(pattern, s):
raise ValueError("Not a valid Roman numeral")
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
prev = 0
for char in reversed(s):
curr = values[char]
total = total + curr if curr >= prev else total - curr
prev = curr
return total
print(roman_to_int("MMXXIII")) # 2023--OUTPUT--2023
This approach elevates validation by using a complex regular expression. Instead of just checking for valid characters, the pattern enforces the grammatical rules of Roman numerals, ensuring the input follows the correct structure for thousands, hundreds, tens, and ones.
- The regex prevents invalid sequences like "IIII" or "IC".
- If the input doesn't match this strict format,
re.match()fails and raises aValueError. Once validated, the function proceeds with the same reversed iteration logic to calculate the integer value.
Creating a RomanNumeral class
class RomanNumeral:
def __init__(self, roman_str):
self.roman_str = roman_str
self.values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
self.value = self._convert()
def _convert(self):
total = 0
prev = 0
for char in reversed(self.roman_str):
curr = self.values[char]
total = total + curr if curr >= prev else total - curr
prev = curr
return total
def __int__(self):
return self.value
roman = RomanNumeral("MCMXCIV")
print(int(roman)) # 1994--OUTPUT--1994
Wrapping the logic in a RomanNumeral class offers a clean, object-oriented solution. This approach bundles the Roman numeral string and its integer value together in a single, reusable structure. It makes your code more organized and intuitive to use.
- The
__init__method automatically performs the conversion when you create an instance, storing the result inself.value. - Implementing the
__int__dunder method is the key trick here. It allows you to cast the object directly to an integer usingint(), making your code more Pythonic.
Optimizing with character code lookups
def roman_to_int(s):
# Using a list for O(1) lookups instead of dictionary
values = [0] * 128
values[ord('I')] = 1
values[ord('V')] = 5
values[ord('X')] = 10
values[ord('L')] = 50
values[ord('C')] = 100
values[ord('D')] = 500
values[ord('M')] = 1000
result = 0
prev = 0
for char in reversed(s):
curr = values[ord(char)]
result += curr if curr >= prev else -curr
prev = curr
return result
print(roman_to_int("MCMXCIV")) # 1994--OUTPUT--1994
This technique offers a micro-optimization by replacing the dictionary with a list for faster lookups. While dictionaries are fast, list indexing is even faster because it avoids the overhead of hashing keys. It’s a clever way to squeeze out extra performance in time-sensitive code.
- The function creates a
valueslist and uses theord()function to get the unique ASCII number for each Roman numeral character. - This number serves as a direct index into the list, like in
values[ord('I')] = 1, giving you true constant-time access.
Move faster with Replit
Replit is an AI-powered development platform that transforms natural language into working applications. Describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.
For the Roman numeral conversion logic we've explored, Replit Agent can turn functions like roman_to_int into production-ready tools.
- Build a historical document analyzer that automatically converts Roman numerals found in texts.
- Create a simple API that validates and converts Roman numerals for other applications to use.
- Deploy an educational game that quizzes users on numeral conversions and provides instant feedback.
Describe your app idea, and Replit Agent writes the code, tests it, and fixes issues automatically, all from your browser.
Common errors and challenges
Even with a solid approach, you might run into common pitfalls like invalid characters, case sensitivity, and tricky subtractive pair logic.
Handling invalid characters in roman_to_int()
Your roman_to_int() function can easily break if it receives a string with non-Roman characters like "XQ" or numbers. Without validation, this can lead to a KeyError or incorrect outputs. The best defense is to check the input first.
By adding a validation step at the beginning of your function—for example, using a regular expression to ensure the string only contains valid numerals—you can reject bad data immediately. This makes your function far more robust and prevents unexpected crashes.
Dealing with case sensitivity in Roman numerals
Dictionary lookups are case-sensitive, so if your value map is {'V': 5}, an input like 'v' will fail. Since users might input lowercase numerals, your function should be prepared to handle them gracefully.
The simplest solution is to standardize the input. By calling the .upper() method on the input string at the start of your function, you ensure that an input like 'ix' is processed as 'IX'. This makes your converter more flexible without complicating the core conversion logic.
Fixing subtractive pairs in Roman numeral conversion
One of the most frequent bugs is implementing a simple left-to-right summation that ignores subtractive pairs. This logic incorrectly calculates "IV" as 6 (1 + 5) instead of 4, making the converter unreliable. Getting this rule right is crucial for accuracy.
To fix this, your logic must account for the order of the numerals.
- When iterating forward, you need to look ahead. If a smaller value appears before a larger one, you must subtract it.
- Processing the string backward often simplifies this. If the current numeral's value is less than the previous one's (the one to its right), you subtract it; otherwise, you add it. This elegantly handles pairs like "IX" and "CM".
Handling invalid characters in roman_to_int()
A basic roman_to_int() function often trusts its input completely. If an unexpected character like 'Z' or '3' appears in the string, the function will fail. This causes a KeyError because the character doesn't exist as a key in the values dictionary.
The code below shows exactly what happens when the function receives an invalid numeral, triggering this error.
def roman_to_int(s):
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
for char in s:
total += values[char] # Will raise KeyError if char is not a valid Roman numeral
return total
print(roman_to_int("XIV2")) # Contains invalid '2' character
The function directly tries to look up each character in the values dictionary. When it encounters the '2', the lookup fails and triggers a KeyError. See how to prevent this with a simple check.
def roman_to_int(s):
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
for char in s:
if char not in values:
raise ValueError(f"Invalid Roman numeral character: {char}")
total += values[char]
return total
try:
print(roman_to_int("XIV2"))
except ValueError as e:
print(f"Error: {e}")
The fix is simple yet effective. Before attempting the dictionary lookup, the code first checks if the char is a valid key in the values dictionary. If it isn't, the function immediately stops and raises a ValueError with a helpful message. This proactive check prevents the original KeyError and makes your function more robust. It’s a crucial safeguard whenever your function expects inputs from a limited set of characters or values.
Dealing with case sensitivity in Roman numerals
Python dictionaries are case-sensitive, so a function expecting 'V' will fail with 'v'. This common oversight can cause a KeyError if a user inputs lowercase numerals, making your converter seem broken. The following code demonstrates exactly how this happens.
def roman_to_int(s):
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
prev = 0
for char in reversed(s):
curr = values[char] # Will fail with lowercase Roman numerals
total = total + curr if curr >= prev else total - curr
prev = curr
return total
print(roman_to_int("xiv")) # Lowercase "xiv" instead of "XIV"
The function attempts to look up lowercase characters like 'v' in a dictionary that only contains uppercase keys. This mismatch triggers a KeyError, stopping the conversion. See how a simple adjustment can fix this.
def roman_to_int(s):
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
prev = 0
s = s.upper() # Convert to uppercase
for char in reversed(s):
curr = values[char]
total = total + curr if curr >= prev else total - curr
prev = curr
return total
print(roman_to_int("xiv")) # Now works with lowercase
The fix is surprisingly simple. By calling s.upper() on the input string before the loop starts, you standardize the data. This converts any lowercase input like 'xiv' into 'XIV', ensuring the characters will match the keys in your values dictionary. This small change prevents frustrating KeyError exceptions and makes your function more forgiving. It's a great habit to build when dealing with case-sensitive lookups.
Fixing subtractive pairs in Roman numeral conversion
A common bug in Roman numeral converters is failing to handle subtractive pairs like IV or IX. A naive approach that simply adds up each numeral's value from left to right will produce incorrect results, calculating IV as 6 instead of 4.
The code below demonstrates this flawed logic, where the function incorrectly processes the input by ignoring the order of the numerals.
def roman_to_int(s):
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
for char in s:
total += values[char] # Doesn't account for subtractive pairs like IV
return total
print(roman_to_int("IV")) # Should be 4, but will return 6
The line total += values[char] adds each numeral's value in isolation. This logic fails because it has no awareness of a numeral's position, which is crucial for handling subtractive pairs correctly. See how the corrected function addresses this.
def roman_to_int(s):
values = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
prev = 0
for char in reversed(s):
curr = values[char]
if curr >= prev:
total += curr
else:
total -= curr
prev = curr
return total
print(roman_to_int("IV")) # Correctly returns 4
The solution processes the string backward using reversed(s), which simplifies handling subtractive pairs. It compares each numeral's value to the one that came before it—the one to its right.
- If the current value,
curr, is greater than or equal to the previous value,prev, it’s added to the total. - If it’s smaller, it’s subtracted.
This logic correctly calculates "IV" as 5 minus 1, not 1 plus 5, fixing the error.
Real-world applications
With the conversion logic down, your roman_to_int() function becomes a practical tool for historical analysis and building custom applications.
Processing historical texts with roman_to_int()
Your roman_to_int() function is especially useful for parsing historical documents, where it can automatically find and convert Roman numerals into modern integers.
def process_historical_dates(text):
import re
roman_pattern = r'\b([IVXLCDM]+)\b'
def replace_roman(match):
roman = match.group(0)
arabic = roman_to_int(roman)
return f"{roman} ({arabic})"
return re.sub(roman_pattern, replace_roman, text)
historical_text = "The Great War ended in MCMXVIII and the treaty was signed."
print(process_historical_dates(historical_text))
The process_historical_dates function uses regular expressions to find and annotate Roman numerals within a string of text. It’s a practical way to make historical documents more readable by providing modern integer equivalents on the fly.
- It uses
re.sub()to search for whole words made of Roman numeral characters, thanks to the patternr'\b([IVXLCDM]+)\b'. - For each match, a helper function calls your
roman_to_int()converter to get the integer. - The original numeral is then replaced with a new string that includes both the numeral and its calculated integer value.
Building a Roman numeral calculator with int_to_roman()
You can also build a simple calculator to perform arithmetic with Roman numerals by pairing your existing converter with a new int_to_roman() function that handles the reverse operation.
def int_to_roman(num):
val = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
result = ''
i = 0
while num > 0:
for _ in range(num // val[i]):
result += syms[i]
num -= val[i]
i += 1
return result
def add_roman_numerals(a, b):
return int_to_roman(roman_to_int(a) + roman_to_int(b))
print(add_roman_numerals("XIV", "XXVIII")) # 14 + 28
The add_roman_numerals function cleverly performs arithmetic by first converting two Roman numerals into integers. After adding them, it relies on the int_to_roman helper function to translate the final sum back into a Roman numeral string.
- The
int_to_romanfunction works by iterating through a predefined list of values and their corresponding symbols, from largest to smallest. - It greedily subtracts the largest possible value from your number, appends the matching symbol to the result, and repeats this process until the number reaches zero.
Get started with Replit
Now, turn your roman_to_int function into a real tool. Describe your idea to Replit Agent, like "build a Roman numeral calculator API" or "create a web page that converts dates from historical texts."
The agent writes the code, tests for errors, and deploys your app directly from your instructions. Start building with Replit.
Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.
Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

.png)
.png)
.png)