Skip to content

bin() is not the bits

Level: 201 · for Python programmers

One line: bin(-9) is '-0b1001' — a minus sign and a magnitude, not a bit pattern — because a Python int has no width, and the bits of a negative number do not exist until you choose one with a mask or a byte count.

bin(9)             # '0b1001'
bin(-9)            # '-0b1001'
bin(-9 & 0xFF)     # '0b11110111'

bin(), oct() and hex() are one function three times — format(n, '#b'), '#o' and '#x': a prefix, then the digits of n in that base. On a positive number those digits are its bits, which is why bin() reads like a look at memory. On a negative number they are not. A fixed-width integer stores -9 as two's complement, 11110111 in eight bits, but a Python int has no fixed width: its bitwise operators behave as though it were two's complement with "an infinite number of sign bits", in the docs' words, so -9 is ...11110111 with the ones running left forever. No finite string spells that, so bin() prints the finite thing it has — a sign, and the digits of |n|. The third line is the question you meant to ask: choose a width, and mask to it.

Verified output of bin_is_not_the_bits_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. THREE FUNCTIONS, ONE FORMAT SPEC
     bin(n) is format(n, '#b'); oct(n) is '#o'; hex(n) is '#x'.

         n   bin(n)         oct(n)    hex(n)
     ----------------------------------------
         0   '0b0'          '0o0'     '0x0'
         9   '0b1001'       '0o11'    '0x9'
       230   '0b11100110'   '0o346'   '0xe6'
        -9   '-0b1001'      '-0o11'   '-0x9'

     equal to format(n, '#b' / '#o' / '#x') for every n in -1000..1000:  True
     bin(True)   '0b1'   a bool is an int, so it has __index__
     bin(1.0)    TypeError   a float has no __index__

2. A NEGATIVE NUMBER PRINTS A SIGN, NOT ITS BITS
     bin(9)      '0b1001'
     bin(-9)     '-0b1001'   <- the same four digits, and a minus sign

     A Python int has no width. In two's complement -9 is ...11110111
     with the ones running left forever -- that is the model &, | and
     >> use -- so there is no finite pattern for bin() to print. It
     prints the one finite thing it has: a sign, and the digits of |n|.
     -9 >> 1000  = -1    <- a thousand places right, and still all ones

3. CHOOSE A WIDTH AND THE BITS APPEAR
     format(-9, '08b')          '-0001001'   <- padded, still sign and magnitude
     format(-9 & 0xFF, '08b')   '11110111'   <- masked to eight bits first

     width    -9 & mask   the bits
         8          247   11110111
        16        65527   1111111111110111
        32   4294967287   11111111111111111111111111110111

     raw = (-9).to_bytes(1, 'big', signed=True)   b'\xf7'   = 11110111
     Every width shows a different number of ones, and none of them is
     wrong. The width is a decision -- a mask or a byte count -- and
     the int never made it.

4. READING IT BACK
     int('0b1001', 0)                         9           base 0 reads the prefix
     int('0b1001', 2)                         9           and so does base 2
     int('-0b1001', 0)                        -9          the sign comes back too
     int('0b1001', 10)                        ValueError  base 10 has no prefix to read
     int('11110111', 2)                       247         the eight bits of -9, read as 247
     int.from_bytes(raw, 'big', signed=True)  -9          told the width AND the sign

     bin() and int(s, 0) are inverses, sign included. The bits of -9
     are not: read as base 2 they are 247, because a width and a
     signedness went into them that the string does not carry.

5. TWO METHODS THAT READ THE MAGNITUDE
        n   bin(n)     bit_length()  bit_count()
        9   '0b1001'              4            2
       -9   '-0b1001'             4            2
        1   '0b1'                 1            1
        0   '0b0'                 0            0

     -9 and 9 answer both the same way: both count |n|, the digits
     bin() prints. The ones in a chosen width are another question:
     (-9 & 0xFF).bit_count() = 7

     The docs define bit_length() as len(bin(n).lstrip('-0b')). lstrip
     removes a SET of characters, not the prefix -- it works here only
     because the digits left over start with 1, or are all zeros:
     bin(0).lstrip('-0b') = '', and (0).bit_length() = 0

What the run shows

The three functions are format specs. Section 1 checks bin(n) == format(n, '#b'), and the same for oct and hex, over every n from -1000 to 1000. All three accept anything with __index__, so bin(True) works and bin(1.0) is a TypeError.

A width is a decision, and each width gives a different answer. Section 3 masks -9 to 8, 16 and 32 bits and gets three different runs of ones, none of them wrong; (-9).to_bytes(1, 'big', signed=True) makes the same decision with a byte count. The two lines above that table are the trap: format(-9, '08b') is '-0001001', eight characters wide and not eight bits. A width in a format spec pads the text — with the zeros after the sign, the way zfill does — and says nothing about the number.

The round trip keeps the sign and loses the width. int(s, 0) and int(s, 2) both read the 0b that bin() writes, sign included, so bin() and int(s, 0) are inverses. The eight bits of -9 are not: read as base 2 they are 247, because the width and the signedness that produced them are not in the string. Only int.from_bytes(raw, 'big', signed=True) is told both, and only it gives -9 back.

bit_length() and bit_count() count what bin() prints, the digits of |n|, so -9 and 9 get the same answers; the docs define both in terms of bin(). Their definition of bit_length() strips with lstrip('-0b'), which removes a set of characters, not a prefix. It gives the right count only because the digits after the prefix start with a 1 — or, for zero, are all stripped away, which is also the right count.

The Rust view

Rust prints the bits, because a Rust integer has a width. {:#b} on -9i32 is 32 digits of two's complement and on -9i8 it is eight — one value, two spellings, chosen by the type it was stored in:

Measured 2026-09-10 — rustc 1.98.0 and Python 3.14.7, put side by side; not verbatim program output
  Python                           Rust
  bin(9)            '0b1001'       format!("{:#b}", 9i32)                     0b1001
  bin(-9)           '-0b1001'      format!("{:#b}", -9i32)                    0b11111111111111111111111111110111
  bin(-9 & 0xFF)    '0b11110111'   format!("{:#b}", -9i8)                     0b11110111
  (-9).bit_count()  2              (-9i32).count_ones()                       31
  int('0b1001', 2)  9              i32::from_str_radix("0b1001", 2)           Err(ParseIntError { kind: InvalidDigit })
  int('-1001', 2)   -9             i32::from_str_radix("-1001", 2)            Ok(-9)
  bin(-9)           '-0b1001'      format!("-{:#b}", (-9i32).unsigned_abs())  -0b1001

Rust never prints a minus sign in binary or hex — the sibling library's Why hexadecimal ↗ files that as a trap. count_ones() counts the ones in the stored bits, 31 for -9i32, where bit_count() counts the ones of |n|. Going the other way, from_str_radix refuses the 0b prefix that int(s, 2) accepts, and reads a leading - just as Python does. To get Python's spelling in Rust, write the sign yourself, as the last row does.

If you are coming from ABAP

Sign and magnitude is not an exotic idea to you: it is how a packed number is stored. A TYPE p field holds the decimal digits of the magnitude and keeps the sign in its last half-byte, so -9 in a p field is the digits of 9 plus a sign nibble — the shape of bin(-9), in decimal. The integer types follow the other model: i and int8 have a width, four and eight bytes, so a negative i is stored in two's complement exactly as a Rust i32 is. What ABAP does not hand you is bin(). To look at an integer's bits, the usual route is to move it into a TYPE x LENGTH 4 field and read it with GET BIT, one position at a time — and at that point the width is the field's length, the decision this page says Python leaves to you. Check the i-to-x conversion rule on your own system before relying on the byte order it produces. (Not machine-checked — CI cannot run ABAP.)

Try it

  1. Find a place in your own code that prints a flag, a mask or a checksum with bin(), hex() or format(..., 'b'), and hand it a negative value. What comes out? Then write the version that masks to the width of the field the value came from — and decide what it should do with a value that does not fit.
  2. Run os.stat() on a file of yours and print its st_mode with oct(), with bin(), and with format(st_mode & 0o777, '09b'). Which of the three reads as rwxr-xr-x, and what was in the bits the mask threw away?
  3. Take a two-byte signed value from a binary file you work with — a WAV sample, a sensor reading, a field in a fixed-width record. Read it with int.from_bytes(..., signed=True) and with signed=False, print both with bin(), and say which one the file meant.

Practice

Eight one-liners, and the two that were given a width. Write down each result before running anything.

  1. bin(-1)
  2. bin(-1 & 0xFF)
  3. bin(True)
  4. format(-5, '08b')
  5. int('-0b101', 0)
  6. int('0b101', 10)
  7. (128).to_bytes(1, 'big', signed=True)
  8. (-5).bit_count()

Then: two of these lines were given a width. Which two, and what did the width change? And line 4 is eight characters wide — say in one sentence why that does not make it a byte.

Answers

Verified output of bin_is_not_the_bits_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.

     expression                               result         note
     --------------------------------------------------------------------------------------
     bin(-1)                                  '-0b1'         a sign, and a magnitude of one
     bin(-1 & 0xFF)                           '0b11111111'   masked to eight bits: now it is the bits
     bin(True)                                '0b1'          a bool is an int, so it has __index__
     format(-5, '08b')                        '-0000101'     the zeros go AFTER the sign
     int('-0b101', 0)                         -5             base 0 reads the sign and the prefix
     int('0b101', 10)                         ValueError     base 10 has no prefix to read
     (128).to_bytes(1, 'big', signed=True)    OverflowError  a signed byte stops at 127
     (-5).bit_count()                         2              the ones of |n|, and 5 is 101

     THE RULE
     Lines 2 and 7 are the only two that were given a width -- a mask
     and a byte count. One prints eight bits; the other refuses 128,
     because a signed byte stops at 127. Every other line writes or
     reads a minus sign and the digits of |n|, which is all an int
     without a width has to offer.

     THE ONE THAT LOOKS RIGHT
     format(-5, '08b') is eight characters wide, which is the shape of
     a byte, and it is not one: it is a minus sign and seven digits of
     5. A width in a format spec pads the TEXT; only a mask or
     to_bytes() chooses a width for the NUMBER.

See also