Skip to content

Binary to Decimal Converter

Converting numbers between bases is the bridge between how we count (base 10) and how computers count (base 2). In this project you build a Python converter that goes both ways between binary and decimal, then extend it to handle octal, hexadecimal, arbitrary bases (2–36), negative numbers via two’s complement, and a peek at how floating-point numbers are stored in IEEE 754. Along the way you implement both the “use the built-in” version and the “from scratch” algorithm so you understand what int(s, 2) and bin() actually do.

You will learn:

  • The mechanics of positional number systems.
  • Python’s int(string, base) and bin() / oct() / hex().
  • How to convert any base in pure Python.
  • Two’s complement for representing negative integers.
  • IEEE 754 single-precision float layout.
  • Input validation and friendly error reporting.
  • Base 2 (binary) — the physical layer of every digital computer (transistors are on/off).
  • Base 8 (octal) — Unix file permissions (chmod 755).
  • Base 10 (decimal) — humans.
  • Base 16 (hex) — RGB colors (#ff8000), memory addresses, MAC addresses, byte dumps.
  • Python 3.6 or above.
  • A text editor or IDE.
  • Comfort with input(), if/elif/else, and basic loops.
  1. Create folder binary-decimal-converter.
  2. Inside, create binary_to_decimal.py.
Binary to Decimal pch.viewSource
Binary to Decimal
"""Binary <-> decimal, then any base from 2 to 36, then two's complement.

`int(s, 2)` and `bin(n)` already do the first job in one call. The hand-written
versions are here because the point of the project is the algorithm: Horner's
method going one way, repeated division going the other. The built-ins stay in
the tests, as the oracle the hand-written code is checked against.

    python binary_to_decimal.py          # menu; unattended it plays DEMO_ANSWERS
    python binary_to_decimal.py --test   # check both directions against int()/bin()
"""

import sys
import unittest

DEMO_ANSWERS = iter(["1", "1011", "2", "11", "4", "255", "16", "3"])


def ask(prompt, default=""):
    """Read a line, or take the next scripted answer when nobody is there."""
    try:
        answer = input(prompt)
    except EOFError:
        answer = next(DEMO_ANSWERS, default)
        print(f"{answer}   (scripted demo answer)")
        return answer
    return answer.strip() or default


def bin_to_dec(s: str) -> int:
    """Horner's method: one multiply-add per digit, left to right.

    Reading `1011` gives 0 -> 1 -> 2 -> 5 -> 11. No powers, no exponent table;
    each step just doubles what came before and adds the new bit.
    """
    total = 0
    for digit in s:
        total = total * 2 + int(digit)
    return total


def dec_to_bin(n: int) -> str:
    """Repeated division, which produces the digits backwards."""
    if n == 0:
        return "0"
    if n < 0:
        return "-" + dec_to_bin(-n)
    out = []
    while n:
        out.append(str(n & 1))                # last bit
        n >>= 1                               # shift right
    return "".join(reversed(out))


DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"


def to_base(n: int, base: int) -> str:
    if not 2 <= base <= 36:
        raise ValueError("base must be 2..36")
    if n == 0:
        return "0"
    sign = "-" if n < 0 else ""
    n = abs(n)
    out = []
    while n:
        out.append(DIGITS[n % base])
        n //= base
    return sign + "".join(reversed(out))


def from_base(s: str, base: int) -> int:
    """The inverse of `to_base`, rejecting digits the base does not have.

    The validation loop is not decoration: without it `from_base("19", 8)`
    quietly returns 9, because `DIGITS.index("9")` is a perfectly good number
    -- it is just not a legal octal digit.
    """
    s = s.lower().strip()
    sign = 1
    if s.startswith("-"):
        sign, s = -1, s[1:]
    if not s:
        raise ValueError("empty string is not a number")
    for ch in s:
        if ch not in DIGITS or DIGITS.index(ch) >= base:
            raise ValueError(f"'{ch}' is not valid in base {base}")
    return sign * sum(DIGITS.index(c) * (base ** i)
                      for i, c in enumerate(reversed(s)))


def to_twos_complement(n: int, bits: int = 8) -> str:
    """How the machine actually stores a negative number.

    There is no sign bit to set; -5 is stored as the 8-bit number that, added
    to 5, wraps back to zero. That is why the conversion is an addition.
    """
    if not -(1 << (bits - 1)) <= n < (1 << (bits - 1)):
        raise ValueError(f"{n} does not fit in {bits} signed bits")
    if n < 0:
        n = (1 << bits) + n               # wrap into range
    return format(n, f"0{bits}b")


def from_twos_complement(s: str) -> int:
    bits = len(s)
    n = int(s, 2)
    return n - (1 << bits) if s[0] == "1" else n


def main():
    while True:
        print("\n1. Binary to decimal   2. Decimal to binary")
        print("3. Exit                4. Any base (2-36)")
        choice = ask("> ", "3").strip()
        if choice == "1":
            raw = ask("Binary: ", "1011").strip()
            if not raw or not all(c in "01" for c in raw):
                print("Use only 0s and 1s.")
                continue
            print(f"{raw} base 2 = {bin_to_dec(raw)} base 10")
        elif choice == "2":
            raw = ask("Decimal: ", "11").strip()
            try:
                n = int(raw)
            except ValueError:
                print("Not an integer.")
                continue
            print(f"{n} base 10 = {dec_to_bin(n)} base 2")
            print(f"  as 8-bit two's complement: {to_twos_complement(n)}")
        elif choice == "3":
            print("Bye.")
            break
        elif choice == "4":
            raw = ask("Decimal: ", "255").strip()
            base = ask("Target base (2-36): ", "16").strip()
            try:
                print(f"{int(raw)} base 10 = {to_base(int(raw), int(base))} "
                      f"base {base}")
            except ValueError as exc:
                print(f"Cannot convert: {exc}")
        else:
            print("Invalid choice.")


class TestConversions(unittest.TestCase):
    """The built-ins are the oracle; the hand-written code has to match them."""

    def test_bin_to_dec_matches_int(self):
        for n in range(256):
            self.assertEqual(bin_to_dec(format(n, "b")), n)

    def test_dec_to_bin_matches_bin(self):
        for n in range(256):
            self.assertEqual(dec_to_bin(n), bin(n)[2:])

    def test_base_round_trip(self):
        for base in range(2, 37):
            for n in (0, 1, 7, 255, 4095):
                self.assertEqual(from_base(to_base(n, base), base), n)

    def test_rejects_digit_outside_base(self):
        with self.assertRaises(ValueError):
            from_base("19", 8)

    def test_twos_complement(self):
        self.assertEqual(to_twos_complement(-5, 8), "11111011")
        self.assertEqual(from_twos_complement("11111011"), -5)
        for n in range(-128, 128):
            self.assertEqual(from_twos_complement(to_twos_complement(n)), n)


if __name__ == "__main__":
    if "--test" in sys.argv:
        unittest.main(argv=sys.argv[:1], exit=False)
    else:
        main()
command
C:\Users\Your Name\binary-decimal-converter> python binary_to_decimal.py
Binary ↔ Decimal Converter
1. Binary → Decimal
2. Decimal → Binary
3. Exit
Choice: 1
Binary: 1101
1101₂ = 13₁₀

Running the file exactly as it ships takes 0.1 s and prints:

python binary_to_decimal.py
 
1. Binary to decimal   2. Decimal to binary
3. Exit                4. Any base (2-36)
> 1   (scripted demo answer)
Binary: 1011   (scripted demo answer)
1011 base 2 = 11 base 10
 
1. Binary to decimal   2. Decimal to binary
3. Exit                4. Any base (2-36)
> 2   (scripted demo answer)
Decimal: 11   (scripted demo answer)
11 base 10 = 1011 base 2
  as 8-bit two's complement: 00001011
 
1. Binary to decimal   2. Decimal to binary
3. Exit                4. Any base (2-36)
> 4   (scripted demo answer)
Decimal: 255   (scripted demo answer)
Target base (2-36): 16   (scripted demo answer)
255 base 10 = ff base 16
...

The first 20 of 25 lines are shown; the run continues past this point.

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid

1. Using built-ins (the right answer for production)

Section titled “1. Using built-ins (the right answer for production)”
builtins.py
# Binary string → decimal int
decimal = int("1101", 2)        # 13
 
# Decimal int → binary string (with '0b' prefix)
binary = bin(13)                # '0b1101'
 
# Strip the prefix if you want just the digits
binary_clean = bin(13)[2:]      # '1101'
 
# Also available:
hexadecimal = hex(13)           # '0xd'
octal       = oct(13)           # '0o15'

int(s, base) accepts any base from 2 to 36. Above 10, digits use letters a..z (case-insensitive).

menu.py
def main():
    while True:
        print("\n1. Binary → Decimal  2. Decimal → Binary  3. Exit")
        choice = input("> ").strip()
        if choice == "1":
            raw = input("Binary: ").strip()
            if not all(c in "01" for c in raw) or not raw:
                print("Use only 0s and 1s."); continue
            print(f"{raw}₂ = {int(raw, 2)}₁₀")
        elif choice == "2":
            raw = input("Decimal: ").strip()
            try:
                n = int(raw)
            except ValueError:
                print("Not an integer."); continue
            print(f"{n}₁₀ = {bin(n)[2:]}₂")
        elif choice == "3":
            break
        else:
            print("Invalid choice.")

Two validation guards — character set for binary, int() for decimal — and friendly error messages.

bin2dec.py
def bin_to_dec(s: str) -> int:
    total = 0
    for digit in s:
        total = total * 2 + int(digit)        # Horner's method
    return total

Trace for "1101":

text
start    total = 0
'1'      total = 0*2 + 1 = 1
'1'      total = 1*2 + 1 = 3
'0'      total = 3*2 + 0 = 6
'1'      total = 6*2 + 1 = 13

That single multiply-and-add idiom is Horner’s method and works for any base (replace 2 with base).

dec2bin.py
def dec_to_bin(n: int) -> str:
    if n == 0: return "0"
    if n < 0:  return "-" + dec_to_bin(-n)
    out = []
    while n:
        out.append(str(n & 1))                # last bit
        n >>= 1                               # shift right
    return "".join(reversed(out))
  • n & 1 extracts the least-significant bit (0 or 1).
  • n >>= 1 shifts right, dropping that bit.
  • Building backwards then reversing avoids inefficient string prepend.
any_base.py
DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"
 
def to_base(n: int, base: int) -> str:
    if not 2 <= base <= 36:
        raise ValueError("base must be 2..36")
    if n == 0: return "0"
    sign = "-" if n < 0 else ""
    n = abs(n)
    out = []
    while n:
        out.append(DIGITS[n % base])
        n //= base
    return sign + "".join(reversed(out))
 
def from_base(s: str, base: int) -> int:
    s = s.lower().strip()
    sign = 1
    if s.startswith("-"): sign, s = -1, s[1:]
    n = 0
    for ch in s:
        digit = DIGITS.index(ch)
        if digit >= base:
            raise ValueError(f"'{ch}' is not valid in base {base}")
    return sign * sum(DIGITS.index(c) * (base ** i)
                      for i, c in enumerate(reversed(s)))

Now to_base(255, 16)"ff", from_base("ff", 16)255. The exact same algorithm handles base-3, base-7, base-32, base-36.

Computers represent signed integers as two’s complement. For an 8-bit number:

  • Positive 5 → 0000 0101.
  • Negative 5 → invert bits, add 1 → 1111 1011.
twos.py
def to_twos_complement(n: int, bits: int = 8) -> str:
    if n < 0:
        n = (1 << bits) + n               # wrap into range
    return format(n, f"0{bits}b")
 
def from_twos_complement(s: str) -> int:
    bits = len(s)
    n = int(s, 2)
    return n - (1 << bits) if s[0] == "1" else n
 
print(to_twos_complement(-5, 8))    # 11111011
print(from_twos_complement("11111011"))  # -5

This is exactly how a C int8_t works.

Python’s struct module lets you peek at how 32-bit floats are stored:

ieee754.py
import struct
bits = struct.pack(">f", 3.14)        # big-endian float
binary = "".join(f"{b:08b}" for b in bits)
print(binary)
# 01000000010010001111010111000011
# sign: 0, exponent: 10000000 (=128 → bias-adjusted 1),
# mantissa: 10010001111010111000011

This is the layout every desktop CPU uses. It explains why 0.1 + 0.2 != 0.3 in every language with IEEE 754 floats.

ProblemCauseFix
ValueError: invalid literal for int()Non-binary characters in inputValidate with all(c in "01" for c in s)
bin(13) returns '0b1101' not '1101'The 0b prefix is informativeSlice [2:]
Manual int(digit) * 2**i is O(n²) string-wiseBad algorithmUse Horner: total = total * base + d
Negative numbers print as -0b101Built-in bin() includes the signEither accept it, or build your own
Leading zeros lostNumbers do not preserve formatPad: format(n, "08b")
int(s, 0) infers wrong base0 means “guess from prefix” — 0b, 0o, 0xPass an explicit base

Extend the menu with Hex → Decimal, Decimal → Octal, etc.

Print each conversion plus a timestamp; export to CSV.

Read a list of numbers from a file; emit a converted column.

cli
python convert.py 255 --from 10 --to 16    # → ff
python convert.py ff  --from 16 --to 2     # → 11111111

Tkinter with two big text boxes and base dropdowns. Type in either box; the other updates live.

For each step of the decimal→binary algorithm, show the running value, the bit being extracted, and the resulting binary so far. Great for classrooms.

Extend to fractions: 0.6250.101. Loop value *= 2; integer part is the next bit.

Use the function above; ask the user how many bits.

Parse #ff8000(255, 128, 0) in RGB.

A Flask form (see Basic Web Server) that converts on submit.

  • Networking — IPv4 192.168.1.1 to its 32-bit binary representation, subnet calculations.
  • Color manipulation#rrggbb ↔ tuples of decimal.
  • Unix permissionschmod 755 is octal rwxr-xr-x.
  • Reverse engineering — interpreting hex dumps and memory addresses.
  • Cryptography — XOR, bit rotations, hex-encoded keys.
  • Embedded programming — register values are hex; bit fields matter.
  • Positional number systems — the most fundamental abstraction in computing.
  • int(s, base) — one of Python’s most underused built-ins.
  • Horner’s method — fewer multiplications than digit * base ** i.
  • Bitwise operators&, |, ^, <<, >>.
  • Two’s complement — the actual representation of signed integers.
  • Extend to octal and hex.
  • Implement to_base / from_base for arbitrary bases.
  • Add two’s complement handling.
  • Build a GUI with live conversion as the user types.
  • Read about IEEE 754 to understand why 0.1 + 0.2 != 0.3.

Click any bit to flip it. Each 1 contributes its place value (128, 64, 32, …), and the total is the decimal number — the same math your code does:

sketch Flip the bits p5.js
Click a bit to toggle it. Each 1 adds its place value; the total is the decimal number.

You built a converter both ways using built-ins, then implemented the math by hand so the int(s, 2) and bin(n) calls stopped looking like magic. The same algorithms power every hex dump, color picker, and networking calculator you have ever used. Full source on GitHub. Find more algorithm projects on Python Central Hub.

  • int(binary, 2) already exists. Writing the conversion by hand is worth doing once to understand positional notation; shipping it is not. The built-in also validates its input, which hand-rolled loops usually forget to.
  • bin(11) returns '0b1011', not '1011'. The prefix is there so the string round-trips through int(x, 0). Strip it with bin(n)[2:] when you want just the digits.
  • No validation on the input. int('1021', 2) raises ValueError, but a hand-written loop that multiplies by 2 and adds the digit will happily accept 1021 and return a number that means nothing.
  • Binary is a representation, not a different number. 0b1011, 11 and 0xB are three spellings of one value; the conversion changes how it is written, never what it is.
  • 1011 in base 2 is 11 in base 10 — measured by running the file.
  • int(text, 2) and bin(n) are the built-ins; the loop is for understanding, not for shipping.
  • bin() includes a 0b prefix that int(x, 2) does not want back.
  • The program accepts a malformed binary string without complaint, which is the gap worth closing first.
  • ask() supplies option 1 and 1011 when nothing is typed, which is what makes the output above reproducible.
pch.quizTag pch.quizDefaultTitle
  1. What does `bin(11)` return?

    pch.quizShowAnswer

    B — '0b1011'

  2. A hand-written loop converts '1021' from binary without raising. What has gone wrong?

    pch.quizShowAnswer

    B — The loop never checks that each character is 0 or 1, so it produces a number that corresponds to no binary value — where `int('1021', 2)` would have raised ValueError

  3. Is converting 11 to '1011' a change of value or of notation?

    pch.quizShowAnswer

    B — Of notation only. 11, 0b1011 and 0xB are three ways of writing one number, and Python treats them as equal

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading