Transforming numerical basis with python

There are 10 types of people.

Those who know binary numbering and those who don't!

The mathematical joke above is attributed to Ian Stewart, and its humorous take comes from playing with the idea that in binary numbering the symbol 10 represents the decimal number two.

Today, I wanted to explore the basis transformation of numbers.

We are accustomed to the decimal system, which is the representation of numbers in base 10. The basic symbols to represent a number are the digits 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9.

In order to represent a quantity bigger than nine, we restart the sequence from 0, but add a second digit to the left of our first (this time we assume there was a zero before, so incrementing it would result in placing a 1).

Hence, the number following the nine (9), would be represented by the symbol 10! 😀

What if…

What if we use a different basis, but playing by the same rules⁉️

Ok, let's reanalyze the algorithm.

  1. Set a basis, e.g. 2.
  2. Start representing the zero by the symbol 0.
  3. Increase the number, and represent them following the predetermined sequence of symbols, until you have as many symbols as your basis.
    • When you reach the maximum symbol in your sequence, restart the sequence from zero while increase by one step the next symbol to the left.
    • If there is no symbol to the left, assume there is a 0.
  4. Repeat the third step until you get to the desired number.

Let's explore how to write the number twelve in base 3.

The sequence of three symbols (because we are in base 3) are 0, 1 and 2. I'll build a table to illustrate the algorithm:

Number Decimal repr. Ternary Repr.
zero 0 0
one 1 1
two 2 2
three 3 10
four 4 11
five 5 12
six 6 20
seven 7 21
eight 8 22
nine 9 100
ten 10 101
eleven 11 102
twelve 12 110

Note that the multiples of the basis (three, six, nine, and twelve, in the ternary representation, and ten in the decimal representation), are represented by symbols ending in 0. This observation might remind you the concept of division remainder.

division-parts.png

In python the division operator is /. However, we can extract the entire quotient through the operator // and the remainder using the operator %.

dividend = 75
divisor = 4
print(f"{dividend} / {divisor} = {dividend/divisor}")
print(f"{dividend} // {divisor} = {dividend//divisor}")
print(f"{dividend} % {divisor} = {dividend%divisor}")
75 / 4 = 18.75
75 // 4 = 18
75 % 4 = 3

Let's take the number five in decimal representation and find the division remainder (also called modulo)

dividend = 5
divisor = 3
print(f"Exact division: {dividend} / {divisor} = {dividend/divisor}")
print(f"Entire division: {dividend} // {divisor} = {dividend//divisor}")
print(f"Remainder: {dividend} % {divisor} = {dividend%divisor}")
Exact division: 5 / 3 = 1.6666666666666667
Entire division: 5 // 3 = 1
Remainder: 5 % 3 = 2

At this point, we note that the entire division is 1 and the modulo is 2, while the number five is represented in ternary numbering by the symbol 12 (composed by 1 and 2)❗

If we try our algorithm starting with eleven as dividend, we get:

dividend = 11
divisor = 3
print(f"Exact division: {dividend} / {divisor} = {dividend/divisor}")
print(f"Entire division: {dividend} // {divisor} = {dividend//divisor}")
print(f"Remainder: {dividend} % {divisor} = {dividend%divisor}")
Exact division: 11 / 3 = 3.6666666666666665
Entire division: 11 // 3 = 3
Remainder: 11 % 3 = 2

Our hunch would drive us to propose that eleven is represented by the symbol 32 in ternary numbering, but 3 is not a valid symbol in this basis.

However, the number from the entire division (three) might be divided by the basis again:

dividend = 3
divisor = 3
print(f"Exact division: {dividend} / {divisor} = {dividend/divisor}")
print(f"Entire division: {dividend} // {divisor} = {dividend//divisor}")
print(f"Remainder: {dividend} % {divisor} = {dividend%divisor}")
Exact division: 3 / 3 = 1.0
Entire division: 3 // 3 = 1
Remainder: 3 % 3 = 0

And here, the representation of three would be 10, as expected from the table.

If we replace the symbol 3 in our previous result (32), we obtain 102 as expected from the table.

The algorithm

  1. Define the origial_basis and the target_basis
    • Build the sequences of valid symbols for each numbering.
  2. Assign a decimal value to each symbol in the sequences (since our mathematical operations (e.g. division) are defined in decimal basis), starting from zero.
  3. Provide a number represented in the original_basis.
  4. Transform the number to decimal basis
  5. Divide the decimal representation of the number, separating the entire quotient and the remainder.
    • The remainder is the right-most symbol of the number in the target_basis representation.
    • If the quotient is a valid symbol in the target_basis, it has to be stacked to the left of the modulo. Otherwise, use the entire quotient as a new divided, and repeat the step 5.
  6. The expected result is the stack of the moduli.

Hexadecimal numbering

In decimal numbering we have ten symbols, from 0 to 9, to represent each value. But, Could we define numbering with bases greater that ten?

The answer is YES. We just have to add more symbols to represent the numbers. A useful ordered set we all know about is the sequence of letters in the Latin alphabet.

PROPOSAL: Start with symbols in the decimal numbering, and if you need more add as much as you need from the Latin alphabet.

Hence, the hexadecimal numbering uses the symbols: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e and f.

The English version of the Latin alphabet has 26 symbols. Hence, using our proposal we could build numbering with basis up to 36.

Code

In our algorithm the first step is assigning values to the source and target numbering, which can be obtained from the user as inputs.

Hence, let's start defining a function that assigns decimal values to the symbols. We can use order of the character to assign its value.

def char_to_val(ch: str) -> int | None:
    """
    Return the decimal numeric value of a symbol (digit/letter),
    or None if the character is not a valid base-36 digit.
    """
    ch = ch.upper()
    code = ord(ch)

    # '0'-'9' → 0-9
    if 48 <= code <= 57:   # ord('0') = 48
        return code - 48
    # 'A'-'Z' → 10-35
    if 65 <= code <= 90:   # ord('A') = 65
        return code - 55   # 65 → 10, 66 → 11, …, 90 → 35

    return None            # not a digit/letter

For rendering the result we need its inverse, a function that transforms an elementary ("valid") decimal value (within the domain of the target basis) back to its symbol:

def val_to_char(val: int) -> str:
    """
    Convert a numeric value (0-35) to its corresponding character.
    0-9 → '0'-'9', 10-35 → 'A'-'Z'.
    """
    if 0 <= val <= 9:
        return chr(val + 48)           # 0 → '0'
    elif 10 <= val <= 35:
        return chr(val + 55)           # 10 → 'A'
    else:
        raise ValueError("Value out of range for base-36 digit")

We move now to the transformation of the symbol into its decimal representation:

def parse_from_base(num_str: str, src_base: int) -> int:
    """
    Convert a string written in `src_base` to its decimal integer value.
    Characters not legal for `src_base` are ignored.
    """
    if not (2 <= src_base <= 36):
        raise ValueError("Source base must be between 2 and 36")

    total = 0
    for ch in num_str:
        val = char_to_val(ch)
        # Skip characters that aren't digits/letters or exceed the source base
        if val is None or val >= src_base:
            continue
        total = total * src_base + val
    return total

The int() function, which is used to cast onto the domain of integers, admits a base argument, whose purpose it to instruct the int() function to assume that the first argument of the function is represented in certain numerical basis, for example: int("102", base=3) would return the decimal number eleven.

Finally, we need a function that execute the fifth step in our algorithm:

def render_to_base(value: int, tgt_base: int) -> str:
    """
    Convert a non-negative integer `value` to a string in `tgt_base`.
    Uses repeated division; returns "0" for the value zero.
    """
    if not (2 <= tgt_base <= 36):
        raise ValueError("Target base must be between 2 and 36")
    if value == 0:
        return "0"

    digits = []
    while value > 0:
        value, rem = divmod(value, tgt_base)
        digits.append(val_to_char(rem))
    # Digits were collected least-significant first → reverse
    return "".join(reversed(digits))

🧩 Putting the puzzle all together!!!

def char_to_val(ch: str) -> int | None:
    """
    Return the numeric value of a digit/letter, or None if the character
    is not a valid base-36 digit.
    """
    ch = ch.upper()
    code = ord(ch)

    # '0'-'9' → 0-9
    if 48 <= code <= 57:               # ord('0') = 48
        return code - 48

    # 'A'-'Z' → 10-35
    if 65 <= code <= 90:               # ord('A') = 65
        return code - 55              # 65 → 10, 66 → 11, …, 90 → 35

    return None                        # not a digit/letter


def val_to_char(val: int) -> str:
    """
    Convert a numeric value (0-35) to its corresponding character.
    0-9 → '0'-'9', 10-35 → 'A'-'Z'.
    """
    if 0 <= val <= 9:
        return chr(val + 48)           # 0 → '0'
    elif 10 <= val <= 35:
        return chr(val + 55)           # 10 → 'A'
    else:
        raise ValueError("Value out of range for base-36 digit")


def parse_from_base(num_str: str, src_base: int) -> int:
    """
    Convert a string written in `src_base` to its decimal integer value.
    Characters not legal for `src_base` are ignored.
    """
    if not (2 <= src_base <= 36):
        raise ValueError("Source base must be between 2 and 36")

    total = 0
    for ch in num_str:
        val = char_to_val(ch)
        # Skip characters that aren't digits/letters
        # or exceed the source base
        if val is None or val >= src_base:
            continue
        total = total * src_base + val
    return total


def render_to_base(value: int, tgt_base: int) -> str:
    """
    Convert a non-negative integer `value` to a string in `tgt_base`.
    Uses repeated division; returns "0" for the value zero.
    """
    if not (2 <= tgt_base <= 36):
        raise ValueError("Target base must be between 2 and 36")
    if value == 0:
        return "0"

    digits = []
    while value > 0:
        value, rem = divmod(value, tgt_base)
        digits.append(val_to_char(rem))
    # Digits were collected least-significant first → reverse
    return "".join(reversed(digits))


def main() -> None:
    try:
        src_base = int(input("Enter source base (2-36): ").strip())
        tgt_base = int(input("Enter target base (2-36): ").strip())
    except ValueError:
        print("Base entries must be integers.")
        return

    raw_number = input(
        f"Enter the number in base {src_base}: "
    ).strip()

    # Step 1: parse → decimal integer
    decimal_val = parse_from_base(raw_number, src_base)

    # Step 2: render → target base string
    converted = render_to_base(decimal_val, tgt_base)

    print(f"\nInterpreted decimal value : {decimal_val}")
    print(f"Converted to base {tgt_base}: {converted}")


if __name__ == "__main__":
    main()

change-numerical-basis.png

Author: Oscar Castillo-Felisola

Created: 2026-04-02 Thu 14:59