JSON-backed key-value store with simple APIs, fast in-memory operations, and a unified sync/async interface
BSD 3-Clause License · © Harrison Erd
from pickledb import PickleDB
# Bind to a JSON file; no I/O yet
db = PickleDB("data.json")
db.load()
db.set("username", "alice")
db.set("theme", {
"color": "blue",
"font": "sans-serif"
})
print(db.get("username")) # → "alice"
db.save() # atomically write to disk
import asyncio
from pickledb import PickleDB
async def main():
db = PickleDB("data.json")
await db.load()
await db.set("score", 42)
value = await db.get("score")
print(value) # → 42
await db.save()
asyncio.run(main())
Explicit I/O: The constructor PickleDB("data.json") never touches disk. load() and save() are explicit (or handled by context managers).
set(), get(), remove(), etc.os.replace to avoid partial writessave() or exit a context manager cleanly⚠️ Thread & Process Safety: PickleDB (the default JSON-backed class) is not thread-safe or process-safe. Multiple threads or processes writing to the same JSON file will cause data corruption.
When you need concurrency on a single machine: Use PickleDBSQLite instead. SQLite provides file-level locking and ACID transactions, making it safe for concurrent access across threads and processes on the same host.
⚡ Performance trade-off: PickleDBSQLite is significantly slower than PickleDB because it reads/writes to disk on every operation instead of keeping everything in memory. Use it only when you need the safety guarantees.
For networked or multi-host workloads: Neither class is suitable. Consider Redis, PostgreSQL, MongoDB, or mongoKV.
pickleDB offers two storage backends, each optimized for different use cases:
| Feature | PickleDB (orJSON) | PickleDBSQLite |
|---|---|---|
| Storage Model | In-memory with JSON persistence | Disk-based SQLite with orjson encoding |
| Performance | Very fast (all operations in RAM) | Slower (disk I/O on every operation) |
| Thread Safety | ❌ Not safe | ✅ Safe (SQLite locking) |
| Process Safety | ❌ Not safe (data corruption risk) | ✅ Safe (file-level locking) |
| ACID Transactions | ❌ No | ✅ Yes |
| Memory Usage | High (entire DB in RAM) | Low (only active queries in RAM) |
| Best For | Single-threaded apps, scripts, fast prototypes | Multi-threaded/process apps, ASGI servers |
| Dependencies | orjson and aiofiles |
orjson, aiofiles and aiosqlite |
Rule of thumb: Use PickleDB (JSON) by default for maximum performance. Only switch to PickleDBSQLite when you need thread/process safety or ACID guarantees, and can accept the performance penalty.
Automatically load and save your database with context managers:
from pickledb import PickleDB
with PickleDB("data.json") as db:
# On enter: db.load()
db.set("foo", "bar")
db.set("hello", "world")
# On exit: db.save()
import asyncio
from pickledb import PickleDB
async def main():
async with PickleDB("data.json") as db:
# On enter: await db.load()
await db.set("foo", "bar")
await db.set("hello", "world")
# On exit: await db.save()
asyncio.run(main())
pickleDB exposes two primary classes: PickleDB for JSON-on-disk, and PickleDBSQLite for an optional SQLite backend. All core methods share the same names in synchronous and asynchronous code — just add await when you're inside an async function.
load() -> bool
Load (or reload) the JSON file at location into memory. If the file is missing or empty, the in-memory database becomes an empty dict.
db.load()await db.load()True on success.save() -> bool
Atomically write the in-memory database to disk at location using a temporary file + os.replace().
db.save()await db.save()True on success.set(key: Any, value: Any) -> bool
Store value under key in the in-memory database. Keys are coerced to str, and values must be JSON-serializable (via orjson).
db.set("name", "alice")await db.set("name", "alice")True after updating the in-memory store (does not write to disk until save()).get(key: Any, default: Any | None = None) -> Any | None
Retrieve the value stored under key, or default if it doesn't exist.
db.get("name") or db.get("name", default="guest")await db.get("name")default (default is None).remove(key: Any) -> bool
Remove key from the in-memory database.
db.remove("name")await db.remove("name")True if the key existed and was removed, False otherwise.all() -> list[str]
Return a list of all keys currently stored in memory.
db.all()await db.all()str keys. Order is not guaranteed.purge() -> bool
Clear the in-memory database (equivalent to db.all() becoming an empty list). Does not touch disk until you call save().
db.purge()await db.purge()True.
Installation:
PickleDBSQLite lives behind an optional dependency. Install
aiosqlite yourself or use:
pip install "pickledb[sqlite]".
Why use it? SQLite adds file-level locking and ACID transactions, so concurrent readers and serialized writers across multiple threads/processes on the same machine are handled for you. It's still not a distributed database, but it's much safer than multiple processes writing to a plain JSON file.
⚠️ Performance warning: Unlike PickleDB, this class does NOT store data in memory. Every read and write hits the disk, making it significantly slower. Only use PickleDBSQLite when thread/process safety is essential.
set(key: str | None, value: Any) -> str
Store value in a SQLite table as an orjson-encoded BLOB. If key is
None, a new UUID key is generated and returned.
key = kv.set(None, {"foo": "bar"})key = await kv.set(None, {"foo": "bar"})get(key: str, default: Any = MISSING) -> Any
Look up a key in SQLite and deserialize it from orjson. If the key does not exist and no default is
provided, a KeyError is raised.
kv.get(key) or kv.get("missing", default=None)await kv.get(key)default, or raises KeyError.remove(key: str) -> bool
Delete key from the SQLite table.
kv.remove("key")await kv.remove("key")True if a row was deleted, False otherwise.all() -> list[str]
Return a list of all keys stored in the SQLite table.
kv.all()await kv.all()purge() -> bool
Remove all rows from the SQLite table.
kv.purge()await kv.purge()True.close() -> None
Close the underlying synchronous SQLite connection. In async code, returns a coroutine you should await.
kv.close()await kv.close()Important: pickleDB is intentionally method-based only. Dict-style access like db["key"] or db["key"] = value is not supported.
# Store a dictionary
db.set("user", {"name": "Alice", "age": 30})
# Update it
user = db.get("user")
user["age"] += 1
db.set("user", user)
print(db.get("user"))
# {'name': 'Alice', 'age': 31}
db.set("tasks", ["write", "test", "deploy"])
tasks = db.get("tasks", [])
tasks.append("celebrate")
db.set("tasks", tasks)
print(db.get("tasks"))
# ['write', 'test', 'deploy', 'celebrate']
db.set("user:1", {"name": "Alice"})
db.set("user:2", {"name": "Bob"})
def keys_with_prefix(db, prefix):
return [k for k in db.all() if k.startswith(prefix)]
print(keys_with_prefix(db, "user:"))
# ['user:1', 'user:2']
import time
def set_with_ttl(db, key, value, ttl_seconds):
db.set(key, {
"value": value,
"expires_at": time.time() + ttl_seconds,
})
def get_if_fresh(db, key):
data = db.get(key)
if not data:
return None
if time.time() < data.get("expires_at", 0):
return data["value"]
db.remove(key)
return None
set_with_ttl(db, "session", "active", ttl_seconds=5)
time.sleep(3)
print(get_if_fresh(db, "session")) # 'active'
time.sleep(3)
print(get_if_fresh(db, "session")) # None
Example timings loading, reading, and saving large JSON payloads using PickleDB (JSON) in async mode on a Dell XPS 9350, Ubuntu 24.04:
| Entries | Load (into memory) | Bulk Read | Save (to disk) |
|---|---|---|---|
| 1M | 0.68 s | 0.64 s | 0.03 s |
| 10M | 7.48 s | 7.27 s | 0.22 s |
| 50M | 43.36 s | 36.53 s | 1.09 s |
Benchmark Details: Full benchmark script available at this GitHub Gist.
PickleDBSQLite Performance: The SQLite backend is orders of magnitude slower than these numbers because it performs disk I/O on every operation. Use it only when you need thread/process safety.
PickleDBSQLite instead)In those cases, consider Redis, PostgreSQL, MongoDB, or mongoKV.