Bit() builds its bits with int(value, 2) at pgvector/bit.py:36, and takes _length from the raw string before that. Python's int accepts underscores, a sign, surrounding whitespace and a 0b prefix, so four strings Postgres rejects come back as a Bit holding different bits and a different length.
$ PYTHONPATH=/var/tmp/p11L1/pgv python pgv_bit_probe.py # master fd74f78, Python 3.12.3
pgvector: /var/tmp/p11L1/pgv/pgvector/bit.py
Bit('1010' ) -> to_text()='1010' len=4
Bit('1010_1010' ) -> to_text()='010101010' len=9
Bit('+1010' ) -> to_text()='01010' len=5
Bit(' 1010' ) -> to_text()='01010' len=5
Bit('1010\n' ) -> ValueError: expected bit string
Bit('-1010' ) -> OverflowError: can't convert negative int to unsigned
Bit('0b1010' ) -> to_text()='001010' len=6
Postgres rejects all four literals, but the client never sends them. What it sends in their place is a valid bit string, and the column takes it. pgvector 0.8.7 on PostgreSQL 17.11, docker.io/pgvector/pgvector:pg17:
postgres=# SELECT '1010_1010'::bit(9);
ERROR: "_" is not a valid binary digit
postgres=# SELECT '0b1010'::bit(6);
ERROR: "b" is not a valid binary digit
postgres=# SELECT '01010'::bit(5); -- what Bit('+1010') sends instead
01010
'-1010' raises OverflowError where the except ValueError at bit.py:37-38 was meant to turn it into expected bit string.
A re.fullmatch('[01]*', value) before the padding rejects all five and times the same as the int() it guards. The obvious set(value) <= {'0', '1'} is 4.6x, which is worth avoiding on a constructor you already tuned in 2ae28ba:
$ python3 -m timeit -s "import re; s='01'*32000" "<expr>" # Python 3.12.3, best of 5
int(s, 2) 98.4 usec
re.fullmatch('[01]*', s) 92.9 usec
set(s) <= {'0','1'} 451 usec
@ankane would you take a pull request for that, with cases in tests/test_bit.py next to test_str, or do you read the permissive parsing here as deliberate?