Sessions & Cookies
HTTP is stateless: every request Bookshelf receives arrives with no memory of
any request before it. The server that handles a visitor loading the book list has no idea,
on its own, whether that same visitor just added a book thirty seconds ago, or is logged in at
all — each request is a clean slate unless something is done to bridge that gap. Flask's
answer is the session object, backed underneath by browser cookies.
The session Object
Flask's session behaves like a Python dictionary — you read and write keys on it
exactly as you would a dict — but its contents persist across separate requests
from the same visitor. Under the hood, Flask serializes whatever you put into
session, cryptographically signs it, and stores the result in a cookie sent to the
visitor's browser. On the next request, Flask reads that cookie back, verifies the signature,
and reconstructs the session dictionary — all automatically, before your view
function runs.
Signed, Not Encrypted — a Distinction Worth Sitting With
This is the single most important thing to understand about Flask's session:
signed does not mean hidden. The cookie's contents are plainly readable by
anyone who opens their browser's dev tools — session data is stored client-side, in the
visitor's own browser. What the cryptographic signature guarantees is that if the cookie's
contents are altered by the visitor (or anyone else) after Flask signed them, Flask will detect
the mismatch and reject the tampered cookie outright.
Signed cookie (Flask's session) | |
|---|---|
| Can the visitor read it? | Yes — it's stored in their own browser, in plain view |
| Can the visitor alter it undetected? | No — any change breaks the signature, and Flask discards the cookie |
Worked Example: Remembering a Recently Viewed Book
A small, genuinely useful bit of state for Bookshelf: remembering the last book a visitor looked at, so a "Jump back in" link can appear on their next visit.
# app.py
from flask import Flask, session, render_template
app = Flask(__name__)
app.config['SECRET_KEY'] = 'a-long-random-value-kept-out-of-source-control'
@app.route('/books/<int:book_id>')
def book_detail(book_id):
book = Book.query.get_or_404(book_id)
session['recently_viewed_book_id'] = book.id # written on every visit
return render_template('book_detail.html', book=book)
@app.route('/')
def home():
recent_id = session.get('recently_viewed_book_id') # read back on a later request
recent_book = Book.query.get(recent_id) if recent_id else None
return render_template('home.html', recent_book=recent_book)
Notice session.get('recently_viewed_book_id') rather than
session['recently_viewed_book_id'] — exactly like a plain dict,
.get() returns None instead of raising a KeyError when a
visitor arrives at home without ever having viewed a book.
Why session Requires a SECRET_KEY
Without app.config['SECRET_KEY'] set, any attempt to write to session
raises a RuntimeError — Flask refuses to sign a cookie with no key to sign it
with. This is the same SECRET_KEY covered in
Configuration & Environments and used by
Flask-WTF's CSRF protection in Form Validation & CSRF
— one configuration value doing cryptographic duty for both the session cookie and the CSRF
token. It should be a long, random, unguessable string, generated once and kept out of source
control, exactly as that lesson describes.
Plain Cookies, Set Directly
Not every small piece of per-visitor state belongs in the signed session — a display
preference like a light/dark theme toggle, for instance, doesn't need cryptographic signing at
all; there's nothing sensitive about it, and it doesn't matter if a visitor edits it in dev
tools. For cases like this, Flask lets you set a cookie directly on a response object, built
with make_response() (the same response-building tool covered in
Request & Response Objects):
# app.py
from flask import make_response, request
@app.route('/set-theme/<theme>')
def set_theme(theme):
resp = make_response('Theme updated')
resp.set_cookie('theme', theme) # plain cookie, not signed via session
return resp
@app.route('/')
def home():
theme = request.cookies.get('theme', 'light') # default if the cookie was never set
return render_template('home.html', theme=theme)
Reading it back is request.cookies.get('theme') — a plain dictionary-style lookup
on the incoming request, with an optional default for visitors who've never set one.
Cookie Security Options: SESSION_COOKIE_SECURE and SESSION_COOKIE_HTTPONLY
Two config values control how Flask's session cookie itself behaves in the browser:
| Config key | What it does |
|---|---|
SESSION_COOKIE_HTTPONLY | When True (the default), blocks JavaScript from reading the session cookie at all — a defense against certain cross-site scripting attacks stealing it |
SESSION_COOKIE_SECURE | When True, tells the browser to only ever send the cookie over HTTPS, never plain HTTP |
Both matter far more in production than in local development, and the full picture of tuning these safely — alongside the rest of Bookshelf's security posture — is covered later in Security Best Practices.
Clearing Session Data on Logout
Ending a session — most commonly, when a user logs out — means removing what's stored in it. Two options exist depending on how much you want to clear:
@app.route('/logout')
def logout():
session.pop('user_id', None) # remove just this one key
# or, to remove everything the session was holding:
# session.clear()
return redirect(url_for('home'))
session.pop('user_id', None) mirrors ordinary dict behavior — removing the key if
present, doing nothing if it's already absent, with no KeyError either way.
session.clear() is the blunter tool, wiping the whole session at once. In a real
Bookshelf login system, Flask-Login (covered in
Authentication with Flask-Login) manages
most of this session bookkeeping for you — but knowing what it's doing underneath makes that
extension far less of a black box.