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!

Ready to Learn More Web Development?

Join my Mailing List to receive two articles per week.


I send two weekly emails on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


The Built-in chr() and ord() Functions

Discover Python's built-in functions chr() and ord() for handling Unicode characters and converting between integers and characters.

By Christoph Schiessl on Python

Function Definition with Simple Parameters

Learn about functions with simple parameters in Python, including how the called can decide to use positional or keyword notation.

By Christoph Schiessl on Python

The Built-in sum() Function

In this article, we explore Python's built-in sum() function, its parameters, and some extreme use cases it wasn't even designed for.

By Christoph Schiessl on Python

Christoph Schiessl

Christoph Schiessl

Independent Consultant + Full Stack Developer


If you hire me, you can rely on more than a decade of experience, which I have collected working on web applications for many clients across multiple industries. My involvement usually focuses on hands-on development work using various technologies like Python, JavaScript, PostgreSQL, or whichever technology we determine to be the best tool for the job. Furthermore, you can also depend on me in an advisory capacity to make educated technological choices for your backend and frontend teams. Lastly, I can help you transition to or improve your agile development processes.