How PyMongo Translates Python Dictionaries to BSON
When working with MongoDB in Python, PyMongo automatically converts
native Python dictionary objects into BSON (Binary JSON) byte streams
required by the database engine. This article provides a technical
overview of how PyMongo performs this serialization process, covering
the underlying bson module, data type mapping, recursive
document traversal, and the byte-level construction of BSON
payloads.
The Underlying
Serialization Engine: bson
PyMongo relies on its integrated bson library to handle
conversion. For performance, this library is implemented as a C
extension (_cbson), falling back to a pure Python
implementation only when C extensions cannot be compiled.
The C extension directly interfaces with Python’s C API, allowing it to inspect native Python objects at the memory level and write their binary representations directly into contiguous memory buffers without unnecessary intermediate allocations.
Step-by-Step Translation Process
Serialization occurs whenever an operation writes or queries data
(such as insert_one(), update_many(), or
find()). The process follows a structured sequence:
1. Allocating the Buffer and Header
A BSON document begins with a 4-byte signed integer representing the total byte length of the document. When PyMongo starts encoding a dictionary:
- It reserves the first 4 bytes in the memory buffer.
- It begins appending key-value pairs sequentially.
- Once serialization of the dictionary completes, PyMongo computes the final byte length, returns to the beginning of the buffer, and writes the total size into the reserved 4 bytes.
2. Traversing Key-Value Pairs
PyMongo iterates through the keys and values of the Python dictionary:
- Key Encoding: Every dictionary key must be a string. PyMongo encodes the key as a null-terminated UTF-8 byte sequence (a BSON "cstring").
- Type Inspection: PyMongo determines the Python type
of the associated value and writes a single-byte type tag identifying
the BSON type (for example,
\x02for a UTF-8 string,\x10for a 32-bit integer). - Value Encoding: The value is serialized into its corresponding binary format according to the official BSON specification.
3. Handling Nested Structures
When PyMongo encounters nested dictionaries or lists, it processes them recursively:
- Embedded Documents (Type
\x03): If a value is another dictionary, PyMongo recursively starts a new BSON frame within the buffer, writing its 4-byte size header, nested key-value pairs, and closing byte. - Arrays (Type
\x04): If a value is a list or tuple, PyMongo encodes it as an embedded BSON document where the keys are sequential string indices ("0","1","2", etc.).
4. Appending the Null Terminator
After all key-value pairs in a dictionary are appended, PyMongo
writes a single null byte (\x00) to signal the end of the
document structure.
Python to BSON Type Mapping
PyMongo maps standard Python data types to specific BSON binary representations:
| Python Type | BSON Type Code | Binary Structure |
|---|---|---|
str |
\x02 (String) |
4-byte length + UTF-8 payload + null byte |
int (within 32 bits) |
\x10 (Int32) |
4 bytes, signed little-endian |
int (within 64 bits) |
\x12 (Int64) |
8 bytes, signed little-endian |
float |
\x01 (Double) |
8 bytes, IEEE 754 double precision |
bool |
\x08 (Boolean) |
1 byte (\x00 for False,
\x01 for True) |
None |
\x0A (Null) |
No payload bytes (type tag only) |
datetime.datetime |
\x09 (UTC DateTime) |
8 bytes, signed int64 milliseconds since Unix epoch |
bson.objectid.ObjectId |
\x07 (ObjectId) |
12 raw bytes (timestamp, random value, counter) |
bytes |
\x05 (Binary) |
4-byte length + 1-byte subtype + raw byte payload |
Performance and Codec Options
Because dictionary key order was historically arbitrary in older Python versions, PyMongo natively handles mapping without requiring explicit ordering. In modern Python (3.7+), insertion order is preserved, and PyMongo writes keys into BSON in the order they appear in the dictionary.
To support non-standard Python types (such as
decimal.Decimal or custom classes), PyMongo provides a
TypeRegistry and CodecOptions. These allow
users to define fallback encoders that convert unsupported Python
objects into types that the core C encoder understands prior to
serialization.