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.

How to convert a roman numeral to an integer in Python
Published on: 
Wed
Mar 25, 2026
Updated on: 
Thu
Mar 26, 2026
The Replit Team

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 current numeral's value is greater than or equal to the prev_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 re module with the expression r'^[IVXLCDM]+$' to perform this check.
  • If the input s contains any character not in the allowed set, the function raises a ValueError, 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 a ValueError. 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 in self.value.
  • Implementing the __int__ dunder method is the key trick here. It allows you to cast the object directly to an integer using int(), 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 values list and uses the ord() 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 pattern r'\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_roman function 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.

Get started free

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.

Get started for free

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.