With Django's low-level cache API, how do cache.set(), cache.get() and cache.add() behave, and what do timeout=None and timeout=0 mean?
answer
- any picklable value
- a miss returns the default
- add only when absent
- None forever, 0 not at all
basics
~20 sDjango's cache.set(key, value, timeout) stores a picklable value, cache.get(key, default) returns it or the default on a miss, and cache.add() stores only if the key is absent. timeout=None means never expire; timeout=0 means do not cache.
solid answer
~40 s`from django.core.cache import cache` gives the default alias. `cache.set("leaderboard:weekly", rows, 300)` stores any picklable object for 300 seconds; leaving `timeout` out uses the alias's `TIMEOUT` (default 300). `cache.get(key)` returns the value or `None` on a miss, or the `default` you pass; to tell a cached `None` from a miss, pass a sentinel such as `object()`. `cache.add(key, value)` writes only when the key is not already present and returns `True` or `False`. For the timeout, `None` stores the value with no expiry and `0` does not cache it, so `set(key, value, 0)` effectively removes whatever was there. Every method also has an `a`-prefixed twin for async code, such as `await cache.aget(key)`.
go deeper
Recall set, get with a default, add, and that None means forever while 0 means not cached.
Explain the TIMEOUT fallback, pickling, the sentinel pattern for cached None, and add's return value.
Choose timeouts deliberately per value, and use add's first-writer-wins behaviour where concurrent writers meet.
Set conventions for keys, timeouts and what may be cached so a team's cache usage stays predictable.
## The low-level API in one import Django's **low-level cache API** is for caching values, not whole pages: a computed leaderboard, the result of an expensive query, a rendered snippet, an API response. You import a backend object and call methods on it: ```python from django.core.cache import cache # the "default" alias from django.core.cache import caches # caches["other"] for other aliases ``` Every built-in backend implements the same methods, so code does not change when the backend does. ## set, get and add | Method | What it does | Returns | |---|---|---| | `cache.set(key, value, timeout=..., version=None)` | Stores or overwrites the value | `None` | | `cache.get(key, default=None, version=None)` | Reads the value | The value, or `default` on a miss | | `cache.add(key, value, timeout=..., version=None)` | Stores only if the key is absent | `True` if stored, `False` otherwise | - **Keys** should be strings. Keep them short and free of spaces: the Memcached backends reject keys over 250 characters or containing whitespace, and the other backends warn about them. - **Values** can be any **picklable** Python object: lists of dicts, model instances, dataclasses. Lambdas, open files and database connections cannot be pickled. - **`add()`** is the "only if empty" write. It is the building block for "first writer wins" logic, and on Redis and Memcached it maps to an atomic server-side operation. ## Timeout semantics The `timeout` argument is in **seconds**: 1. **Omitted**: uses the alias's `TIMEOUT` from `CACHES`, which defaults to `300`. 2. **A positive number**: expire after that many seconds. 3. **`None`**: store with **no expiry**. The entry stays until it is deleted, overwritten or evicted by the backend. 4. **`0`**: **do not cache**. The value expires immediately; on several backends a `set()` with `0` simply deletes the existing key. Confusing `0` with "forever" is the classic mistake: code meant to cache a leaderboard permanently with `timeout=0` caches nothing, and every request recomputes it. ## Telling a cached None from a miss `cache.get()` returns `None` both when the key is missing and when the stored value is `None`. If `None` is a legitimate result, for example "no leader yet this week", pass a sentinel: ```python MISSING = object() value = cache.get("leaderboard:weekly", MISSING) if value is MISSING: value = compute_leaderboard() cache.set("leaderboard:weekly", value, 300) ``` ## Related calls worth knowing - **`cache.has_key(key)`** and **`key in cache`** report whether a live, unexpired entry exists. - **`cache.get_or_set(key, default, timeout)`** combines the read, the fallback and the write; `default` may be a callable. - **`cache.delete(key)`** removes one entry and returns whether a key was deleted. - **Async twins**: `aget`, `aset`, `aadd`, `adelete` and the rest take the same arguments and are awaited in async views. ## Choosing keys and values A few habits keep low-level caching predictable: - **Namespace keys by purpose**: `leaderboard:weekly`, `leaderboard:weekly:page:2`, `player:42:rank`. Readable keys are easy to delete deliberately and easy to find when debugging. - **Put every input in the key**: if the leaderboard depends on the season and the region, both belong in the key, otherwise one region's rows are served to another. - **Cache plain data rather than live objects**: a list of dicts or tuples pickles small and survives code changes better than full model instances, which drag their whole field state and class path into the pickle. - **Keep values modest**: large pickles cost time to serialise on every write and read, and Memcached rejects values above its item size limit. - **Pick the timeout from how stale the data may be**, not from how expensive it is to compute; expensive data that must be fresh needs invalidation, not a long timeout. ## A leaderboard example A view shows the weekly leaderboard, which needs an expensive aggregate query: - Read with `cache.get("leaderboard:weekly")`. - On a miss, run the query, then `cache.set("leaderboard:weekly", rows, 300)`. - When a match result is recorded, call `cache.delete("leaderboard:weekly")` so the next reader rebuilds it. That is the whole pattern at junior level; the traps come from concurrency, invalidation and the choice of backend.
- Why can cache.get() not tell you whether a key holds None?`cache.get()` returns its `default`, which is `None` unless you pass another value, both on a miss and when the stored value is `None`. Passing a unique sentinel such as `object()` as the default separates the two cases, because a miss returns that exact object.
- What does cache.add() return, and when would you use it instead of set()?It returns `True` if it stored the value and `False` if the key already existed. Use it when the first writer must win, for example initialising a counter before `incr()`, or claiming a short-lived marker so only one worker rebuilds an expensive value.
A timeout of None is a note pinned to the board with no date; a timeout of 0 is a note that you throw away the moment you write it.
saying these in an interview costs you the question
- timeout=0 caches the value forever.
- Omitting timeout stores the value with no expiry.
- cache.get() raises KeyError when the key is missing.
- cache.add() overwrites an existing value like set().
- Only strings and numbers can be stored in Django's cache.