Skip to main content

sniffbom

Detect, name and strip the Unicode byte-order mark on a byte string: every signature in use (UTF-8, UTF-16 and UTF-32 in both orders, UTF-7, UTF-1, UTF-EBCDIC, SCSU, BOCU-1 and GB18030), streams and partial reads, the WHATWG decode rule and XML entity autodetection. Zero dependencies, fully typed; every function takes bytes, bytearray or memoryview.

Install

pip install sniffbom

Usage

Every example below runs in the test suite, and each comment is exactly what it prints.

from sniffbom import decode, detect, strip

raw = b"\xef\xbb\xbfhello"
info = detect(raw)
print(info.encoding, info.length, info.codec)
print(strip(raw))
print(decode(raw))
print(detect(b"plain text"))
# utf-8 3 utf-8
# b'hello'
# hello
# None

decode reads the mark through its codec and consumes it; without a mark it uses default (utf-8 unless you pass another). Only one mark is removed, a second one stays as U+FEFF. errors goes to the codec.

from sniffbom import UnsupportedBomError, decode, detect

print(decode(b"\x84\x31\x95\x33caf\xa8\xa6"))
print(decode(b"+/v8A6Q-"))
print(decode(b"\xef\xbb\xbfa\xff", errors="replace") == "a\ufffd")
try:
    decode(b"\x0e\xfe\xffhi")
except UnsupportedBomError as error:
    print(error.encoding, isinstance(error, LookupError))
# café
# é
# True
# scsu True

UTF-1, UTF-EBCDIC, SCSU and BOCU-1 are recognised and named, but Python has no codec for them, so decode raises UnsupportedBomError and BomInfo.codec is None.

Which rule reads FF FE 00 00?

The Unicode table reads it as a UTF-32LE mark; the WHATWG Encoding Standard never sniffs UTF-32 and reads UTF-16LE followed by NUL. detect follows the Unicode table and among picks the rule. among also says "only the mark of the encoding I already know", which is what a TextDecoder does.

from sniffbom import bom_sniff, decode, detect, whatwg_decode

data = b"\xff\xfe\x00\x00"
print(detect(data).encoding)
print(detect(data, among=("utf-8", "utf-16-le", "utf-16-be")).encoding)
print(bom_sniff(data), repr(whatwg_decode(data)))
print(repr(decode(b"\xef\xbb\xbfabc", among=())))
# utf-32-le
# utf-16-le
# UTF-16LE '\x00'
# '\ufeffabc'

whatwg_decode(data, fallback) is the "decode" algorithm: the mark wins over the fallback encoding and malformed input becomes U+FFFD instead of raising.

Streams and partial reads

import io

from sniffbom import needs_more, text_stream

print(needs_more(b"\xff\xfe"), needs_more(b"\xff\xfeh"))
stream = text_stream(io.BytesIO(b"\xff\xfeh\x00i\x00"))
print(stream.read())
# True False
# hi

needs_more says whether more bytes could change the answer (FF FE may still become a UTF-32LE mark). text_stream reads at most five bytes to decide, never seeks, and returns a text stream with the mark consumed, so it works on pipes and on sockets through sock.makefile("rb"). A non-blocking stream that has no data before the mark is decided raises BlockingIOError rather than guessing.

XML entities

from sniffbom import xml_autodetect

print(xml_autodetect(b"\xfe\xff\x00<\x00?"))
print(xml_autodetect(b"<?xml version='1.0'?>").family)
# XmlGuess(family='utf-16-be', bom_length=2, codec='utf-16-be')
# ascii

xml_autodetect follows XML 1.0 Appendix F.1: it names the family of the first four bytes, with or without a mark, including the unusual UCS-4 orders and EBCDIC. codec is set when the family alone decides the encoding and is None when the encoding declaration must be read.

Command line

sniffbom FILE...              # name the mark of each file
sniffbom --check FILE...      # exit 1 if any file has a mark
sniffbom --strip FILE...      # remove the mark in place; - reads stdin, writes stdout
sniffbom --json FILE...       # one JSON array
sniffbom --among utf-8 FILE   # only look for some marks

Exit status: 0 when done, 1 when --check finds a mark, 2 on a usage or read error. --strip writes the new content next to the file and swaps it in (following a symlink to its target, keeping the mode); a malformed UTF-7 run it cannot shorten exactly is left as it is. When stdin is stripped to stdout, the report goes to stderr.

API

Name What it does
detect(data, *, among=None) The leading mark as a BomInfo, or None
strip(data, *, among=None) data without one leading mark, exact for UTF-7 too
decode(data, default="utf-8", *, errors="strict", among=None) Text with the mark consumed
needs_more(data, *, among=None) Whether more bytes could change detect
text_stream(binary, default="utf-8", *, errors, among, newline) A text stream without the mark
bom_sniff(data) The WHATWG "BOM sniff": "UTF-8", "UTF-16BE", "UTF-16LE" or None
whatwg_decode(data, fallback="utf-8") The WHATWG "decode" algorithm
xml_autodetect(data) An XmlGuess after XML 1.0 Appendix F.1
BomInfo encoding, length and the codec property
XmlGuess family, bom_length, codec
XmlFamily, WhatwgEncoding The literal types of those names
ENCODINGS Every name detect reports, in the order the marks are tried
UnsupportedBomError A LookupError for a mark Python cannot decode

A UTF-7 mark is +/v and a fourth byte (8, 9, + or /) that also carries two bits of the next character, so slicing data[info.length:] is only exact for +/v8-; strip re-encodes the rest of the run and is always exact. Text that happens to start with +/v8 is read as UTF-7, as with any signature.

Why

The standard library exposes the BOM byte sequences as codecs.BOM_* constants but offers no single call that detects one, names the encoding and hands back the content without it. utf-8-sig only covers UTF-8, and the usual hand-rolled sniffers get the ordering wrong: a UTF-32-LE mark (FF FE 00 00) begins with the UTF-16-LE mark (FF FE), so a naive check reports UTF-16. Larger options pull in dependencies. This is the small, typed primitive that does those steps correctly and says which rule it uses.

Sources

The tables are checked against the Unicode BOM FAQ, UTS #6 (SCSU), UTN #6 (BOCU-1), UTR #16 (UTF-EBCDIC), RFC 2152 (UTF-7), the WHATWG Encoding Standard, XML 1.0 Appendix F and web-platform-tests. python scripts/fetch_upstream.py fetches them into tests/upstream/ (they are not part of the repository) and their tests run once they are there.

License

MIT. See LICENSE.

Metadata

Release files for sniffbom 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sniffbom 0.2.0
File Size Uploaded Provenance
sniffbom-0.2.0.tar.gz 27.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sniffbom 0.2.0
File Interpreter ABI Platform
sniffbom-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.7 kB

Release files / sniffbom-0.2.0.tar.gz

Download URL sniffbom-0.2.0.tar.gz
Size 27.7 kB
Tags Source
SHA-256 checksum
How to use checksums
0fd83a4e023cc508557ef83a92f8a603fa737629b7f1af09e1c93d25fc5dfd44
BLAKE2b-256 checksum
How to use checksums
8a51ccc79ee27320db62c77a5279876da027b09cc70543aa967b25de7f0fdff2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / sniffbom-0.2.0-py3-none-any.whl

Download URL sniffbom-0.2.0-py3-none-any.whl
Size 15.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c157053afe47e0afbad567f3c50c7885c606aa8c73e8be4996c44c5ccf4c41f5
BLAKE2b-256 checksum
How to use checksums
7a01b3c59a65873851c770eebefea4ec6ce911381135906ccf82797fd6564a5f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Security for sniffbom 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page