Conversion Algorithms in TheAlgorithms/Python
Seven base, numeral, and unit conversions showing how repeated division, recursion, lookup tables, and pivot factors each solve the same family of problems.
What you will learn
- How iterative decimal-to-binary conversion extracts each bit by taking the remainder of division by 2 and right-shifting
- How the recursive helper for binary conversion reduces the problem by one bit at each call using divmod
- Why a module-level lookup table maps integers 0-15 to hex digit strings, avoiding special-case character arithmetic
- How Horner's method accumulates a decimal value from a binary string by doubling and adding each digit left to right
- How the greedy ROMAN table encodes all subtractive numerals and converts integers by divmod from largest to smallest
- Why temperature formulas require affine transformations (multiply and offset) rather than plain scaling
- How a pivot-factor approach normalizes any unit to a base unit first, then scales to the target unit with one multiplication
Prerequisites
- Familiarity with Python integers, strings, and list operations
- Basic understanding of positional notation (decimal and binary)
- No prior knowledge of any specific numeral system required
Decimal to binary (iterative): extract bits from the right by repeated mod-2
conversions/decimal_to_binary.py:4Dividing by 2 and taking the remainder extracts the least-significant bit; repeating until zero collects all bits in right-to-left order.
Converting decimal to binary is the foundational base-conversion operation. The iterative approach applies the standard algorithm directly: divide the number by 2, record the remainder as the next (least significant) bit, and repeat with the quotient. Because remainders accumulate from right to left, the code uses binary.insert(0, num % 2) to prepend each new bit, building the binary string in the correct order. The right-shift num >>= 1 divides by 2 without a division instruction. Negative inputs are handled by setting a flag, converting the absolute value, then prepending a minus sign. The function returns the string with a '0b' prefix to match Python's bin() convention. The doctest confirms that 35 produces '0b100011', which is 32+2+1 in binary, and that negatives produce the expected '-0b10' form.
Iterative decimal-to-binary extracts bits right to left via num % 2 and prepends each to the result list, then right-shifts with num >>= 1 for the next bit.
def decimal_to_binary_iterative(num: int) -> str:
"""
Convert an Integer Decimal Number to a Binary Number as str.
>>> decimal_to_binary_iterative(0)
'0b0'
>>> decimal_to_binary_iterative(2)
'0b10'
>>> decimal_to_binary_iterative(7)
'0b111'
>>> decimal_to_binary_iterative(35)
'0b100011'
>>> # negatives work too
>>> decimal_to_binary_iterative(-2)
'-0b10'
>>> # other floats will error
>>> decimal_to_binary_iterative(16.16) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TypeError: 'float' object cannot be interpreted as an integer
>>> # strings will error as well
>>> decimal_to_binary_iterative('0xfffff') # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TypeError: 'str' object cannot be interpreted as an integer
"""
if isinstance(num, float):
raise TypeError("'float' object cannot be interpreted as an integer")
if isinstance(num, str):
raise TypeError("'str' object cannot be interpreted as an integer")
if num == 0:
return "0b0"
negative = False
if num < 0:
negative = True
num = -num
binary: list[int] = []
while num > 0:
binary.insert(0, num % 2)
num >>= 1
if negative:
return "-0b" + "".join(str(e) for e in binary)
return "0b" + "".join(str(e) for e in binary)Decimal to binary (recursive): reduce to a single bit at each call
conversions/decimal_to_binary.py:55The recursive approach expresses binary conversion as concatenating the binary of n divided by 2 with the remainder, resolving at single-bit base cases.
The iterative and recursive approaches to binary conversion both extract remainders of division by 2, but the recursive version makes the structure of the algorithm explicit as a function definition. The base case returns '0' or '1' when the input is 0 or 1, because single-bit numbers are already in binary. The recursive case uses divmod(decimal, 2) to obtain both the quotient and the remainder in one call, then concatenates the recursive result for the quotient with the string form of the remainder. The concatenation order is critical: the quotient's bits are more significant, so they appear on the left. This mirrors the iterative version's left-prepend pattern but expresses it through the call stack. The doctest shows 1000 in decimal producing '1111101000', and the function also accepts string inputs by wrapping with int(), which the string doctest verifies.
The recursive binary helper defines the conversion as binary(div) + str(mod) where divmod(n, 2) produces both parts, unwinding through the call stack for each bit.
def decimal_to_binary_recursive_helper(decimal: int) -> str:
"""
Take a positive integer value and return its binary equivalent.
>>> decimal_to_binary_recursive_helper(1000)
'1111101000'
>>> decimal_to_binary_recursive_helper("72")
'1001000'
>>> decimal_to_binary_recursive_helper("number")
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: 'number'
"""
decimal = int(decimal)
if decimal in (0, 1): # Exit cases for the recursion
return str(decimal)
div, mod = divmod(decimal, 2)
return decimal_to_binary_recursive_helper(div) + str(mod)Decimal to hexadecimal: a lookup table maps remainders 0-15 to digit characters
conversions/decimal_to_hexadecimal.py:3A lookup table avoids conditional character arithmetic by mapping each base-16 digit value directly to its ASCII representation, including letters a-f.
Hexadecimal conversion introduces a complication that binary does not have: digits above 9 require letter characters. One approach is conditional arithmetic, checking whether the remainder is above 9 and adding an offset to produce the ASCII code for a letter. This file uses a cleaner alternative: a module-level dictionary that maps each integer from 0 to 15 directly to its one-character string. The conversion function (lines 24-74) then only needs to call divmod(decimal, 16) repeatedly and look up values[remainder], building the hex string from the remainders right to left. The doctest confirms 255 produces '0xff', 4096 produces '0x1000', and -256 produces '-0x100'. The final assertion in the doctest, decimal_to_hexadecimal(-256) == hex(-256), verifies the output matches Python's built-in hex function.
The module-level values dict maps remainders 0-15 to hex digit strings, eliminating conditional character arithmetic from the conversion loop entirely.
# set decimal value for each hexadecimal digit
values = {
0: "0",
1: "1",
2: "2",
3: "3",
4: "4",
5: "5",
6: "6",
7: "7",
8: "8",
9: "9",
10: "a",
11: "b",
12: "c",
13: "d",
14: "e",
15: "f",
}Binary to decimal: Horner's method accumulates the value left to right
conversions/binary_to_decimal.py:1Horner's method evaluates positional notation by doubling the accumulator and adding each digit, avoiding power-of-2 computation entirely.
Converting a binary string to decimal requires computing the positional value of each digit. The naive approach computes 2 to the power of each position and multiplies by the digit, which requires knowing the string length upfront. Horner's method avoids the power computation by accumulating left to right: start with 0, multiply by 2, add the current digit, repeat. The final result equals the positional sum because multiplying by 2 at each step effectively shifts all previously accumulated bits one position left, exactly what positional notation requires. The loop decimal_number = 2 * decimal_number + int(char) is the entire algorithm. The function strips whitespace before parsing, handles a leading minus sign, and validates that all characters are binary digits before the loop, raising informative errors for empty strings and non-binary input. The doctest confirms "101" produces 5 and "-11101" produces -29.
Horner's method evaluates binary strings in one pass: decimal_number = 2 * decimal_number + int(char) left to right, avoiding any power-of-2 computation.
def bin_to_decimal(bin_string: str) -> int:
"""
Convert a binary value to its decimal equivalent
>>> bin_to_decimal("101")
5
>>> bin_to_decimal(" 1010 ")
10
>>> bin_to_decimal("-11101")
-29
>>> bin_to_decimal("0")
0
>>> bin_to_decimal("a")
Traceback (most recent call last):
...
ValueError: Non-binary value was passed to the function
>>> bin_to_decimal("")
Traceback (most recent call last):
...
ValueError: Empty string was passed to the function
>>> bin_to_decimal("39")
Traceback (most recent call last):
...
ValueError: Non-binary value was passed to the function
"""
bin_string = str(bin_string).strip()
if not bin_string:
raise ValueError("Empty string was passed to the function")
is_negative = bin_string[0] == "-"
if is_negative:
bin_string = bin_string[1:]
if not all(char in "01" for char in bin_string):
raise ValueError("Non-binary value was passed to the function")
decimal_number = 0
for char in bin_string:
decimal_number = 2 * decimal_number + int(char)
return -decimal_number if is_negative else decimal_numberRoman numerals: a greedy table converts both ways using divmod from largest to smallest
conversions/roman_numerals.py:1Encoding all subtractive Roman numerals in a descending table lets greedy divmod convert any integer to Roman without special-case logic.
Roman numeral encoding looks complex because of subtractive combinations like IV for 4 and CM for 900, but encoding all thirteen of them explicitly in the ROMAN lookup table collapses the special-case logic entirely. The table is sorted in descending order. int_to_roman iterates the table and applies divmod(number, arabic): the quotient tells how many times the symbol repeats, and the remainder becomes the new number. Because the table starts at 1000 and descends, each divmod greedily extracts as much of the largest remaining denomination as possible. The doctest verifies 3999, the largest valid Roman numeral. The reverse direction, roman_to_int, uses a look-ahead: when the current symbol's value is less than the next symbol's value, the pair is a subtractive combination and the code adds the difference and advances by two positions.
Encoding all subtractive numerals in the ROMAN table and applying greedy divmod from largest to smallest converts any integer to Roman numerals without conditionals.
ROMAN = [
(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"),
]
def roman_to_int(roman: str) -> int:
"""
LeetCode No. 13 Roman to Integer
Given a roman numeral, convert it to an integer.
Input is guaranteed to be within the range from 1 to 3999.
https://en.wikipedia.org/wiki/Roman_numerals
>>> tests = {"III": 3, "CLIV": 154, "MIX": 1009, "MMD": 2500, "MMMCMXCIX": 3999}
>>> all(roman_to_int(key) == value for key, value in tests.items())
True
"""
vals = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
total = 0
place = 0
while place < len(roman):
if (place + 1 < len(roman)) and (vals[roman[place]] < vals[roman[place + 1]]):
total += vals[roman[place + 1]] - vals[roman[place]]
place += 2
else:
total += vals[roman[place]]
place += 1
return total
def int_to_roman(number: int) -> str:
"""
Given a integer, convert it to an roman numeral.
https://en.wikipedia.org/wiki/Roman_numerals
>>> tests = {"III": 3, "CLIV": 154, "MIX": 1009, "MMD": 2500, "MMMCMXCIX": 3999}
>>> all(int_to_roman(value) == key for key, value in tests.items())
True
"""
result = []
for arabic, roman in ROMAN:
(factor, number) = divmod(number, arabic)
result.append(roman * factor)
if number == 0:
break
return "".join(result)Temperature conversion: affine formulas require both a scale factor and an offset
conversions/temperature_conversions.py:4Temperature conversions are affine, not linear: they require multiplying by a scale factor and adding an offset because the scales have different zero points.
Temperature conversion is the canonical example of an affine transformation: it is not enough to multiply by a constant because the two scales have different zero points. Celsius and Fahrenheit both measure the same physical temperature, but 0 degrees Celsius corresponds to 32 degrees Fahrenheit, not 0. The formula celsius * 9 / 5 + 32 applies both a scale factor (9/5, accounting for the different degree sizes) and an offset (32, accounting for the different zero points). The doctest documents the known equivalence at -40 degrees, the one temperature where Celsius and Fahrenheit are equal: celsius_to_fahrenheit(-40.0) returns -40.0. The ndigits parameter controls rounding and defaults to 2, so 273.354 returns 524.04 by default. The file contains analogous functions for all common scale pairs including Celsius-Kelvin and Celsius-Rankine.
Temperature conversion needs both a scale factor (9/5) and an offset (32) because Celsius and Fahrenheit have different degree sizes and different zero points.
def celsius_to_fahrenheit(celsius: float, ndigits: int = 2) -> float:
"""
Convert a given value from Celsius to Fahrenheit and round it to 2 decimal places.
Wikipedia reference: https://en.wikipedia.org/wiki/Celsius
Wikipedia reference: https://en.wikipedia.org/wiki/Fahrenheit
>>> celsius_to_fahrenheit(273.354, 3)
524.037
>>> celsius_to_fahrenheit(273.354, 0)
524.0
>>> celsius_to_fahrenheit(-40.0)
-40.0
>>> celsius_to_fahrenheit(-20.0)
-4.0
>>> celsius_to_fahrenheit(0)
32.0
>>> celsius_to_fahrenheit(20)
68.0
>>> celsius_to_fahrenheit("40")
104.0
>>> celsius_to_fahrenheit("celsius")
Traceback (most recent call last):
...
ValueError: could not convert string to float: 'celsius'
"""
return round((float(celsius) * 9 / 5) + 32, ndigits)Length conversion: a pivot-factor table converts any unit to any other via a base unit
conversions/length_conversion.py:28Storing a from-meter and to-meter factor per unit means any unit-to-unit conversion reduces to two multiplications through the base unit.
Supporting conversions between eight length units would require 56 separate conversion factors if every pair were stored explicitly. The pivot-factor design avoids this by normalizing through a single base unit. Each entry in METRIC_CONVERSION is a FromTo named tuple with two fields: from_factor converts the source unit to meters, and to_factor converts meters to the target unit. Any conversion then costs two multiplications: multiply the input by METRIC_CONVERSION[from].from_factor to reach meters, then multiply by METRIC_CONVERSION[to].to_factor to reach the destination. Adding a new unit requires only one new table entry rather than updating all existing entries. The TYPE_CONVERSION dict above it normalizes name variants (singular, plural, abbreviation) to canonical abbreviations, so "miles", "mile", and "mi" all resolve to "mi" before the lookup.
Storing (to-meter, from-meter) factor pairs in METRIC_CONVERSION means any unit pair converts in two multiplications, and adding a new unit requires one table entry.
class FromTo(NamedTuple):
from_factor: float
to_factor: float
TYPE_CONVERSION = {
"millimeter": "mm",
"centimeter": "cm",
"meter": "m",
"kilometer": "km",
"inch": "in",
"inche": "in", # Trailing 's' has been stripped off
"feet": "ft",
"foot": "ft",
"yard": "yd",
"mile": "mi",
}
METRIC_CONVERSION = {
"mm": FromTo(0.001, 1000),
"cm": FromTo(0.01, 100),
"m": FromTo(1, 1),
"km": FromTo(1000, 0.001),
"in": FromTo(0.0254, 39.3701),
"ft": FromTo(0.3048, 3.28084),
"yd": FromTo(0.9144, 1.09361),
"mi": FromTo(1609.34, 0.000621371),
}You've walked through 7 key areas of the The Algorithms - Python codebase.
Continue: Bit Manipulation in TheAlgorithms/Python → Browse all projectsCreate code tours for your project
Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.
Install Intraview Free