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(), 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:
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¶
- Find a place in your own code that prints a flag, a mask or a checksum with
bin(),hex()orformat(..., '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. - Run
os.stat()on a file of yours and print itsst_modewithoct(), withbin(), and withformat(st_mode & 0o777, '09b'). Which of the three reads asrwxr-xr-x, and what was in the bits the mask threw away? - 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 withsigned=False, print both withbin(), 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.
bin(-1)bin(-1 & 0xFF)bin(True)format(-5, '08b')int('-0b101', 0)int('0b101', 10)(128).to_bytes(1, 'big', signed=True)(-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¶
- The format mini-language — the
#,0, width andb/o/xslots thatbin(),oct()andhex()are shorthand for - Making a
bytesobject —to_bytesand the other calls that are told a width repris notstr— why the prompt shows every result on this page in quotes- A byte is eight bits ↗ — the bits themselves, and why a Python
intis not a byte int.bit_length()↗ and bitwise operations on integers ↗ — the definition in terms ofbin(), and the infinite sign bits