Skip to main content

How to Convert Integers to Roman Numerals and Viceversa in Python

Roman numerals are a numeric system originating in ancient Rome, using combinations of letters from the Latin alphabet to signify values. While modern computing uses the decimal system, converting integers to Roman numerals is a common task in software development for copyright dates, stylized lists, or generation of outlines.

This guide explains the logic behind Roman numeral composition and provides efficient Python solutions for converting integers to Roman numerals and vice versa.

Understanding Roman Numeral Logic

Standard Roman numerals use seven basic symbols. However, the system relies on a combination of addition and subtraction rules.

SymbolValue
I1
V5
X10
L50
C100
D500
M1000

The Subtraction Rule

To avoid four consecutive characters (like "IIII"), smaller values are placed before larger values to indicate subtraction. To write an efficient algorithm, we treat these special pairs as distinct "values."

  • IV (4) and IX (9)
  • XL (40) and XC (90)
  • CD (400) and CM (900)
note

By including these subtraction pairs in our lookup table, we simplify the logic significantly. Instead of writing complex "if previous char" logic, we just treat "CM" as a single symbol worth 900.

Method 1: Integer to Roman (Greedy Algorithm)

The most efficient way to convert an integer to a Roman numeral is the Greedy Algorithm. We iterate through the values from largest to smallest, subtracting the value from the input number and appending the corresponding symbol until the number reaches zero.

def int_to_roman(num):
# Map values to symbols in descending order
# Includes standard subtraction pairs (e.g., 900, 400, 90, 40)
val_map = [
(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')
]

roman_numeral = ''

for value, symbol in val_map:
# While the number is large enough to contain this value
while num >= value:
roman_numeral += symbol
num -= value

return roman_numeral

# Testing the conversion
print(f"1994 -> {int_to_roman(1994)}")
print(f"58 -> {int_to_roman(58)}")
print(f"4 -> {int_to_roman(4)}")

Output:

1994 -> MCMXCIV
58 -> LVIII
4 -> IV

Method 2: Roman to Integer (Reverse Parsing)

To convert back from Roman numerals to an integer, we scan the string. A clever trick is to iterate in reverse. This allows us to maintain a running total where we add the value if it is greater than or equal to the previous value seen, or subtract it if it is smaller.

def roman_to_int(s):
roman_values = {
'I': 1, 'V': 5, 'X': 10,
'L': 50, 'C': 100, 'D': 500, 'M': 1000
}

total = 0
prev_value = 0

# Process from right to left (reversed)
for char in reversed(s.upper()):
current_value = roman_values.get(char, 0)

if current_value >= prev_value:
total += current_value
else:
total -= current_value

prev_value = current_value

return total

print(f"MCMXCIV -> {roman_to_int('MCMXCIV')}")
print(f"IV -> {roman_to_int('IV')}")

Output:

MCMXCIV -> 1994
IV -> 4

Handling Errors and Edge Cases

Standard Roman numerals are typically defined for the range 1 to 3999. Zero and negative numbers were not part of the original system, and standard notation breaks down above 3999 (requiring lines over letters to indicate multiplication by 1000).

You should implement validation to ensure inputs adhere to these constraints.

def int_to_roman(num):
# Map values to symbols in descending order
# Includes standard subtraction pairs (e.g., 900, 400, 90, 40)
val_map = [
(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')
]

roman_numeral = ''

for value, symbol in val_map:
# While the number is large enough to contain this value
while num >= value:
roman_numeral += symbol
num -= value

return roman_numeral

def safe_int_to_roman(num):
# ⛔️ Check for invalid ranges
if not isinstance(num, int):
raise TypeError("Input must be an integer.")
if not (0 < num < 4000):
raise ValueError("Number must be between 1 and 3999.")

# ... perform conversion logic here ...
return int_to_roman(num)

# ✅ Correct Usage
try:
print(safe_int_to_roman(2023))
except ValueError as e:
print(e)

# ⛔️ Error Usage
try:
print(safe_int_to_roman(0))
except ValueError as e:
print(f"Error: {e}")

Output:

MMXXIII
Error: Number must be between 1 and 3999.
warning

Python dictionaries are fast, but roman_to_int will fail if the input string contains invalid characters (like 'A' or 'Z'). Always ensure roman_values.get(char) handles missing keys or validate the string beforehand.

Conclusion

Converting integers to Roman numerals is a classic algorithmic problem solved efficiently using a greedy approach with a lookup list containing subtraction pairs.

  1. Define Mappings: Create a list of tuples for values and symbols, including special cases like 900 (CM) and 4 (IV).
  2. Iterate Descending: Process the largest values first.
  3. Validate Inputs: Ensure the integer is within the standard range (1-3999).
  4. Reverse for Decoding: When converting back to integers, processing the string from right to left simplifies the logic for subtraction rules.