In the previous article, we learned how to read and write numbers in different bases, such as binary and hexadecimal. Now we are going to do arithmetic with those numbers, because computers store data and also process it.
The good news is that you do not need to relearn math. The addition and subtraction rules you learned in elementary school for the decimal base (base 10) are universal. The idea of a “carry” or of “borrowing” works exactly the same way, whether you are counting apples (decimal) or electrical pulses (binary).
In this article, we will revisit these fundamental operations in base 10 to isolate the algorithm and then apply exactly the same logic to perform calculations like a computer.
Decimal base #
The decimal base, also known as base 10, is the most widely used number system in everyday life. It is made up of the digits 0 to 9 and uses a positional system, where the value of each digit depends on its position within the number.
Addition in the decimal base - practical rule #
Adding decimal numbers follows a positional process, where the digits are added column by column, starting from the units (right) and moving to the left.
The “secret” here is to generalize the rule: the carry occurs whenever the sum reaches or exceeds the value of the base. In the decimal case, if it came to 10 or more, carry one.
Example: Sum \(67 + 84\) in base 10
Step by step:
- We add the units column: \(7 + 4 = 11\). Since the result has two digits (it went past the base), we put the 1 in the units position and carry the other 1 to the next column (called the “carry”).
- We add the tens column: \(6 + 8 = 14\) and add the “carry”, giving \(14 + 1 = 15\). Since there are no more columns, 15 is written as the final result.
Therefore, the result of the sum is 151.
The Carry Movement #
The carry occurs when the sum of two digits results in a value greater than 9, since 10 is the base of the decimal system and the carry occurs whenever the sum reaches or exceeds the base, as we have already seen. In that case, we keep the units digit in the corresponding column and carry the tens digit to the next column.
Example: Sum \(489 + 276\) in base 10
- Units: \(9 + 6 = 15\), we write the 5 and carry 1 to the tens column.
- Tens: \(8 + 7 = 15\), we add the “carry” (\(15 + 1 = 16\)), write 6 and carry 1 to the next column.
- Hundreds: \(4 + 2 = 6\), we add the “carry” (\(6 + 1 = 7\)), giving 7.
The final result is 765.
Addition in the decimal base - formal algorithm #
Adding decimal numbers follows a positional process, as we saw. To better understand how this will work in other bases, let us look at the formal algorithm: we add the column and check the result.
If the value is greater than or equal to the base (in this case, 10), we cannot represent it with a single digit. So we subtract the value of the base (10 in this case) to find what stays in the current column and, in exchange, we send 1 to the next column (the carry).
Let us repeat the previous example, but now detailing the algorithm formally.
Example: Sum \(67 + 84\) in base 10 (Formal Algorithm)
Step by step through the algorithm:
- Units: \(7 + 4 = 11\). Since \(11 \ge 10\), we compute \(11 - 10 = 1\). The 1 stays and we generate a “carry”.
- Tens: \(6 + 8 = 14\). We add the “carry” (\(15\)). Since \(15 \ge 10\), we compute \(15 - 10 = 5\). The 5 stays and we generate a “carry” for the next column (hundreds).
Subtraction in the Decimal Base - Practical Rule #
Subtraction also follows a positional process. In this article, we will adopt the equal additions method (or compensation method, which some European schools call the Austrian method), which we will call “carry sub”.
Whenever you need to “borrow”, instead of decreasing the top value, we add 1 to the immediately higher place of the subtrahend (the bottom number). This keeps the difference correct but leaves the look of the calculation cleaner.
Perhaps you learned in school the method of “borrowing” by crossing out the top number (e.g., the 7 becomes 6 to lend 1 to the 5). Both methods are mathematically equivalent. We will use the compensation method here because it makes the visual organization easier in computational algorithms. It has its own history and a direct connection to the next article in this series, which deals with the method of complements, the idea that hardware uses to avoid having to subtract.
Example: Subtraction \(75 - 38\) (carry sub)
Step by step:
- Units: \(5 - 8\) is not possible. To get around this, we add 10 to the minuend in the units place (the \(5\) becomes 15) and add 1 to the tens place of the subtrahend (the \(3\) becomes 4). This way, the difference remains unchanged.
- Now we subtract the units: \(15 - 8 = 7\).
- Tens: after the adjustment (the 3 became 4), we subtract \(7 - 4 = 3\).
In the end, we get \(37\) as the result.
The Borrowing Process - carry sub #
In the “carry sub” method, whenever the upper place is not enough for the subtraction, we add 10 (the value of the base) to the minuend digit in that place and, simultaneously, add 1 to the immediately next place of the subtrahend.
Example: Subtraction \(402 - 189\) (carry sub)
- Units: we need to compute \(2 - 9\). Since \(2 < 9\), we add 10 to the 2 (it becomes 12) and add 1 to the tens place of the subtrahend (8 becomes 9). Now we subtract: \(12 - 9 = 3\).
- Tens: we now have \(0 - 9\). Again it is not possible without “borrowing”: we add 10 to the 0 (it becomes 10) and add 1 to the hundreds place of the subtrahend (1 becomes 2). Then we compute \(10 - 9 = 1\).
- Hundreds: what is left is \(4 - 2\), which is \(2\).
This way, we obtain the result \(213\).
Subtraction in the Decimal Base - Formal Algorithm #
Subtraction also follows a positional process. In the compensation method (carry sub), the algorithmic logic is clear: if the top digit is smaller than the bottom one, we add the value of the base (10 in this case) to the top digit to make the subtraction possible.
To keep the equation balanced, we must compensate by adding 1 to the next column of the bottom number (the subtrahend).
Example: Subtraction \(75 - 38\) in base 10 (Formal Algorithm)
Step by step through the algorithm:
- Units: \(5 - 8\) is not possible. We add the base to the 5 (\(5 + 10 = 15\)). Now we compute \(15 - 8 = 7\). We compensate by adding \(+1\) to the neighbor below (the 3).
- Tens: Now we have \(7\) on top and \(3 + 1 = 4\) on the bottom. We compute \(7 - 4 = 3\).
Other bases #
Addition in different bases #
Addition in other bases follows the same principle as the decimal base: the “carry” occurs whenever the result of a sum exceeds the largest value allowed in the base. Now the formal algorithm becomes more evident: if the value of the sum is greater than or equal to the base, we subtract the value of the base to find what stays in the current column and send 1 to the next column.
Let us go to the practical examples:
Example: Sum in base 9
Base 9 has digits from 0 to 8. Whenever the sum is greater than or equal to the base (9), the “carry” is added to the next column.
$$ \begin{array}{c c c c} {\scriptscriptstyle 1} & \scriptscriptstyle 1 & & {} \\ {} & 6 & 7 & {} \\ {} + & 8 & 4 & {} \\ \hline 1 & (15) & (11) & {} \\ {} - & 9 & 9 & {\text{ subtracting the base}} \\ \hline 1 & 6 & 2 & {} \end{array} $$Step by step:
- Units: \(7 + 4 = 11\). We write 2, the result of \(11 - 9\) (base), and carry 1 to the next column.
- Tens: \(6 + 8 = 14\). We add the “carry” and get \(14 + 1 = 15\). The result is 6 and we carry 1 to the next column.
Therefore, the result of the sum \(67 + 84\) in base 9 is \(162_9\).
Example: Sum in base 16
Base 16 has digits from 0 to F (representing 15). Whenever the sum is greater than or equal to the base (16), the “carry” is added to the next column.
$$ \begin{array}{c c c c c} {\scriptscriptstyle 1} & & \scriptscriptstyle 1 & & {} \\ {} & F & 1 & A & {} \\ {} + & E & 0 & 9 & {} \\ \hline 1 & (29) & 2 & (19) & {} \\ {} - & 16 & & 16 & {\text{ subtracting the base}} \\ \hline 1 & D & 2 & 3 & {} \end{array} $$Result: \(\mathtt{F1A} + \mathtt{E09} = \mathtt{1D23_{16}}\).
Before looking at the base 2 examples, it is worth keeping in mind the basic binary addition “table”, which is where most of the confusion happens:
- \(0 + 0 = 0\)
- \(0 + 1 = 1\)
- \(1 + 0 = 1\)
- \(1 + 1 = 10_2\) (Write 0 and carry 1)
- \(1 + 1 + 1 = 11_2\) (Write 1 and carry 1)
Example: Sum in base 2
Base 2 has digits 0 and 1. Whenever the sum is greater than or equal to the base (2), the “carry” is added to the next column.
$$ \begin{array}{c c c c c c c} \scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & & {} \\ {} & 1 & 0 & 1 & 1 & 1 & {} \\ {} + & 1 & 1 & 0 & 1 & 1 & {} \\ \hline & (3) & (2) & (2) & (3) & (2) & {} \\ {} - & 2 & 2 & 2 & 2 & 2 & {\text{ subtracting the base}} \\ \hline 1 & 1 & 0 & 0 & 1 & 0 & {} \end{array} $$Result: \(10111_2 + 11011_2 = 110010_2\).
In base 2, we saw that \(1_2 + 1_2 = 10_2\) and \(1_2 + 1_2 + 1_2 = 11_2\). So it can be easier to add binary numbers if we remember these sums by heart.
Example: Sum in base 2 using knowledge of the values of 2 and 3 in the base
Three cases that test your understanding #
The examples above have, at most, one isolated “carry” per column. Three situations depart from this pattern and are worth studying, because they are where the calculation tends to go off the rails.
Two maximum digits in the same column. In base 8, the largest digit is 7. Adding \(77_8 + 77_8\) puts the algorithm in the worst possible case in every column:
$$ \begin{array}{c c c c} \scriptscriptstyle 1 & \scriptscriptstyle 1 & & {} \\ {} & 7 & 7 & {} \\ {} + & 7 & 7 & {} \\ \hline & (15) & (14) & {} \\ {} - & 8 & 8 & {\text{ subtracting the base}} \\ \hline 1 & 7 & 6 & {} \end{array} $$Result: \(77_8 + 77_8 = 176_8\). Check it in decimal: \(77_8 = 63\), and \(63 + 63 = 126 = 176_8\) (\(1 \times 64 + 7 \times 8 + 6\)).
Carry chain. In \(2\mathtt{FF}_{16} + 1\mathtt{FE}_{16}\), the “carry” does not stay stuck in one column: it is born in the units and crosses the entire calculation.
$$ \begin{array}{c c c c c} {} & \scriptscriptstyle 1 & \scriptscriptstyle 1 & & {} \\ {} & 2 & F & F & {} \\ {} + & 1 & F & E & {} \\ \hline {} & 4 & (31) & (29) & {} \\ {} - & {} & 16 & 16 & {\text{ subtracting the base}} \\ \hline {} & 4 & F & D & {} \end{array} $$Result: \(2\mathtt{FF}_{16} + 1\mathtt{FE}_{16} = 4\mathtt{FD}_{16}\). In decimal: \(767 + 510 = 1277\), and \(4 \times 256 + 15 \times 16 + 13 = 1277\).
In binary this chain effect is even clearer, and it is worth seeing it drawn. The figure below follows an extreme case of an 8-bit sum, \(01111111_2 + 00000001_2\), that is, \(127 + 1\). The “carry” is born in the rightmost column and has to travel, one by one, through all seven following columns before the result is ready. The three panels at the bottom highlight the three moments of this journey:

This is where the name ripple carry comes from: each column can only finish after the previous one has handed over its carry. On paper this is just work; in hardware, it is time, and that is why modern processors use smarter adders, which compute the carry in parallel instead of waiting for it.
Adding three numbers at once. Here lies the most beautiful trap of the algorithm. Everyone learns “carry one”, but the carry is only worth one because we are adding two numbers. With three addends, a column can generate a carry of 2.
$$ \begin{array}{c c c c c c c c} {} & {} & \scriptscriptstyle 2 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & {} \\ {} & {} & {} & 1 & 0 & 1 & 0 & 0 \\ {} & {} & {} & 1 & 1 & 1 & 0 & 1 \\ {} + & {} & {} & 1 & 1 & 0 & 1 & 1 \\ \hline {} & 1 & 0 & 0 & 1 & 1 & 0 & 0 \end{array} $$Follow the column that overflows: in the fifth position we have \(1 + 1 + 1 = 3\), plus the carry 1 that arrived from the previous column, which makes \(4\). Since \(4 = 100_2\), \(0\) stays in the column and 2 go to the next one. Result: \(10100_2 + 11101_2 + 11011_2 = 1001100_2\), or \(20 + 29 + 27 = 76\).
The general rule is not “write the remainder and carry 1”, but rather: write the remainder of the division by the base and carry the quotient. With two addends the quotient never exceeds 1, and the simplified rule works. With three or more, it can be 2 or more, and that is exactly where a calculation done on autopilot goes wrong.
Subtraction in different bases #
Subtraction in other bases follows the same principle we saw in the decimal base (carry sub/compensation): if the top digit is smaller than the bottom one, we add the base to the top one and compensate by adding 1 to the neighbor below.
Again, the formal algorithm becomes more evident: if the value of the subtraction is negative, we add the value of the base to the top digit to make the subtraction possible and add 1 to the next column of the bottom number (the subtrahend) to keep the equation balanced.
Example: Subtraction in base 8
In base 8, the digits go from 0 to 7. Whenever the subtraction results in a negative number, we borrow 1 from the next column.
$$ \begin{array}{c c c} {} & 5 & 2^{\scriptscriptstyle+8} \\ {} - & 1^{\scriptscriptstyle+1} & 7 \\ \hline {} & 3 & 3 \end{array} $$Step by step:
- Units: \(2 - 7\) is not possible. We add the base (8) to the top digit (the \(2\) becomes \(10\)) and compensate with \(+1\) in the next column of the subtrahend (the \(1\) becomes \(2\)). Now \(10 - 7 = 3\).
- Next column: \(5 - 2 = 3\).
Result: \(52_8 - 17_8 = 33_8\).
Check: \(52_8 = 42\) and \(17_8 = 15\); \(42 - 15 = 27\), which in base 8 is \(3 \times 8 + 3 = 33_8\).
Example: Subtraction in base 16
In base 16, the digits go from 0 to F. Whenever the subtraction results in a negative number, we borrow 1 from the next column.
$$ \begin{array}{c c c} {} & 5 & 2^{\scriptscriptstyle+16} \\ {} - & 1^{\scriptscriptstyle+1} & 7 \\ \hline {} & 3 & B \end{array} $$Step by step:
- Units: \(2 - 7\) is not possible. We add the base (16) to the top digit (the \(2\) becomes \(18\)) and compensate with \(+1\) in the next column of the subtrahend (the \(1\) becomes \(2\)). Now \(18 - 7 = 11\), which in hexadecimal is the digit \(\mathtt{B}\).
- Next column: \(5 - 2 = 3\).
Result: \(52_{16} - 17_{16} = 3\mathtt{B}_{16}\).
Check: \(52_{16} = 82\) and \(17_{16} = 23\); \(82 - 23 = 59\), which in base 16 is \(3 \times 16 + 11 = 3\mathtt{B}_{16}\).
Notice that the calculation is the same as in the previous two examples: only the borrowed value changes, which is always the base.
In base 2, we can use the knowledge already acquired from the “table” to make subtraction easier.
Example: Subtraction in base 2
Step by step, from right to left:
- \(0 - 1\) does not work. We add the base (2) to the top, which becomes \(10_2\), and compensate with \(+1\) in the next bottom column. \(2 - 1 = 1\).
- Top \(1\), bottom \(0 + 1 = 1\). \(1 - 1 = 0\), no compensation.
- Top \(0\), bottom \(1\). We add the base: \(2 - 1 = 1\), and compensate.
- Top \(0\), bottom \(1 + 1 = 2\). We add the base: \(2 - 2 = 0\), and compensate.
- Top \(1\), bottom \(0 + 1 = 1\). \(1 - 1 = 0\).
- Top \(1\), bottom \(1\). \(1 - 1 = 0\).
Result: \(110010_2 - 101101_2 = 101_2\).
Check: \(110010_2 = 50\) and \(101101_2 = 45\); \(50 - 45 = 5\), which in binary is \(101_2\).
Formal Algorithms and Implementation #
For a computer scientist, understanding arithmetic includes knowing how to do the calculation on paper and understanding the algorithm that can be programmed. Below, we present the generalized logic for any Base \(B\), first in pseudocode and then in Python.
Addition Algorithm (Generalized) #
The central logic depends on two integer operations: the remainder of the
division (mod in the pseudocode, % in Python) determines the digit that
stays, and the integer division (\(\lfloor \cdot \rfloor\) in the pseudocode,
// in Python) determines the “carry”.
To begin with, let us look at pseudocode, which is a way of writing algorithms independently of any programming language. It may make things easier to understand before we see the Python code.
Pseudocode:
ADD(A, B, n, b)
% A[1..n] and B[1..n]: the digits of the two addends in base b, right-aligned
% and padded with leading zeros; A[n] and B[n] are the units
carry := 0
for i = n downto 1
sum := A[i] + B[i] + carry
R[i] := sum mod b % the digit that stays
carry := ⌊sum / b⌋ % the carry for the next column
if carry > 0
R[0] := carry % the last carry becomes an extra digit
return R[0..n]
else
return R[1..n]
Python implementation:
For the Python code, we will use an auxiliary string to map digits greater than
9:
DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def add_bases(n1: str, n2: str, base: int) -> str:
# Reverse the strings to make iterating from right to left easier
n1, n2 = n1[::-1].upper(), n2[::-1].upper()
# Equalize the string lengths with zeros on the right (which are on the left in the real number)
max_len = max(len(n1), len(n2))
n1 = n1.ljust(max_len, '0')
n2 = n2.ljust(max_len, '0')
carry = 0
result = []
for i in range(max_len):
# Convert character to numeric value (e.g., 'A' -> 10)
v1 = DIGITS.index(n1[i])
v2 = DIGITS.index(n2[i])
partial_sum = v1 + v2 + carry
# Base logic
kept_digit = partial_sum % base
carry = partial_sum // base
result.append(DIGITS[kept_digit])
if carry > 0:
result.append(DIGITS[carry])
# Reverse back to return the correct string
return "".join(result[::-1])
# --- Execution Examples (Based on the article's examples) ---
# Base 10: 67 + 84
print(f"Base 10: {add_bases('67', '84', 10)}")
# Result: 151
# Base 9: 67 + 84
print(f"Base 9: {add_bases('67', '84', 9)}")
# Result: 162 (Note that 7+4=11, which is 12 in base 9)
# Base 16: F1A + E09
print(f"Base 16: {add_bases('F1A', 'E09', 16)}")
# Result: 1D23
# Base 2: 10111 + 11011
print(f"Base 2: {add_bases('10111', '11011', 2)}")
# Result: 110010Subtraction Algorithm (Carry Sub Method) #
In this method, when the top digit is smaller, we add the base b to it and
mark a compensation (+1) to be added to the bottom digit in the next column.
Pseudocode:
SUBTRACT(A, B, n, b)
% A[1..n]: minuend; B[1..n]: subtrahend, with A ≥ B; both in base b,
% right-aligned and padded with leading zeros
compensation := 0
for i = n downto 1
bottom := B[i] + compensation
top := A[i]
if top < bottom
top := top + b % "borrows" the base
compensation := 1
else
compensation := 0
R[i] := top - bottom
return R[1..n] % leading zeros do not change the value
Python implementation:
DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def subtract_bases(n1: str, n2: str, base: int) -> str:
# This algorithm can only produce non-negative results. Without the check
# below, subtract_bases('38', '75', 10) would silently return '63'.
# Comparing numerals of the same base does not require converting them: just
# look at the length (without leading zeros) and, in case of a tie, compare
# digit by digit (the symbols in DIGITS are already in increasing order).
a = n1.upper().lstrip('0') or '0'
b = n2.upper().lstrip('0') or '0'
if (len(a), a) < (len(b), b):
raise ValueError(f"{n1} < {n2} in base {base}: result would be negative")
n1, n2 = n1[::-1].upper(), n2[::-1].upper()
max_len = max(len(n1), len(n2))
n1 = n1.ljust(max_len, '0')
n2 = n2.ljust(max_len, '0')
compensation = 0
result = []
for i in range(max_len):
v_top = DIGITS.index(n1[i])
v_bottom = DIGITS.index(n2[i]) + compensation
if v_top < v_bottom:
# Apply the Carry Sub logic (add the base on top)
v_top += base
compensation = 1
else:
compensation = 0
difference = v_top - v_bottom
result.append(DIGITS[difference])
# Remove resulting leading zeros (e.g., 037 -> 37)
return "".join(result[::-1]).lstrip('0') or "0"
# --- Execution Examples ---
# Base 10: 75 - 38
print(f"Base 10: {subtract_bases('75', '38', 10)}")
# Result: 37
# Base 8: 52 - 17 (Article example: we borrow 8)
print(f"Base 8: {subtract_bases('52', '17', 8)}")
# Result: 33 (Since 2 became 10 (2+8), 10-7=3. The 1 became 2, 5-2=3)
# Base 16: 52 - 17
print(f"Base 16: {subtract_bases('52', '17', 16)}")
# Result: 3B (Since 2 became 18 (2+16), 18-7=11(B). The 1 became 2, 5-2=3)
# Base 2: 1101010 - 101101 (Adapted example)
print(f"Base 2: {subtract_bases('1101010', '101101', 2)}")
# Result: 111101Summary Table #
| Situation | Rule | Example from the article |
|---|---|---|
| Addition, column by column | write \(s \bmod b\) and carry \(s \div b\) | \(67_9 + 84_9 = 162_9\) |
| Carry with two addends | the carry never exceeds 1 | \(1_2 + 1_2 = 10_2\) |
| Carry with three addends | the carry can be 2 or more | \(10100_2 + 11101_2 + 11011_2 = 1001100_2\) |
| Worst case of addition | two maximum digits in the same column | \(77_8 + 77_8 = 176_8\) |
| Carry chain | the carry crosses several columns | \(2\mathtt{FF}_{16} + 1\mathtt{FE}_{16} = 4\mathtt{FD}_{16}\) |
| Subtraction (carry sub) | if the top is smaller, add the base on top and \(+1\) in the next bottom column | \(52_8 - 17_8 = 33_8\) |
| How much is borrowed | always the value of the base | 8 in base 8, 16 in base 16, 2 in base 2 |
| Check | convert to decimal and redo the calculation | \(42 - 15 = 27 = 33_8\) |
| Addition in code | kept_digit = partial_sum % base, carry = partial_sum // base |
add_bases('F1A','E09',16) |
| Subtraction in code | if v_top < v_bottom: v_top += base |
subtract_bases('52','17',16) |
| Limit of the algorithm | only non-negative results | subtract_bases('38','75',10) raises ValueError |
Conclusion and Next Steps #
As we have seen, positional arithmetic is consistent: it does not matter whether we have 2 symbols (binary) or 16 (hexadecimal), the algorithm is the same. The only thing that changes is the value that triggers the “carry” or the “borrow”.
Mastering these operations on paper is excellent for understanding the logic, but it raises an important question: does computer hardware really perform subtractions by borrowing neighboring bits?
In practice, no. A full subtractor has a cost similar to that of a full adder, because both are made essentially of the same logic gates; the problem is the waste.
It is worth taking a close look at the circuit we are talking about. The full adder is the piece that carries out, in silicon, exactly one column of the calculation we did on paper: it receives two bits and the carry that arrived from the previous column, and returns the sum bit and the carry for the next column. It is five logic gates, that is all:

Wiring eight of these pieces in a row, with the carry of one feeding into the next, gives exactly the 8-bit sum we drew above, including the chain wait. And a full subtractor is of similar size: the same gates plus two inverters, wired differently. That is where the waste lies.
Keeping two separate circuits, one for adding and one for subtracting, uses up processor area doing almost identical work twice and, worse, forces the design to decide which of the two to use for each operation.
The way out in modern computing is elegant: turn every subtraction into an addition, so that a single adder handles both operations. This is the concept of Two’s Complement and of signed numbers, and it is exactly this representation of negative numbers that we will talk about in the next article.
See you then!