The Built-In bin()
, oct()
and hex()
Functions
by Christoph Schiessl on Python
Today, I want to explore three built-ins at once: the bin()
, the oct()
, and the hex()
functions. It makes sense to talk about all three at once because they are very similar, and if you learn about one, then your knowledge easily transfers to the other two.
All three take a single parameter, which must be an int
object or interpretable as an int
if it is a different type — we get to details of the conversion logic later.
Python 3.12.3 (main, Apr 21 2024, 14:06:33) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> bin(255), bin(-255)
('0b11111111', '-0b11111111')
>>> oct(255), oct(-255)
('0o377', '-0o377')
>>> hex(255), hex(-255)
('0xff', '-0xff')
As you can see, bin()
returns a string that is a binary number (i.e., a number that uses two distinct digits), oct()
returns a string that is an octal number (i.e., a number that uses eight distinct digits), and finally, hex()
returns a string that is a hexadecimal number (i.e., a number that uses sixteen distinct digits).
Note that we only have ten digits — 0 through 9 — available, but we need sixteen for hexadecimal numbers. Hence, these hexadecimal numbers use the letters a through f to denote the remaining six digits.
Furthermore, all returned strings are prefixed with 0b
, 0o
, or 0x
, making them valid Python expressions. For negative integers, the minus sign goes before the prefix.
Python 3.12.3 (main, Apr 21 2024, 14:06:33) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0b11111111 == 255
True
>>> -0b11111111 == -255
True
>>> 0o377 == 255
True
>>> -0o377 == -255
True
>>> 0xff == 255
True
>>> -0xff == -255
True
How to Read Binary, Octal, and Hexadecimal Numbers
Decimal numbers use ten distinct digits; therefore, they are also called base 10 numbers. Now, look at a decimal number like 255
and read it from the right to the left — which is, read from the least significant to the most significant digit. Then the right-most digit represents 5
, the digit in the middle represents 50
, and the left-most digit represents 200
. To get the final number, you calculate the sum: 5 + 50 + 200 == 255
.
A more generic way to look at this is the following: The right-most digit represents 5 * 10 ** 0 == 5 * 1 == 5
, the digit in the middle represents 5 * 10 ** 1 == 5 * 10 == 50
, and the left-most digit represents 2 * 10 ** 2 == 2 * 100 == 200
. So, the exponent starts with zero for the least significant digit and is then incremented each time you parse the next, more significant digit.
I'm using a 10 in this calculation because, so far, we have been talking about a base 10 number. However, the same calculation also applies to numbers with a different base. Generally speaking, for a base n number, you would replace the 10 with n in the calculation above.
We have already established that binary numbers use two distinct digits, so they are base 2. Similarly, octal numbers are base 8, and hexadecimal numbers are base 16 because they use eight and sixteen distinct digits, respectively.
Take, for example, the octal number 377
. If you have to parse this by hand, you would go from left to right and calculate as follows: 7 * 8 ** 0 == 7 * 1 == 7
, 7 * 8 ** 1 == 7 * 8 == 56
, and 3 * 8 ** 2 == 3 * 64 == 192
. This gives a final number of 7 + 56 + 192 == 255
.
Again, this calculation is not specific to any number system. If you wanted to, you could invent a base 42 number ;)
Conversion of objects other than int
I mentioned before that the parameter of bin()
, oct()
, and hex()
must be an int
or interpretable as an int
. Well, most objects are not losslessly convertible to an int
.
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> bin(object())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'object' object cannot be interpreted as an integer
>>> oct(1.6) # not using normal int conversion!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer
Ordinary integer conversion, if available, is lossless because the digits after the floating point are truncated (i.e., int(1.6) == 1
). Instead, a special procedure normally used for indexing is utilized. As it turns out, the operator
module defines an index()
operator that delegates to the object's magic __index__()
method behind the scenes. This magic method, if implemented, is supposed to provide the ability to do lossless conversion. We can easily demonstrate this:
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import operator
>>> operator.index(object()) # doesn't work with arbitrary objects
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'object' object cannot be interpreted as an integer
>>> operator.index(1.6) # doesn't work with floats
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer
>>> '__index__' in dir(255)
True
>>> operator.index(255) # works with ints that implement __index__()
255
>>> class Foo:
... def __index__(self) -> int:
... return 255
...
>>> operator.index(Foo()) # works with custom objects that implement __index__()
255
Last but not least, we can also demonstrate all of this together:
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Foo:
... def __index__(self):
... print("Foo.__index__() has been called!")
... return 255
...
>>> bin(Foo())
Foo.__index__() has been called!
'0b11111111'
>>> oct(Foo())
Foo.__index__() has been called!
'0o377'
>>> hex(Foo())
Foo.__index__() has been called!
'0xff'
That's everything for today. Thank you for reading, and see you soon!