Insert and Query Documents with PyMongo
Learn how to insert and query documents with PyMongo in MongoDB. This hands-on tutorial covers core concepts, step-by-step operations, troubleshooting, and what to study next.
Focus: insert and query documents with pymongo
You’ve installed MongoDB, launched a server, and maybe even connected PyMongo. But if you’ve ever tried to actually put data into a collection and then get it back out, you know the pain: documents that silently fail to save, queries that return nothing when you expect results, and a gnawing feeling that you’re not using the full power of the database. This lesson strips away the guesswork. You’ll learn exactly how to insert and query documents with PyMongo — the two operations that sit at the heart of every MongoDB application — and you’ll walk away with patterns you can use immediately.
The problem this lesson solves
Copying and pasting MongoDB shell commands from a blog post is easy. But when you move to Python, the shell syntax changes, error messages become cryptic, and suddenly db.collection.find() turns into collection.find({}) with a dictionary where you expected a JSON string. The core problem this lesson solves is the friction between MongoDB’s document model and Python’s data structures. You need to translate MongoDB’s query language into Python-friendly dictionaries, handle the differences between insert_one and insert_many, and write queries that actually return the documents you want — not just the ones that happen to work by accident.
Beyond the syntax, there’s a deeper issue: consistency and confidence. When you insert without checking the result, you can’t be sure your data landed. When you query with a vague find() call, you might retrieve more data than you need, slowing down your application. This lesson gives you the tools to insert and query documents with precision, verify every operation, and handle edge cases before they become production incidents.
Core concept / mental model
Think of a MongoDB collection as a smart filing cabinet where each drawer is a document, and each document is a JSON-like object that stores your data. You don’t need to predefine a schema — the cabinet accepts whatever you slide in, as long as it’s valid BSON. The moment you insert a document, MongoDB assigns it a unique _id field (if you don’t provide one), which acts like a barcode for retrieval.
When you query, you’re not scanning the entire cabinet; you’re asking MongoDB to find documents that match a filter. The filter is just a Python dictionary that describes the fields and values you’re looking for. For example, {"status": "active"} tells MongoDB: “Give me every document where the status field equals 'active'.” This mental model — documents as flexible records, queries as filters — is the foundation for everything you’ll do.
Key terms to remember:
- Document: a single record, stored as a BSON object (binary JSON).
- Collection: a group of documents, analogous to a table in SQL.
- Filter: a dictionary that defines the criteria for matching documents.
- Cursor: the result of a query — a lazy, iterable object that fetches documents as you loop through it.
How it works step by step
The process of inserting and querying documents with PyMongo follows a straight-line path. Let’s break it down:
Step 1: Connect to MongoDB
Before anything else, you need a client instance. The MongoDB server runs on localhost:27017 by default. You create a MongoClient and then select a database and collection.
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["shop"] # or client.shop
products = db["products"] # or db.products
Pro tip:
client["shop"]andclient.shopare equivalent. Use the bracket notation if your database name contains characters that aren’t valid Python identifiers.
Step 2: Insert documents
MongoDB gives you two primary insertion methods: insert_one for a single document and insert_many for a list of documents. Both return result objects that confirm what happened.
Step 3: Query documents
Use find_one to get the first matching document, or find to get a cursor for all matches. Filters use standard comparison operators like $gt, $lt, $in, and $regex.
Step 4: Process the results
Iterate over the cursor or access fields from a single document. Always handle the possibility that no document matches — find_one returns None in that case.
The cause-and-effect chain is simple: insert → document exists in the collection; query → MongoDB scans the collection (or uses an index) and returns matching documents.
Hands-on walkthrough
Now let’s put that theory into practice. Below are complete, runnable examples. I assume you have MongoDB running locally and PyMongo installed (pip install pymongo).
Example 1: Insert a single document and verify
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["shop"]
products = db["products"]
product = {
"name": "Laptop",
"price": 999.99,
"in_stock": True,
"tags": ["electronics", "computers"]
}
result = products.insert_one(product)
print("Inserted ID:", result.inserted_id)
# Verify the insert
query = {"_id": result.inserted_id}
doc = products.find_one(query)
print("Found document:", doc)
Expected output:
Inserted ID: 65f2a1b4c9e3f5a7d8b0c1d2
Found document: {'_id': ObjectId('65f2a1b4c9e3f5a7d8b0c1d2'), 'name': 'Laptop', 'price': 999.99, 'in_stock': True, 'tags': ['electronics', 'computers']}
Notice that the _id was auto-generated. If you want to control the _id, just include it in the document — MongoDB will honor your value.
Example 2: Insert multiple documents and query with filters
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["shop"]
products = db["products"]
# Insert several products at once
many_products = [
{"name": "Mouse", "price": 29.99, "in_stock": False, "tags": ["electronics"]},
{"name": "Keyboard", "price": 89.99, "in_stock": True, "tags": ["electronics"]},
{"name": "Desk", "price": 199.99, "in_stock": True, "tags": ["furniture"]},
]
result = products.insert_many(many_products)
print("Inserted IDs:", result.inserted_ids)
# Query all in-stock products
in_stock = list(products.find({"in_stock": True}))
print("In-stock products:")
for prod in in_stock:
print(f"- {prod['name']} at ${prod['price']}")
# Query products under $100
cheap = list(products.find({"price": {"$lt": 100}}))
print("Products under $100:")
for prod in cheap:
print(f"- {prod['name']} at ${prod['price']}")
Expected output:
Inserted IDs: [ObjectId('65f2a1b4c9e3f5a7d8b0c1d3'), ObjectId('65f2a1b4c9e3f5a7d8b0c1d4'), ObjectId('65f2a1b4c9e3f5a7d8b0c1d5')]
In-stock products:
- Laptop at $999.99
- Keyboard at $89.99
- Desk at $199.99
Products under $100:
- Mouse at $29.99
- Keyboard at $89.99
Example 3: More advanced queries with operators and projections
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["shop"]
products = db["products"]
# Products that are either in the 'electronics' tag OR have a price >= 500
query = {
"$or": [
{"tags": "electronics"},
{"price": {"$gte": 500}}
]
}
result = list(products.find(query))
print("Matches electronics or expensive:")
for prod in result:
print(f"- {prod['name']} (${prod['price']})")
# Project only the 'name' and 'price' fields, exclude _id
projection = {"_id": 0, "name": 1, "price": 1}
names = list(products.find({}, projection))
print("All products (name & price):")
for prod in names:
print(f"- {prod}")
Expected output (will vary based on your existing data):
Matches electronics or expensive:
- Laptop ($999.99)
- Mouse ($29.99)
- Keyboard ($89.99)
- Desk ($199.99)
All products (name & price):
- {'name': 'Laptop', 'price': 999.99}
- {'name': 'Mouse', 'price': 29.99}
- {'name': 'Keyboard', 'price': 89.99}
- {'name': 'Desk', 'price': 199.99}
Pro tip: Use projections to reduce network transfer and memory usage. Only fetch the fields you actually need.
Compare options / when to choose what
When inserting documents, you have two main methods; when querying, you have multiple operators and tools. Here’s a comparison to help you decide.
Insertion: insert_one vs insert_many
| Method | When to use | Performance & behavior |
|---|---|---|
insert_one |
Single document, need to reuse the result ID immediately | One round-trip to server, returns inserted_id |
insert_many |
Batch inserts, e.g., initial data load or log ingestion | Single round-trip for many documents, returns list of IDs |
Query operators: $eq, $gt, $in, $regex
| Operator | Use case | Example query |
|---|---|---|
$eq |
Exact match (default if no operator) | {"name": "Laptop"} |
$gt/$lt |
Range comparisons for numbers/dates | {"price": {"$gt": 100}} |
$in |
Match any value in a list | {"tags": {"$in": ["electronics", "furniture"]}} |
$regex |
Pattern matching on strings | {"name": {"$regex": "^L"}} |
Query methods: find_one vs find
| Method | Returns | When to use |
|---|---|---|
find_one |
Single document or None |
Get specific record by ID, check existence |
find |
Cursor (iterable) | Get all matching documents, often with filters |
Insert vs Update
Inserting always creates a new document. If you need to modify an existing one, use update_one or replace_one. Inserting a document with an _id that already exists will raise a DuplicateKeyError.
Troubleshooting & edge cases
Even with the right approach, things can go wrong. Here are the most common pitfalls and how to fix them.
Error: pymongo.errors.PyMongoError: connection refused
Cause: MongoDB server isn’t running, or the connection string is wrong.
Fix: Start MongoDB (mongod) and check the port. Use ps aux | grep mongod on Linux/macOS or Task Manager on Windows.
Error: DuplicateKeyError on insert
Cause: You tried to insert a document with an _id that already exists.
Fix: Check if the _id is supposed to be unique. If you need upsert behavior, use update_one with upsert=True.
Empty result when you expect documents
Cause: The filter doesn’t match any document, or you’re querying a different collection/database.
Fix: Verify your collection name and database name. Use collection.find_one({}) to see if any data exists. Print your filter to ensure types match (e.g., "price" as a string vs. number).
Type mismatches
Cause: Querying with a string where the field stores a number, or vice versa.
Example: {"price": "100"} won’t match price: 100.
Fix: Convert your variable to the correct type before inserting or querying.
Slow queries with many documents
Cause: Missing indexes on frequently queried fields.
Fix: Create an index on the filter fields, e.g., products.create_index("name"). This is essential for production performance.
find returns a cursor, not a list
Beginners often expect find to return a list. Remember, find is lazy — you must iterate over it, or convert to a list with list(). If you forget, you’ll get a Cursor object, which can be confusing.
What you learned & what's next
You now know how to insert and query documents with PyMongo — from connecting to a database, inserting single or multiple documents, to querying with filters, operators, and projections. You’ve also learned how to troubleshoot common issues like connection errors, duplicate keys, and type mismatches. These skills are the bedrock of any MongoDB application, whether you’re building a simple script or a full-scale web service.
What’s next? Now that you can get data in and out, you should move on to updating and deleting documents — the natural next step in your MongoDB journey. You’ll learn how to modify existing documents with update_one, update_many, and replace_one, and how to remove them with delete_one and delete_many. Mastering the full CRUD cycle will make you confident in handling any data operation with PyMongo.
Practice recap
Now it's your turn: create a new collection called students and insert five student documents with fields like name, age, grade, and subjects (a list). Practice querying all students above a certain age, students in a specific subject, and use a projection to show only names. Try inserting a duplicate _id to see the error, then fix it with an update.
Common mistakes
- Querying with a string instead of a number (e.g.,
{"price": "100"}) when the field stores an integer — this silently returns no matches. - Forgetting to convert a
Cursorto a list — callingprint(cursor)showspymongo.cursor.Cursorinstead of your documents. - Assuming
insert_manyis atomic — if one document has an invalid field, the entire operation fails unless you setordered=False. - Using
find_onewithout checking forNone— accessing fields on aNoneresult raisesTypeError. - Ignoring indexes — querying large collections without indexes leads to slow performance and timeouts.
Variations
- Use
Motor(async PyMongo) for asynchronous web frameworks like FastAPI or Tornado. - Use
bulk_writefor complex mixed operations (insert, update, delete) in a single batch. - Use the MongoDB Aggregation Pipeline (
aggregate()) instead of multiple queries when you need to transform or join data.
Real-world use cases
- E-commerce catalog: insert thousands of products with
insert_many, then query by category and price range for storefront displays. - IoT sensor data: append time-series readings as documents, then query recent data with
$gton timestamps for dashboards. - User activity logs: store each event as a document, filter by user ID and date range for analytics and audit trails.
Key takeaways
- Connect with
MongoClientand use bracket or attribute notation to access databases and collections. - Use
insert_onefor single documents andinsert_manyfor batches; always check theinserted_id(s)to confirm success. - Query with
find_oneto get a single document andfindto get a cursor; always iterate or convert to a list. - Master comparison operators like
$gt,$lt,$in, and$regexto build precise filters. - Use projections to retrieve only the fields you need, improving performance and reducing bandwidth.
- Troubleshoot by checking connection strings, verifying types in filters, and creating indexes for speed.
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.