PyMongo Basics
Explore the MongoDB driver for Python: PyMongo basics — MongoDB.
Focus: explore the mongodb driver for python: pymongo basics
You’ve mastered the MongoDB shell and maybe a few admin scripts, but now your application needs to talk to MongoDB directly — and that means choosing a driver. Without the right driver, you’re stuck hand-rolling HTTP requests to the MongoDB wire protocol, which is error-prone, insecure, and a total time sink. The official PyMongo driver solves this by giving you a clean, Pythonic API to connect, query, insert, and manage your MongoDB clusters — and this lesson walks you through the basics so you can go from zero to a working script in minutes.
The problem this lesson solves
MongoDB’s shell (mongosh) is great for ad-hoc queries, but your Python application can’t run db.collection.find() in a terminal at runtime. You need a programmatic way to:
- Connect to a local or remote MongoDB instance
- Insert, read, update, and delete documents
- Handle authentication, connection pooling, and errors gracefully
Without a driver, you’d have to implement the MongoDB wire protocol from scratch — a huge, low-level task that’s also a security risk. PyMongo is the official, community-supported driver for Python, and it abstracts all that complexity. It’s what tools like Flask-MongoEngine and Django’s MongoDB backend use under the hood. This lesson gives you the essentials so you can write real code, not just theory.
Core concept / mental model
Think of PyMongo as a remote control for your MongoDB server. You don’t manipulate MongoDB directly; you send commands through a client that translates your Python calls into the wire protocol and back.
Here’s the key architecture in your head:
MongoClient— the connection to the server. It manages a pool of connections, handles retries, and is your entry point.db = client.database_name— a proxy to a specific database. Doesn’t create it until you write data.collection = db.collection_name— a proxy to a collection. Same lazy behavior.collection.insert_one(document)— actually sends the operation and returns a result object.
A useful analogy: MongoClient is the power plug, the database is a room in your house, the collection is a drawer, and documents are the items you put inside. You don’t build the drawer until you put something in it — MongoDB creates collections and databases on first write.
Key definitions:
- Document: a JSON-like structure (BSON) with field:value pairs.
- _id: a unique primary key, auto-generated as an ObjectId if you don’t supply one.
- Cursor: an iterable returned by find() — lazy, fetches batches as you iterate.
How it works step by step
Here’s the logical flow of a typical PyMongo script — follow this and you’ll avoid most common pitfalls:
- Install PyMongo with
pip install pymongo(orpymongo[srv]for connection strings using themongodb+srv://scheme). - Create a
MongoClientwith your connection string (e.g.,mongodb://localhost:27017). The client is lazy — it doesn’t connect until an operation runs, so invalid URIs surface as errors only when you execute. - Access a database via
client['mydb']or attribute styleclient.mydb. - Access a collection similarly:
db['users']ordb.users. - Run a write operation —
insert_one()/insert_many()to add data. MongoDB creates the DB/collection on first insert. - Query with
find_one()(returns a dict orNone) orfind()(returns aCursor). - Close the client when done (or use a
withblock for context management).
Cause and effect: if you try to access a database that doesn’t exist, you get a proxy object — no error. But if you call find_one() on it, you get None. If you insert, the DB and collection are created implicitly. This lazy model is elegant but can confuse beginners who expect eager validation.
Hands-on walkthrough
Let’s put it together with a complete example. First, install the driver (best practice: use a virtual environment):
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install pymongo
Now, connect to a local MongoDB and perform CRUD operations:
from pymongo import MongoClient
# Connect (lazy — no network call until first op)
client = MongoClient('mongodb://localhost:27017')
# Access database and collection (still lazy)
db = client['shop']
products = db['products']
# Insert one document
result = products.insert_one({'name': 'Laptop', 'price': 1200, 'in_stock': True})
print(result.inserted_id) # ObjectId('...')
# Insert many
many = products.insert_many([
{'name': 'Mouse', 'price': 25, 'in_stock': True},
{'name': 'Keyboard', 'price': 75, 'in_stock': False},
{'name': 'Monitor', 'price': 300, 'in_stock': True},
])
print(len(many.inserted_ids)) # 3
# Find one
laptop = products.find_one({'name': 'Laptop'})
print(laptop)
# Find all in stock (cursor)
for doc in products.find({'in_stock': True}):
print(doc['name'], doc['price'])
client.close()
Expected output (IDs will vary):
ObjectId('60f7b1c9d5c2d3a1b2c3d4e5')
3
{'_id': ObjectId('...'), 'name': 'Laptop', 'price': 1200, 'in_stock': True}
Laptop 1200
Mouse 25
Monitor 300
Notice: find_one() returns a single dict, while find() returns a Cursor that you iterate over. Also, the _id is auto-generated as an ObjectId — you can supply your own, but that’s rare.
Using a with block for cleaner resource management
PyMongo supports context managers to ensure the client is closed:
from pymongo import MongoClient
with MongoClient('mongodb://localhost:27017') as client:
db = client.blog
posts = db.posts
posts.insert_one({'title': 'First post', 'views': 10})
total = posts.count_documents({})
print(f'Total posts: {total}')
Output:
Total posts: 1
Filtering with operators
PyMongo uses the same query operators as the shell — prefixed with $:
# Find products under $500
cheap = products.find({'price': {'$lt': 500}})
print([p['name'] for p in cheap])
# Count documents matching a filter
print(products.count_documents({'in_stock': True}))
Output:
['Mouse', 'Keyboard', 'Monitor']
3
Compare options / when to choose what
PyMongo isn’t the only way to talk to MongoDB from Python. Here’s a quick comparison:
| Option | Best for | Key trade-off |
|---|---|---|
| PyMongo (low-level) | Full control, custom queries, performance tuning | Verbose, manual schema validation |
| MongoEngine (ODM) | Modelling documents as classes, validation | Adds abstraction, learning curve |
| Motor (Async) | Asynchronous apps (FastAPI, aiohttp) | Requires async/await syntax |
| Raw HTTP/REST | Legacy systems, minimal dependencies | Reimplements protocol, insecure |
For most CRUD-heavy apps, PyMongo is the right start. If you need an object-document mapper, consider MongoEngine later. If you’re building an async app, Motor is built on PyMongo and shares most APIs — so learning PyMongo first pays off.
Pro tip: Always use
pymongo[srv]if your connection string usesmongodb+srv://(e.g., MongoDB Atlas). The standard install doesn’t include the DNS SRV resolution needed for that scheme.
Troubleshooting & edge cases
ServerSelectionTimeoutError
This is the most common error. Causes: server not running, wrong host/port, or unreachable network. Fix: ensure mongod is running, check the URI, and add serverSelectionTimeoutMS to fail faster:
client = MongoClient('mongodb://localhost:27017', serverSelectionTimeoutMS=2000)
InvalidURI
Malformed connection strings (e.g., missing scheme). Double-check for typos like missing // after mongodb:.
TypeError when passing wrong types
The driver is type-sensitive. For example, comparing a string to a numeric field won’t cast; use pymongo conversions or the right BSON types. If you need a concrete error, check that your queries use $lt numeric values, not strings.
Lazy connection surprises
Remember, MongoClient() doesn’t connect until an operation. If you instantiate but never run a query, you won’t know the server is down until the first operation — which might be much later. Use client.admin.command('ping') to test connectivity explicitly:
try:
client.admin.command('ping')
print('Connected')
except Exception as e:
print('Connection failed:', e)
Indexes and performance
Common mistake: querying large collections without indexes. This isn’t a driver error, but it leads to slow find() operations. Create indexes with collection.create_index([('field', 1)]) for fields you filter on frequently.
What you learned & what's next
You’ve now got the core of PyMongo basics under your belt: you can install the driver, connect to a MongoDB instance, and perform CRUD operations with insert_one, find, and count_documents. You understand the lazy connection model, the difference between a document and a cursor, and how to troubleshoot the most common connection errors. You also saw how to compare PyMongo with alternatives like Motor and MongoEngine, so you can choose the right tool for your next project.
This is the foundation for building real applications. As a next step in this MongoDB track, you’ll dive into advanced querying — using aggregation pipelines, indexing strategies, and transactions. You’ll also learn how to handle data migrations and work with MongoDB Atlas in the cloud. Armed with these basics, you’re ready to move from scripting to production-grade code.
Pro tip: Keep your connection strings out of code. Use environment variables or a config file — never hardcode credentials, especially if you push to GitHub.
Practice recap
Practice by writing a small inventory script: connect to a local MongoDB, create a products collection, insert 5 items, then query all in-stock items with a price filter. Next, add error handling to catch connection failures and re-run to confirm your code is robust. This will turn the hands-on example into muscle memory.
Common mistakes
- Forgetting to install
pymongo[srv]when using amongodb+srv://connection string; you get anInvalidURIerror instead of a clean install-time hint. - Assuming
MongoClient()connects eagerly — it’s lazy, so a typo in the hostname doesn’t raise until the first operation, causing confusing timeouts later. - Iterating over a
Cursortwice expecting the same results; cursors are single-use — re-create the query instead. - Using string types for numeric comparisons (e.g.,
{'price': {'$lt': '500'}}) — the driver doesn’t coerce, so you get wrong results or no matches.
Variations
- Motor — the async wrapper around PyMongo; use
awaitfor non-blocking I/O in FastAPI or asyncio apps. - MongoEngine — an ODM that maps documents to Python classes and adds validation, useful for complex domain models.
- PyMongo with
pymongo[srv]— the standard variation since it enables Atlas and anymongodb+srv://connection; keep it as your default install.
Real-world use cases
- A Flask backend that stores user sessions in a
sessionscollection and retrieves them per request viafind_one. - An ETL script that ingests logs into MongoDB using
insert_manyin batches, with aMongoClientconnecting to a replication set. - A CI/CD pipeline that spins up a test MongoDB instance and runs integration tests with
pymongoto validate data models.
Key takeaways
- PyMongo is the official Python driver — install with
pip install pymongo(add[srv]for Atlas). - The
MongoClientis lazy: connections happen on first operation, so always test withping()early. - Access databases and collections via
client.db.collection— they’re created automatically on first insert. - Use
insert_one/insert_manyto write,find_one/findto read —findreturns a single-use cursor. - Always check
ServerSelectionTimeoutErrorand validate connection strings before debugging queries. - PyMongo is the foundation for ODM (MongoEngine) and async (Motor) layers — master it first.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.