Persistent Memory for a Python Agent: Start with a Tested Store
Build a scoped, durable memory baseline in Python, then decide when semantic retrieval and a managed memory service are useful.

Persistent memory in a Python agent starts with durable records and a retrieval path. Python itself does not make your application forget: a new model request simply sees the input you send it. Store useful context outside the request, associate it with the authenticated user, and retrieve relevant records before answering.
Start with a small, observable baseline. A database of explicit preferences is easier to validate than a system that silently extracts facts from every generated response. Add semantic search when exact lookup no longer serves the questions your users ask.
A runnable local baseline
This example uses Python's standard-library SQLite support. It stores explicit facts, survives reopening the database, and separates users within tenants. Save it as memory_store.py and import the MemoryStore class in your application. It performs exact key lookup; it does not implement embeddings, automatic extraction, or a complete chatbot.
import sqlite3
class MemoryStore:
def __init__(self, path):
self.db = sqlite3.connect(path)
self.db.execute("""CREATE TABLE IF NOT EXISTS facts (
tenant TEXT NOT NULL, user_id TEXT NOT NULL,
fact_key TEXT NOT NULL, value TEXT NOT NULL,
PRIMARY KEY (tenant, user_id, fact_key))""")
self.db.commit()
def put(self, tenant, user_id, key, value):
if not all(isinstance(x, str) and x.strip()
for x in (tenant, user_id, key, value)):
raise ValueError("All fields must be non-empty strings")
with self.db:
self.db.execute("""INSERT INTO facts VALUES (?, ?, ?, ?)
ON CONFLICT(tenant, user_id, fact_key)
DO UPDATE SET value = excluded.value""",
(tenant, user_id, key, value))
def get(self, tenant, user_id, key):
row = self.db.execute("""SELECT value FROM facts
WHERE tenant = ? AND user_id = ? AND fact_key = ?""",
(tenant, user_id, key)).fetchone()
return row[0] if row else None
def forget(self, tenant, user_id, key):
with self.db:
self.db.execute("""DELETE FROM facts
WHERE tenant = ? AND user_id = ? AND fact_key = ?""",
(tenant, user_id, key))
def close(self):
self.db.close()
The application must supply tenant and user identity from its authenticated session. The database filters separate records; they do not authenticate a caller. An endpoint that accepts arbitrary user IDs from the browser defeats that boundary.
Put memory around the model call
For a scheduling assistant, look up the user's preferred meeting timezone, attach that value as background data, and let the current request override an older preference when appropriate. If no record exists, ask or proceed without personalization. Do not fill the gap with an invented preference.
Write a change only after the user actually states or confirms it. If an assistant suggests a timezone, that suggestion should not automatically become a user fact. Keep important transactional state, such as whether a meeting was booked, in the scheduling system that owns the operation.
The example overwrites a value for one key. Applications that need historical answers should store revisions and effective times instead. The temporal-memory guide explains that distinction.
Test persistence independently of answer quality
- Save a fictional preference and close the store.
- Open a new store instance against the same file and retrieve it.
- Ask for the same key under another user and another tenant; both should be absent.
- Correct the preference, reopen the database, and verify the new value.
- Delete it, reopen again, and verify absence.
Those checks exercise storage behavior without paying for model calls. A second layer of tests should verify that the assistant uses relevant facts, ignores irrelevant ones, and treats stored text as data rather than new instructions. A successful database test does not establish successful model behavior.
When to add Supermemory
Exact keys work well for a small preference schema. Free-form conversations and documents may need extraction, semantic retrieval, profiles, and a broader correction lifecycle. The Supermemory SDK guide describes the Python client; keep its API key on the server and use the same authorized scope on writes and reads.
A managed write can be accepted before its content becomes searchable. Track processing state and bound any wait rather than immediately interpreting an empty search as data loss. Keep a mapping from your source record to the provider's document ID so corrections and deletion have an explicit target.
What this baseline proves
The accompanying local checks cover reopening, updates, deletion, and scope separation. They do not measure semantic recall, a provider's availability, or an end-to-end deployed chatbot. Start with this contract, then compare a managed implementation against the same cases. Use the memory lifecycle guide to extend the acceptance criteria before expanding the number of stored facts.
Ready to compare the local store with a managed implementation? Get a Supermemory API key and follow the Python SDK guide above. Run the same reopen, scope, correction, and deletion cases against your application’s new path.